{"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n  \"name\": \"@jspsych/plugin-instructions\",\n  \"version\": \"2.1.0\",\n  \"description\": \"jsPsych plugin to display instructions\",\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-instructions\"\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/instructions\",\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\";\nimport { parameterPathArrayToString } from \"jspsych/src/timeline/util\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n  name: \"instructions\",\n  version: version,\n  parameters: {\n    /** Each element of the array is the content for a single page. Each page should be an HTML-formatted string.  */\n    pages: {\n      type: ParameterType.HTML_STRING,\n      default: undefined,\n      array: true,\n    },\n    /** This is the key that the participant can press in order to advance to the next page. This key should be\n     * specified as a string (e.g., `'a'`, `'ArrowLeft'`, `' '`, `'Enter'`). */\n    key_forward: {\n      type: ParameterType.KEY,\n      default: \"ArrowRight\",\n    },\n    /** This is the key that the participant can press to return to the previous page. This key should be specified as a\n     * string (e.g., `'a'`, `'ArrowLeft'`, `' '`, `'Enter'`). */\n    key_backward: {\n      type: ParameterType.KEY,\n      default: \"ArrowLeft\",\n    },\n    /** If true, the participant can return to previous pages of the instructions. If false, they may only advace to the next page. */\n    allow_backward: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /** If `true`, the participant can use keyboard keys to navigate the pages. If `false`, they may not. */\n    allow_keys: {\n      type: ParameterType.BOOL,\n      default: true,\n    },\n    /** If true, then a `Previous` and `Next` button will be displayed beneath the instructions. Participants can\n     * click the buttons to navigate. */\n    show_clickable_nav: {\n      type: ParameterType.BOOL,\n      default: false,\n    },\n    /** If true, and clickable navigation is enabled, then Page x/y will be shown between the nav buttons. */\n    show_page_number: {\n      type: ParameterType.BOOL,\n      default: false,\n    },\n    /** The text that appears before x/y pages displayed when show_page_number is true.*/\n    page_label: {\n      type: ParameterType.STRING,\n      default: \"Page\",\n    },\n    /** The text that appears on the button to go backwards. */\n    button_label_previous: {\n      type: ParameterType.STRING,\n      default: \"Previous\",\n    },\n    /** The text that appears on the button to go forwards. */\n    button_label_next: {\n      type: ParameterType.STRING,\n      default: \"Next\",\n    },\n    /** The callback function when page changes */\n    on_page_change: {\n      type: ParameterType.FUNCTION,\n      pretty_name: \"Page change callback\",\n      default: function (current_page: number) {},\n    },\n  },\n  data: {\n    /** An array containing the order of pages the participant viewed (including when the participant returned to previous pages)\n     *  and the time spent viewing each page. Each object in the array represents a single page view,\n     * and contains keys called `page_index` (the page number, starting with 0) and `viewing_time`\n     * (duration of the page view). This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()`\n     * functions.\n     */\n    view_history: {\n      type: ParameterType.COMPLEX,\n      array: true,\n      nested: {\n        page_index: {\n          type: ParameterType.INT,\n        },\n        viewing_time: {\n          type: ParameterType.INT,\n        },\n      },\n    },\n    /** The response time in milliseconds for the participant to view all of the pages. */\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 is for showing instructions to the participant. It allows participants to navigate through multiple pages\n * of instructions at their own pace, recording how long the participant spends on each page. Navigation can be done using\n *  the mouse or keyboard. participants can be allowed to navigate forwards and backwards through pages, if desired.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/instructions/ instructions plugin documentation on jspsych.org}\n */\nclass InstructionsPlugin implements JsPsychPlugin<Info> {\n  static info = info;\n\n  constructor(private jsPsych: JsPsych) {}\n\n  trial(display_element: HTMLElement, trial: TrialType<Info>) {\n    var current_page = 0;\n\n    var view_history = [];\n\n    var start_time = performance.now();\n\n    var last_page_update_time = start_time;\n\n    function btnListener() {\n      if (this.id === \"jspsych-instructions-back\") {\n        back();\n      } else if (this.id === \"jspsych-instructions-next\") {\n        next();\n      }\n    }\n\n    function show_current_page() {\n      var html = trial.pages[current_page];\n\n      var pagenum_display = \"\";\n      if (trial.show_page_number) {\n        pagenum_display =\n          \"<span style='margin: 0 1em;' class='\" +\n          \"jspsych-instructions-pagenum'>\" +\n          trial.page_label +\n          \" \" +\n          (current_page + 1) +\n          \"/\" +\n          trial.pages.length +\n          \"</span>\";\n      }\n\n      if (trial.show_clickable_nav) {\n        var nav_html = \"<div class='jspsych-instructions-nav' style='padding: 10px 0px;'>\";\n        if (trial.allow_backward) {\n          var allowed = current_page > 0 ? \"\" : \"disabled='disabled'\";\n          nav_html +=\n            \"<button id='jspsych-instructions-back' class='jspsych-btn' style='margin-right: 5px;' \" +\n            allowed +\n            \">&lt; \" +\n            trial.button_label_previous +\n            \"</button>\";\n        }\n        if (trial.pages.length > 1 && trial.show_page_number) {\n          nav_html += pagenum_display;\n        }\n        nav_html +=\n          \"<button id='jspsych-instructions-next' class='jspsych-btn'\" +\n          \"style='margin-left: 5px;'>\" +\n          trial.button_label_next +\n          \" &gt;</button></div>\";\n\n        html += nav_html;\n        display_element.innerHTML = html;\n        if (current_page != 0 && trial.allow_backward) {\n          display_element\n            .querySelector(\"#jspsych-instructions-back\")\n            .addEventListener(\"click\", btnListener, { once: true });\n        }\n\n        display_element\n          .querySelector(\"#jspsych-instructions-next\")\n          .addEventListener(\"click\", btnListener, { once: true });\n      } else {\n        if (trial.show_page_number && trial.pages.length > 1) {\n          // page numbers for non-mouse navigation\n          html += \"<div class='jspsych-instructions-pagenum'>\" + pagenum_display + \"</div>\";\n        }\n        display_element.innerHTML = html;\n      }\n    }\n\n    function next() {\n      add_current_page_to_view_history();\n\n      current_page++;\n\n      // if done, finish up...\n      if (current_page >= trial.pages.length) {\n        endTrial();\n      } else {\n        show_current_page();\n      }\n\n      trial.on_page_change(current_page);\n    }\n\n    function back() {\n      add_current_page_to_view_history();\n\n      current_page--;\n\n      show_current_page();\n\n      trial.on_page_change(current_page);\n    }\n\n    function add_current_page_to_view_history() {\n      var current_time = performance.now();\n\n      var page_view_time = Math.round(current_time - last_page_update_time);\n\n      view_history.push({\n        page_index: current_page,\n        viewing_time: page_view_time,\n      });\n\n      last_page_update_time = current_time;\n    }\n\n    const endTrial = () => {\n      if (trial.allow_keys) {\n        this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboard_listener);\n      }\n\n      var trial_data = {\n        view_history: view_history,\n        rt: Math.round(performance.now() - start_time),\n      };\n\n      this.jsPsych.finishTrial(trial_data);\n    };\n\n    const after_response = (info) => {\n      // have to reinitialize this instead of letting it persist to prevent accidental skips of pages by holding down keys too long\n      keyboard_listener = this.jsPsych.pluginAPI.getKeyboardResponse({\n        callback_function: after_response,\n        valid_responses: [trial.key_forward, trial.key_backward],\n        rt_method: \"performance\",\n        persist: false,\n        allow_held_key: false,\n      });\n      // check if key is forwards or backwards and update page\n      if (this.jsPsych.pluginAPI.compareKeys(info.key, trial.key_backward)) {\n        if (current_page !== 0 && trial.allow_backward) {\n          back();\n        }\n      }\n\n      if (this.jsPsych.pluginAPI.compareKeys(info.key, trial.key_forward)) {\n        next();\n      }\n    };\n\n    show_current_page();\n\n    if (trial.allow_keys) {\n      var keyboard_listener = this.jsPsych.pluginAPI.getKeyboardResponse({\n        callback_function: after_response,\n        valid_responses: [trial.key_forward, trial.key_backward],\n        rt_method: \"performance\",\n        persist: false,\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    let curr_page = 0;\n    let rt = 0;\n    let view_history = [];\n\n    // if there is no view history and no RT, simulate a random walk through the pages\n    if (!simulation_options.data?.view_history && !simulation_options.data?.rt) {\n      while (curr_page !== trial.pages.length) {\n        const view_time = Math.round(\n          this.jsPsych.randomization.sampleExGaussian(3000, 300, 1 / 300)\n        );\n        view_history.push({ page_index: curr_page, viewing_time: view_time });\n        rt += view_time;\n        if (curr_page == 0 || !trial.allow_backward) {\n          curr_page++;\n        } else {\n          if (this.jsPsych.randomization.sampleBernoulli(0.9) == 1) {\n            curr_page++;\n          } else {\n            curr_page--;\n          }\n        }\n      }\n    }\n\n    // if there is an RT but no view history, simulate a random walk through the pages\n    // that ends on the final page when the RT is reached\n    if (!simulation_options.data?.view_history && simulation_options.data?.rt) {\n      rt = simulation_options.data.rt;\n      while (curr_page !== trial.pages.length) {\n        view_history.push({ page_index: curr_page, viewing_time: null });\n        if (curr_page == 0 || !trial.allow_backward) {\n          curr_page++;\n        } else {\n          if (this.jsPsych.randomization.sampleBernoulli(0.9) == 1) {\n            curr_page++;\n          } else {\n            curr_page--;\n          }\n        }\n      }\n      const avg_rt_per_page = simulation_options.data.rt / view_history.length;\n      let total_time = 0;\n      for (const page of view_history) {\n        const t = Math.round(\n          this.jsPsych.randomization.sampleExGaussian(\n            avg_rt_per_page,\n            avg_rt_per_page / 10,\n            1 / (avg_rt_per_page / 10)\n          )\n        );\n        page.viewing_time = t;\n        total_time += t;\n      }\n      const diff = simulation_options.data.rt - total_time;\n      // remove equal diff from each page\n      const diff_per_page = Math.round(diff / view_history.length);\n      for (const page of view_history) {\n        page.viewing_time += diff_per_page;\n      }\n    }\n\n    // if there is a view history but no RT, make the RT equal the sum of the view history\n    if (simulation_options.data?.view_history && !simulation_options.data?.rt) {\n      view_history = simulation_options.data.view_history;\n      rt = 0;\n      for (const page of simulation_options.data.view_history) {\n        rt += page.viewing_time;\n      }\n    }\n\n    const default_data = {\n      view_history: view_history,\n      rt: rt,\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    const advance = (rt) => {\n      if (trial.allow_keys) {\n        this.jsPsych.pluginAPI.pressKey(trial.key_forward, rt);\n      } else if (trial.show_clickable_nav) {\n        this.jsPsych.pluginAPI.clickTarget(\n          display_element.querySelector(\"#jspsych-instructions-next\"),\n          rt\n        );\n      }\n    };\n\n    const backup = (rt) => {\n      if (trial.allow_keys) {\n        this.jsPsych.pluginAPI.pressKey(trial.key_backward, rt);\n      } else if (trial.show_clickable_nav) {\n        this.jsPsych.pluginAPI.clickTarget(\n          display_element.querySelector(\"#jspsych-instructions-back\"),\n          rt\n        );\n      }\n    };\n\n    let curr_page = 0;\n    let t = 0;\n    for (let i = 0; i < data.view_history.length; i++) {\n      if (i == data.view_history.length - 1) {\n        advance(t + data.view_history[i].viewing_time);\n      } else {\n        if (data.view_history[i + 1].page_index > curr_page) {\n          advance(t + data.view_history[i].viewing_time);\n        }\n        if (data.view_history[i + 1].page_index < curr_page) {\n          backup(t + data.view_history[i].viewing_time);\n        }\n        t += data.view_history[i].viewing_time;\n        curr_page = data.view_history[i + 1].page_index;\n      }\n    }\n  }\n}\n\nexport default InstructionsPlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC6FA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}