{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n  \"name\": \"@jspsych/plugin-image-button-response\",\n  \"version\": \"2.2.0\",\n  \"description\": \"jsPsych plugin for displaying a stimulus and getting a button response\",\n  \"type\": \"module\",\n  \"main\": \"dist/index.cjs\",\n  \"exports\": {\n    \"import\": \"./dist/index.js\",\n    \"require\": \"./dist/index.cjs\"\n  },\n  \"typings\": \"dist/index.d.ts\",\n  \"unpkg\": \"dist/index.browser.min.js\",\n  \"files\": [\n    \"src\",\n    \"dist\"\n  ],\n  \"source\": \"src/index.ts\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"test:watch\": \"npm test -- --watch\",\n    \"tsc\": \"tsc\",\n    \"build\": \"rollup --config\",\n    \"build:watch\": \"npm run build -- --watch\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n    \"directory\": \"packages/plugin-image-button-response\"\n  },\n  \"author\": \"Josh de Leeuw\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n  },\n  \"homepage\": \"https://www.jspsych.org/latest/plugins/image-button-response\",\n  \"peerDependencies\": {\n    \"jspsych\": \">=7.1.0\"\n  },\n  \"devDependencies\": {\n    \"@jspsych/config\": \"^3.3.1\",\n    \"@jspsych/test-utils\": \"^1.2.0\"\n  }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n  name: \"image-button-response\",\n  version: version,\n  parameters: {\n    /** The path of the image file to be displayed. */\n    stimulus: {\n      type: ParameterType.IMAGE,\n      default: undefined,\n    },\n    /** Set the height of the image in pixels. If left null (no value specified), then the image will display at its natural height. */\n    stimulus_height: {\n      type: ParameterType.INT,\n      default: null,\n    },\n    /** Set the width of the image in pixels. If left null (no value specified), then the image will display at its natural width. */\n    stimulus_width: {\n      type: ParameterType.INT,\n      default: null,\n    },\n    /** If setting *only* the width or *only* the height and this parameter is true, then the other dimension will be\n     * scaled to maintain the image's aspect ratio.  */\n    maintain_aspect_ratio: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /** Labels for the buttons. Each different string in the array will generate a different button. */\n    choices: {\n      type: ParameterType.STRING,\n      default: undefined,\n      array: true,\n    },\n    /**\n     * ``(choice: string, choice_index: number)=>`<button class=\"jspsych-btn\">${choice}</button>``; | A function that\n     * generates the HTML for each button in the `choices` array. The function gets the string and index of the item in\n     * the `choices` array and should return valid HTML. If you want to use different markup for each button, you can do\n     * that by using a conditional on either parameter. The default parameter returns a button element with the text\n     * label of the choice.\n     */\n    button_html: {\n      type: ParameterType.FUNCTION,\n      default: function (choice: string, choice_index: number) {\n        return `<button class=\"jspsych-btn\">${choice}</button>`;\n      },\n    },\n    /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that\n     * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */\n    prompt: {\n      type: ParameterType.HTML_STRING,\n      default: null,\n    },\n    /** How long to show the stimulus for in milliseconds. If the value is null, then the stimulus will be shown until\n     * the participant makes a response. */\n    stimulus_duration: {\n      type: ParameterType.INT,\n      default: null,\n    },\n    /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant\n     * fails to make a response before this timer is reached, the participant's response will be recorded as null for the\n     * trial and the trial will end. If the value of this parameter is null, the trial will wait for a response indefinitely. */\n    trial_duration: {\n      type: ParameterType.INT,\n      default: null,\n    },\n    /** Setting to `'grid'` will make the container element have the CSS property `display: grid` and enable the use of\n     * `grid_rows` and `grid_columns`. Setting to `'flex'` will make the container element have the CSS property\n     * `display: flex`. You can customize how the buttons are laid out by adding inline CSS in the `button_html` parameter.  */\n    button_layout: {\n      type: ParameterType.STRING,\n      default: \"grid\",\n    },\n    /**\n     * The number of rows in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the\n     *  number of rows will be determined automatically based on the number of buttons and the number of columns.\n     */\n    grid_rows: {\n      type: ParameterType.INT,\n      default: 1,\n    },\n    /**\n     * The number of columns in the button grid. Only applicable when `button_layout` is set to `'grid'`. If null, the\n     * number of columns will be determined automatically based on the number of buttons and the number of rows.\n     */\n    grid_columns: {\n      type: ParameterType.INT,\n      default: null,\n    },\n    /** If true, then the trial will end whenever the participant makes a response (assuming they make their response\n     * before the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until\n     * the value for `trial_duration` is reached. You can set this parameter to `false` to force the participant to\n     * view a stimulus for a fixed amount of time, even if they respond before the time is complete. */\n    response_ends_trial: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /**\n     * If true, the image will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive image trials in some browsers, like Firefox and Edge.\n     * If false, the image will be shown via an img element, as in previous versions of jsPsych. If the stimulus is an **animated gif**, you must set this parameter to false, because the canvas rendering method will only present static images.\n     */\n    render_on_canvas: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /** How long the button will delay enabling in milliseconds. */\n    enable_button_after: {\n      type: ParameterType.INT,\n      default: 0,\n    },\n  },\n  data: {\n    /** The path of the image that was displayed. */\n    stimulus: {\n      type: ParameterType.STRING,\n    },\n    /** Indicates which button the participant pressed. The first button in the `choices` array is 0, the second is 1, and so on.  */\n    response: {\n      type: ParameterType.INT,\n    },\n    /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */\n    rt: {\n      type: ParameterType.INT,\n    },\n  },\n  // prettier-ignore\n  citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin displays an image and records responses generated with a button click. The stimulus can be displayed until\n * a response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant\n * has failed to respond within a fixed length of time. The button itself can be customized using HTML formatting.\n *\n * Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you\n * are using timeline variables or another dynamic method to specify the image stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the images.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/image-button-response/ image-button-response plugin documentation on jspsych.org}\n */\nclass ImageButtonResponsePlugin implements JsPsychPlugin<Info> {\n  static info = info;\n\n  constructor(private jsPsych: JsPsych) {}\n\n  trial(display_element: HTMLElement, trial: TrialType<Info>) {\n    const calculateImageDimensions = (image: HTMLImageElement): [number, number] => {\n      let width: number, height: number;\n      // calculate image height and width - this can only be done after image loads because it uses\n      // the image's naturalWidth/naturalHeight properties\n      if (trial.stimulus_height !== null) {\n        height = trial.stimulus_height;\n        if (trial.stimulus_width == null && trial.maintain_aspect_ratio) {\n          width = image.naturalWidth * (trial.stimulus_height / image.naturalHeight);\n        }\n      } else {\n        height = image.naturalHeight;\n      }\n      if (trial.stimulus_width !== null) {\n        width = trial.stimulus_width;\n        if (trial.stimulus_height == null && trial.maintain_aspect_ratio) {\n          height = image.naturalHeight * (trial.stimulus_width / image.naturalWidth);\n        }\n      } else if (!(trial.stimulus_height !== null && trial.maintain_aspect_ratio)) {\n        // if stimulus width is null, only use the image's natural width if the width value wasn't set\n        // in the if statement above, based on a specified height and maintain_aspect_ratio = true\n        width = image.naturalWidth;\n      }\n\n      return [width, height];\n    };\n\n    display_element.innerHTML = \"\";\n    let stimulusElement: HTMLCanvasElement | HTMLImageElement;\n    let canvas: HTMLCanvasElement;\n\n    const image = trial.render_on_canvas ? new Image() : document.createElement(\"img\");\n\n    if (trial.render_on_canvas) {\n      canvas = document.createElement(\"canvas\");\n      canvas.style.margin = \"0\";\n      canvas.style.padding = \"0\";\n      stimulusElement = canvas;\n    } else {\n      stimulusElement = image;\n    }\n\n    const drawImage = () => {\n      const [width, height] = calculateImageDimensions(image);\n      if (trial.render_on_canvas) {\n        canvas.width = width;\n        canvas.height = height;\n        canvas.getContext(\"2d\").drawImage(image, 0, 0, width, height);\n      } else {\n        image.style.width = `${width}px`;\n        image.style.height = `${height}px`;\n      }\n    };\n\n    let hasImageBeenDrawn = false;\n\n    // if image wasn't preloaded, then it will need to be drawn whenever it finishes loading\n    image.onload = () => {\n      if (!hasImageBeenDrawn) {\n        drawImage();\n      }\n    };\n\n    image.src = trial.stimulus;\n    if (image.complete && image.naturalWidth !== 0) {\n      // if image has loaded then draw it now (don't rely on img onload function to draw image\n      // when image is in the cache, because that causes a delay in the image presentation)\n      drawImage();\n      hasImageBeenDrawn = true;\n    }\n\n    stimulusElement.id = \"jspsych-image-button-response-stimulus\";\n    display_element.appendChild(stimulusElement);\n\n    // Display buttons\n    const buttonGroupElement = document.createElement(\"div\");\n    buttonGroupElement.id = \"jspsych-image-button-response-btngroup\";\n    if (trial.button_layout === \"grid\") {\n      buttonGroupElement.classList.add(\"jspsych-btn-group-grid\");\n      if (trial.grid_rows === null && trial.grid_columns === null) {\n        throw new Error(\n          \"You cannot set `grid_rows` to `null` without providing a value for `grid_columns`.\"\n        );\n      }\n      const n_cols =\n        trial.grid_columns === null\n          ? Math.ceil(trial.choices.length / trial.grid_rows)\n          : trial.grid_columns;\n      const n_rows =\n        trial.grid_rows === null\n          ? Math.ceil(trial.choices.length / trial.grid_columns)\n          : trial.grid_rows;\n      buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`;\n      buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`;\n    } else if (trial.button_layout === \"flex\") {\n      buttonGroupElement.classList.add(\"jspsych-btn-group-flex\");\n    }\n\n    for (const [choiceIndex, choice] of trial.choices.entries()) {\n      buttonGroupElement.insertAdjacentHTML(\"beforeend\", trial.button_html(choice, choiceIndex));\n      const buttonElement = buttonGroupElement.lastChild as HTMLElement;\n      buttonElement.dataset.choice = choiceIndex.toString();\n      buttonElement.addEventListener(\"click\", () => {\n        after_response(choiceIndex);\n      });\n    }\n\n    display_element.appendChild(buttonGroupElement);\n\n    // Show prompt if there is one\n    if (trial.prompt !== null) {\n      display_element.insertAdjacentHTML(\"beforeend\", trial.prompt);\n    }\n\n    // start timing\n    var start_time = performance.now();\n\n    // store response\n    var response = {\n      rt: null,\n      button: null,\n    };\n\n    // function to end trial when it is time\n    const end_trial = () => {\n      // gather the data to store for the trial\n      var trial_data = {\n        rt: response.rt,\n        stimulus: trial.stimulus,\n        response: response.button,\n      };\n\n      // move on to the next trial\n      this.jsPsych.finishTrial(trial_data);\n    };\n\n    // function to handle responses by the subject\n    function after_response(choice) {\n      // measure rt\n      var end_time = performance.now();\n      var rt = Math.round(end_time - start_time);\n      response.button = parseInt(choice);\n      response.rt = rt;\n\n      // after a valid response, the stimulus will have the CSS class 'responded'\n      // which can be used to provide visual feedback that a response was recorded\n      stimulusElement.classList.add(\"responded\");\n\n      // disable all the buttons after a response\n      for (const button of buttonGroupElement.children) {\n        button.setAttribute(\"disabled\", \"disabled\");\n      }\n\n      if (trial.response_ends_trial) {\n        end_trial();\n      }\n    }\n\n    function enable_buttons() {\n      var btns = document.querySelectorAll(\"#jspsych-image-button-response-btngroup button\");\n      for (var i = 0; i < btns.length; i++) {\n        btns[i].removeAttribute(\"disabled\");\n      }\n    }\n\n    function disable_buttons() {\n      var btns = document.querySelectorAll(\"#jspsych-image-button-response-btngroup button\");\n      for (var i = 0; i < btns.length; i++) {\n        btns[i].setAttribute(\"disabled\", \"disabled\");\n      }\n    }\n\n    // set timer of button delay\n    if (trial.enable_button_after > 0) {\n      disable_buttons();\n      this.jsPsych.pluginAPI.setTimeout(() => {\n        enable_buttons();\n      }, trial.enable_button_after);\n    }\n\n    // hide image if timing is set\n    if (trial.stimulus_duration !== null) {\n      this.jsPsych.pluginAPI.setTimeout(() => {\n        stimulusElement.style.visibility = \"hidden\";\n      }, trial.stimulus_duration);\n    }\n\n    // end trial if time limit is set\n    if (trial.trial_duration !== null) {\n      this.jsPsych.pluginAPI.setTimeout(() => {\n        end_trial();\n      }, trial.trial_duration);\n    } else if (trial.response_ends_trial === false) {\n      console.warn(\n        \"The experiment may be deadlocked. Try setting a trial duration or set response_ends_trial to true.\"\n      );\n    }\n  }\n\n  simulate(\n    trial: TrialType<Info>,\n    simulation_mode,\n    simulation_options: any,\n    load_callback: () => void\n  ) {\n    if (simulation_mode == \"data-only\") {\n      load_callback();\n      this.simulate_data_only(trial, simulation_options);\n    }\n    if (simulation_mode == \"visual\") {\n      this.simulate_visual(trial, simulation_options, load_callback);\n    }\n  }\n\n  private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n    const default_data = {\n      stimulus: trial.stimulus,\n      rt:\n        this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true) +\n        trial.enable_button_after,\n      response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1),\n    };\n\n    const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n    this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n    return data;\n  }\n\n  private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n    const data = this.create_simulation_data(trial, simulation_options);\n\n    this.jsPsych.finishTrial(data);\n  }\n\n  private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n    const data = this.create_simulation_data(trial, simulation_options);\n\n    const display_element = this.jsPsych.getDisplayElement();\n\n    this.trial(display_element, trial);\n    load_callback();\n\n    if (data.rt !== null) {\n      this.jsPsych.pluginAPI.clickTarget(\n        display_element.querySelector(\n          `#jspsych-image-button-response-btngroup [data-choice=\"${data.response}\"]`\n        ),\n        data.rt\n      );\n    }\n  }\n}\n\nexport default ImageButtonResponsePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC6HA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}