{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n  \"name\": \"@jspsych/plugin-image-keyboard-response\",\n  \"version\": \"2.1.0\",\n  \"description\": \"jsPsych plugin for displaying a stimulus and getting a keyboard 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-keyboard-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-keyboard-response\",\n  \"peerDependencies\": {\n    \"jspsych\": \">=7.1.0\"\n  },\n  \"devDependencies\": {\n    \"@jspsych/config\": \"^3.2.0\",\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-keyboard-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 scaled\n     * to maintain the image's aspect ratio. */\n    maintain_aspect_ratio: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /**his array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should\n     * be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see\n     * [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and\n     * [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/)\n     * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `\"ALL_KEYS\"`\n     * means that all keys will be accepted as valid responses. Specifying `\"NO_KEYS\"` will mean that no responses are allowed. */\n    choices: {\n      type: ParameterType.KEYS,\n      default: \"ALL_KEYS\",\n    },\n    /**This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can\n     * 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 the\n     * 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`, then the trial will wait for a response indefinitely. */\n    trial_duration: {\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 before\n     * the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for\n     * `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a stimulus for a\n     * 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  },\n  data: {\n    /** The path of the image that was displayed. */\n    stimulus: {\n      type: ParameterType.STRING,\n    },\n    /**  Indicates which key the participant pressed. */\n    response: {\n      type: ParameterType.STRING,\n    },\n    /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus\n     * 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 the keyboard. The stimulus can be displayed until a\n * response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant has\n * failed to respond within a fixed length of time.\n *\n * Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using\n * 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-keyboard-response/ image-keyboard-response plugin documentation on jspsych.org}\n */\nclass ImageKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n  static info = info;\n\n  constructor(private jsPsych: JsPsych) {}\n\n  trial(display_element: HTMLElement, trial: TrialType<Info>) {\n    var height, width;\n    if (trial.render_on_canvas) {\n      var image_drawn = false;\n      // first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML)\n      if (display_element.hasChildNodes()) {\n        // can't loop through child list because the list will be modified by .removeChild()\n        while (display_element.firstChild) {\n          display_element.removeChild(display_element.firstChild);\n        }\n      }\n      // create canvas element and image\n      var canvas = document.createElement(\"canvas\");\n      canvas.id = \"jspsych-image-keyboard-response-stimulus\";\n      canvas.style.margin = \"0\";\n      canvas.style.padding = \"0\";\n      var ctx = canvas.getContext(\"2d\");\n      var img = new Image();\n      img.onload = () => {\n        // if image wasn't preloaded, then it will need to be drawn whenever it finishes loading\n        if (!image_drawn) {\n          getHeightWidth(); // only possible to get width/height after image loads\n          ctx.drawImage(img, 0, 0, width, height);\n        }\n      };\n      img.src = trial.stimulus;\n      // get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties\n      const getHeightWidth = () => {\n        if (trial.stimulus_height !== null) {\n          height = trial.stimulus_height;\n          if (trial.stimulus_width == null && trial.maintain_aspect_ratio) {\n            width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);\n          }\n        } else {\n          height = img.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 = img.naturalHeight * (trial.stimulus_width / img.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 = img.naturalWidth;\n        }\n        canvas.height = height;\n        canvas.width = width;\n      };\n      getHeightWidth(); // call now, in case image loads immediately (is cached)\n      // add canvas and draw image\n      display_element.insertBefore(canvas, null);\n      if (img.complete && Number.isFinite(width) && Number.isFinite(height)) {\n        // if image has loaded and width/height have been set, then draw it now\n        // (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation)\n        ctx.drawImage(img, 0, 0, width, height);\n        image_drawn = true;\n      }\n      // add prompt if there is one\n      if (trial.prompt !== null) {\n        display_element.insertAdjacentHTML(\"beforeend\", trial.prompt);\n      }\n    } else {\n      // display stimulus as an image element\n      var html = '<img src=\"' + trial.stimulus + '\" id=\"jspsych-image-keyboard-response-stimulus\">';\n      // add prompt\n      if (trial.prompt !== null) {\n        html += trial.prompt;\n      }\n      // update the page content\n      display_element.innerHTML = html;\n\n      // set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth)\n      var img = display_element.querySelector(\n        \"#jspsych-image-keyboard-response-stimulus\"\n      ) as HTMLImageElement;\n      if (trial.stimulus_height !== null) {\n        height = trial.stimulus_height;\n        if (trial.stimulus_width == null && trial.maintain_aspect_ratio) {\n          width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);\n        }\n      } else {\n        height = img.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 = img.naturalHeight * (trial.stimulus_width / img.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 = img.naturalWidth;\n      }\n      img.style.height = height.toString() + \"px\";\n      img.style.width = width.toString() + \"px\";\n    }\n\n    // store response\n    var response = {\n      rt: null,\n      key: null,\n    };\n\n    // function to end trial when it is time\n    const end_trial = () => {\n      // kill keyboard listeners\n      if (typeof keyboardListener !== \"undefined\") {\n        this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener);\n      }\n\n      // gather the data to store for the trial\n      var trial_data = {\n        rt: response.rt,\n        stimulus: trial.stimulus,\n        response: response.key,\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    var after_response = (info) => {\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      display_element.querySelector(\"#jspsych-image-keyboard-response-stimulus\").className +=\n        \" responded\";\n\n      // only record the first response\n      if (response.key == null) {\n        response = info;\n      }\n\n      if (trial.response_ends_trial) {\n        end_trial();\n      }\n    };\n\n    // start the response listener\n    if (trial.choices != \"NO_KEYS\") {\n      var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n        callback_function: after_response,\n        valid_responses: trial.choices,\n        rt_method: \"performance\",\n        persist: false,\n        allow_held_key: false,\n      });\n    }\n\n    // hide stimulus if stimulus_duration is set\n    if (trial.stimulus_duration !== null) {\n      this.jsPsych.pluginAPI.setTimeout(() => {\n        display_element.querySelector<HTMLElement>(\n          \"#jspsych-image-keyboard-response-stimulus\"\n        ).style.visibility = \"hidden\";\n      }, trial.stimulus_duration);\n    }\n\n    // end trial if trial_duration 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 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.pressKey(data.response, data.rt);\n    }\n  }\n\n  private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n    const default_data = {\n      stimulus: trial.stimulus,\n      rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n      response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\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\nexport default ImageKeyboardResponsePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECyFA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}