@jspsych/plugin-audio-button-response 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,324 +1,331 @@
1
1
  var jsPsychAudioButtonResponse = (function (jspsych) {
2
- 'use strict';
2
+ 'use strict';
3
3
 
4
- const info = {
5
- name: "audio-button-response",
6
- parameters: {
7
- /** The audio to be played. */
8
- stimulus: {
9
- type: jspsych.ParameterType.AUDIO,
10
- pretty_name: "Stimulus",
11
- default: undefined,
12
- },
13
- /** Array containing the label(s) for the button(s). */
14
- choices: {
15
- type: jspsych.ParameterType.STRING,
16
- pretty_name: "Choices",
17
- default: undefined,
18
- array: true,
19
- },
20
- /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */
21
- button_html: {
22
- type: jspsych.ParameterType.HTML_STRING,
23
- pretty_name: "Button HTML",
24
- default: '<button class="jspsych-btn">%choice%</button>',
25
- array: true,
26
- },
27
- /** Any content here will be displayed below the stimulus. */
28
- prompt: {
29
- type: jspsych.ParameterType.HTML_STRING,
30
- pretty_name: "Prompt",
31
- default: null,
32
- },
33
- /** The maximum duration to wait for a response. */
34
- trial_duration: {
35
- type: jspsych.ParameterType.INT,
36
- pretty_name: "Trial duration",
37
- default: null,
38
- },
39
- /** Vertical margin of button. */
40
- margin_vertical: {
41
- type: jspsych.ParameterType.STRING,
42
- pretty_name: "Margin vertical",
43
- default: "0px",
44
- },
45
- /** Horizontal margin of button. */
46
- margin_horizontal: {
47
- type: jspsych.ParameterType.STRING,
48
- pretty_name: "Margin horizontal",
49
- default: "8px",
50
- },
51
- /** If true, the trial will end when user makes a response. */
52
- response_ends_trial: {
53
- type: jspsych.ParameterType.BOOL,
54
- pretty_name: "Response ends trial",
55
- default: true,
56
- },
57
- /** If true, then the trial will end as soon as the audio file finishes playing. */
58
- trial_ends_after_audio: {
59
- type: jspsych.ParameterType.BOOL,
60
- pretty_name: "Trial ends after audio",
61
- default: false,
62
- },
63
- /**
64
- * If true, then responses are allowed while the audio is playing.
65
- * If false, then the audio must finish playing before a response is accepted.
66
- */
67
- response_allowed_while_playing: {
68
- type: jspsych.ParameterType.BOOL,
69
- pretty_name: "Response allowed while playing",
70
- default: true,
71
- },
72
- /** The delay of enabling button */
73
- enable_button_after: {
74
- type: jspsych.ParameterType.INT,
75
- pretty_name: "Enable button after",
76
- default: 0,
77
- },
78
- },
79
- };
80
- /**
81
- * **audio-button-response**
82
- *
83
- * jsPsych plugin for playing an audio file and getting a button response
84
- *
85
- * @author Kristin Diep
86
- * @see {@link https://www.jspsych.org/plugins/jspsych-audio-button-response/ audio-button-response plugin documentation on jspsych.org}
87
- */
88
- class AudioButtonResponsePlugin {
89
- constructor(jsPsych) {
90
- this.jsPsych = jsPsych;
91
- }
92
- trial(display_element, trial, on_load) {
93
- // hold the .resolve() function from the Promise that ends the trial
94
- let trial_complete;
95
- // setup stimulus
96
- var context = this.jsPsych.pluginAPI.audioContext();
97
- // store response
98
- var response = {
99
- rt: null,
100
- button: null,
101
- };
102
- // record webaudio context start time
103
- var startTime;
104
- // load audio file
105
- this.jsPsych.pluginAPI
106
- .getAudioBuffer(trial.stimulus)
107
- .then((buffer) => {
108
- if (context !== null) {
109
- this.audio = context.createBufferSource();
110
- this.audio.buffer = buffer;
111
- this.audio.connect(context.destination);
112
- }
113
- else {
114
- this.audio = buffer;
115
- this.audio.currentTime = 0;
116
- }
117
- setupTrial();
118
- })
119
- .catch((err) => {
120
- console.error(`Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.`);
121
- console.error(err);
122
- });
123
- const setupTrial = () => {
124
- // set up end event if trial needs it
125
- if (trial.trial_ends_after_audio) {
126
- this.audio.addEventListener("ended", end_trial);
127
- }
128
- // enable buttons after audio ends if necessary
129
- if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
130
- this.audio.addEventListener("ended", enable_buttons);
131
- }
132
- //display buttons
133
- var buttons = [];
134
- if (Array.isArray(trial.button_html)) {
135
- if (trial.button_html.length == trial.choices.length) {
136
- buttons = trial.button_html;
137
- }
138
- else {
139
- console.error("Error in audio-button-response plugin. The length of the button_html array does not equal the length of the choices array");
140
- }
141
- }
142
- else {
143
- for (var i = 0; i < trial.choices.length; i++) {
144
- buttons.push(trial.button_html);
145
- }
146
- }
147
- var html = '<div id="jspsych-audio-button-response-btngroup">';
148
- for (var i = 0; i < trial.choices.length; i++) {
149
- var str = buttons[i].replace(/%choice%/g, trial.choices[i]);
150
- html +=
151
- '<div class="jspsych-audio-button-response-button" style="cursor: pointer; display: inline-block; margin:' +
152
- trial.margin_vertical +
153
- " " +
154
- trial.margin_horizontal +
155
- '" id="jspsych-audio-button-response-button-' +
156
- i +
157
- '" data-choice="' +
158
- i +
159
- '">' +
160
- str +
161
- "</div>";
162
- }
163
- html += "</div>";
164
- //show prompt if there is one
165
- if (trial.prompt !== null) {
166
- html += trial.prompt;
167
- }
168
- display_element.innerHTML = html;
169
- if (trial.response_allowed_while_playing) {
170
- disable_buttons();
171
- enable_buttons();
172
- }
173
- else {
174
- disable_buttons();
175
- }
176
- // start time
177
- startTime = performance.now();
178
- // start audio
179
- if (context !== null) {
180
- startTime = context.currentTime;
181
- this.audio.start(startTime);
182
- }
183
- else {
184
- this.audio.play();
185
- }
186
- // end trial if time limit is set
187
- if (trial.trial_duration !== null) {
188
- this.jsPsych.pluginAPI.setTimeout(() => {
189
- end_trial();
190
- }, trial.trial_duration);
191
- }
192
- on_load();
193
- };
194
- // function to handle responses by the subject
195
- function after_response(choice) {
196
- // measure rt
197
- var endTime = performance.now();
198
- var rt = Math.round(endTime - startTime);
199
- if (context !== null) {
200
- endTime = context.currentTime;
201
- rt = Math.round((endTime - startTime) * 1000);
202
- }
203
- response.button = parseInt(choice);
204
- response.rt = rt;
205
- // disable all the buttons after a response
206
- disable_buttons();
207
- if (trial.response_ends_trial) {
208
- end_trial();
209
- }
210
- }
211
- // function to end trial when it is time
212
- const end_trial = () => {
213
- // kill any remaining setTimeout handlers
214
- this.jsPsych.pluginAPI.clearAllTimeouts();
215
- // stop the audio file if it is playing
216
- // remove end event listeners if they exist
217
- if (context !== null) {
218
- this.audio.stop();
219
- }
220
- else {
221
- this.audio.pause();
222
- }
223
- this.audio.removeEventListener("ended", end_trial);
224
- this.audio.removeEventListener("ended", enable_buttons);
225
- // gather the data to store for the trial
226
- var trial_data = {
227
- rt: response.rt,
228
- stimulus: trial.stimulus,
229
- response: response.button,
230
- };
231
- // clear the display
232
- display_element.innerHTML = "";
233
- // move on to the next trial
234
- this.jsPsych.finishTrial(trial_data);
235
- trial_complete();
236
- };
237
- const enable_buttons_with_delay = (delay) => {
238
- this.jsPsych.pluginAPI.setTimeout(enable_buttons_without_delay, delay);
239
- };
240
- function button_response(e) {
241
- var choice = e.currentTarget.getAttribute("data-choice"); // don't use dataset for jsdom compatibility
242
- after_response(choice);
243
- }
244
- function disable_buttons() {
245
- var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
246
- for (var i = 0; i < btns.length; i++) {
247
- var btn_el = btns[i].querySelector("button");
248
- if (btn_el) {
249
- btn_el.disabled = true;
250
- }
251
- btns[i].removeEventListener("click", button_response);
252
- }
253
- }
254
- function enable_buttons_without_delay() {
255
- var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
256
- for (var i = 0; i < btns.length; i++) {
257
- var btn_el = btns[i].querySelector("button");
258
- if (btn_el) {
259
- btn_el.disabled = false;
260
- }
261
- btns[i].addEventListener("click", button_response);
262
- }
263
- }
264
- function enable_buttons() {
265
- if (trial.enable_button_after > 0) {
266
- enable_buttons_with_delay(trial.enable_button_after);
267
- }
268
- else {
269
- enable_buttons_without_delay();
270
- }
271
- }
272
- return new Promise((resolve) => {
273
- trial_complete = resolve;
274
- });
275
- }
276
- simulate(trial, simulation_mode, simulation_options, load_callback) {
277
- if (simulation_mode == "data-only") {
278
- load_callback();
279
- this.simulate_data_only(trial, simulation_options);
280
- }
281
- if (simulation_mode == "visual") {
282
- this.simulate_visual(trial, simulation_options, load_callback);
283
- }
284
- }
285
- create_simulation_data(trial, simulation_options) {
286
- const default_data = {
287
- stimulus: trial.stimulus,
288
- rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true) +
289
- trial.enable_button_after,
290
- response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1),
291
- };
292
- const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
293
- this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
294
- return data;
295
- }
296
- simulate_data_only(trial, simulation_options) {
297
- const data = this.create_simulation_data(trial, simulation_options);
298
- this.jsPsych.finishTrial(data);
299
- }
300
- simulate_visual(trial, simulation_options, load_callback) {
301
- const data = this.create_simulation_data(trial, simulation_options);
302
- const display_element = this.jsPsych.getDisplayElement();
303
- const respond = () => {
304
- if (data.rt !== null) {
305
- this.jsPsych.pluginAPI.clickTarget(display_element.querySelector(`div[data-choice="${data.response}"] button`), data.rt);
306
- }
307
- };
308
- this.trial(display_element, trial, () => {
309
- load_callback();
310
- if (!trial.response_allowed_while_playing) {
311
- this.audio.addEventListener("ended", respond);
312
- }
313
- else {
314
- respond();
315
- }
316
- });
317
- }
318
- }
319
- AudioButtonResponsePlugin.info = info;
4
+ function getDefaultExportFromCjs (x) {
5
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
6
+ }
320
7
 
321
- return AudioButtonResponsePlugin;
8
+ // Gets all non-builtin properties up the prototype chain
9
+ const getAllProperties = object => {
10
+ const properties = new Set();
11
+
12
+ do {
13
+ for (const key of Reflect.ownKeys(object)) {
14
+ properties.add([object, key]);
15
+ }
16
+ } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);
17
+
18
+ return properties;
19
+ };
20
+
21
+ var autoBind = (self, {include, exclude} = {}) => {
22
+ const filter = key => {
23
+ const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);
24
+
25
+ if (include) {
26
+ return include.some(match);
27
+ }
28
+
29
+ if (exclude) {
30
+ return !exclude.some(match);
31
+ }
32
+
33
+ return true;
34
+ };
35
+
36
+ for (const [object, key] of getAllProperties(self.constructor.prototype)) {
37
+ if (key === 'constructor' || !filter(key)) {
38
+ continue;
39
+ }
40
+
41
+ const descriptor = Reflect.getOwnPropertyDescriptor(object, key);
42
+ if (descriptor && typeof descriptor.value === 'function') {
43
+ self[key] = self[key].bind(self);
44
+ }
45
+ }
46
+
47
+ return self;
48
+ };
49
+
50
+ var autoBind$1 = /*@__PURE__*/getDefaultExportFromCjs(autoBind);
51
+
52
+ var _package = {
53
+ name: "@jspsych/plugin-audio-button-response",
54
+ version: "2.0.0",
55
+ description: "jsPsych plugin for playing an audio file and getting a button response",
56
+ type: "module",
57
+ main: "dist/index.cjs",
58
+ exports: {
59
+ import: "./dist/index.js",
60
+ require: "./dist/index.cjs"
61
+ },
62
+ typings: "dist/index.d.ts",
63
+ unpkg: "dist/index.browser.min.js",
64
+ files: [
65
+ "src",
66
+ "dist"
67
+ ],
68
+ source: "src/index.ts",
69
+ scripts: {
70
+ test: "jest",
71
+ "test:watch": "npm test -- --watch",
72
+ tsc: "tsc",
73
+ build: "rollup --config",
74
+ "build:watch": "npm run build -- --watch"
75
+ },
76
+ repository: {
77
+ type: "git",
78
+ url: "git+https://github.com/jspsych/jsPsych.git",
79
+ directory: "packages/plugin-audio-button-response"
80
+ },
81
+ author: "Kristin Diep",
82
+ license: "MIT",
83
+ bugs: {
84
+ url: "https://github.com/jspsych/jsPsych/issues"
85
+ },
86
+ homepage: "https://www.jspsych.org/latest/plugins/audio-button-response",
87
+ peerDependencies: {
88
+ jspsych: ">=7.1.0"
89
+ },
90
+ devDependencies: {
91
+ "@jspsych/config": "^3.0.0",
92
+ "@jspsych/test-utils": "^1.2.0"
93
+ }
94
+ };
95
+
96
+ const info = {
97
+ name: "audio-button-response",
98
+ version: _package.version,
99
+ parameters: {
100
+ stimulus: {
101
+ type: jspsych.ParameterType.AUDIO,
102
+ default: void 0
103
+ },
104
+ choices: {
105
+ type: jspsych.ParameterType.STRING,
106
+ default: void 0,
107
+ array: true
108
+ },
109
+ button_html: {
110
+ type: jspsych.ParameterType.FUNCTION,
111
+ default: function(choice, choice_index) {
112
+ return `<button class="jspsych-btn">${choice}</button>`;
113
+ }
114
+ },
115
+ prompt: {
116
+ type: jspsych.ParameterType.HTML_STRING,
117
+ default: null
118
+ },
119
+ trial_duration: {
120
+ type: jspsych.ParameterType.INT,
121
+ default: null
122
+ },
123
+ button_layout: {
124
+ type: jspsych.ParameterType.STRING,
125
+ default: "grid"
126
+ },
127
+ grid_rows: {
128
+ type: jspsych.ParameterType.INT,
129
+ default: 1
130
+ },
131
+ grid_columns: {
132
+ type: jspsych.ParameterType.INT,
133
+ default: null
134
+ },
135
+ response_ends_trial: {
136
+ type: jspsych.ParameterType.BOOL,
137
+ default: true
138
+ },
139
+ trial_ends_after_audio: {
140
+ type: jspsych.ParameterType.BOOL,
141
+ default: false
142
+ },
143
+ response_allowed_while_playing: {
144
+ type: jspsych.ParameterType.BOOL,
145
+ default: true
146
+ },
147
+ enable_button_after: {
148
+ type: jspsych.ParameterType.INT,
149
+ default: 0
150
+ }
151
+ },
152
+ data: {
153
+ rt: {
154
+ type: jspsych.ParameterType.INT
155
+ },
156
+ response: {
157
+ type: jspsych.ParameterType.INT
158
+ }
159
+ }
160
+ };
161
+ class AudioButtonResponsePlugin {
162
+ constructor(jsPsych) {
163
+ this.jsPsych = jsPsych;
164
+ autoBind$1(this);
165
+ }
166
+ static info = info;
167
+ audio;
168
+ params;
169
+ buttonElements = [];
170
+ display;
171
+ response = { rt: null, button: null };
172
+ context;
173
+ startTime;
174
+ trial_complete;
175
+ async trial(display_element, trial, on_load) {
176
+ this.trial_complete;
177
+ this.params = trial;
178
+ this.display = display_element;
179
+ this.context = this.jsPsych.pluginAPI.audioContext();
180
+ this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus);
181
+ if (trial.trial_ends_after_audio) {
182
+ this.audio.addEventListener("ended", this.end_trial);
183
+ }
184
+ if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
185
+ this.audio.addEventListener("ended", this.enable_buttons);
186
+ }
187
+ const buttonGroupElement = document.createElement("div");
188
+ buttonGroupElement.id = "jspsych-audio-button-response-btngroup";
189
+ if (trial.button_layout === "grid") {
190
+ buttonGroupElement.classList.add("jspsych-btn-group-grid");
191
+ if (trial.grid_rows === null && trial.grid_columns === null) {
192
+ throw new Error(
193
+ "You cannot set `grid_rows` to `null` without providing a value for `grid_columns`."
194
+ );
195
+ }
196
+ const n_cols = trial.grid_columns === null ? Math.ceil(trial.choices.length / trial.grid_rows) : trial.grid_columns;
197
+ const n_rows = trial.grid_rows === null ? Math.ceil(trial.choices.length / trial.grid_columns) : trial.grid_rows;
198
+ buttonGroupElement.style.gridTemplateColumns = `repeat(${n_cols}, 1fr)`;
199
+ buttonGroupElement.style.gridTemplateRows = `repeat(${n_rows}, 1fr)`;
200
+ } else if (trial.button_layout === "flex") {
201
+ buttonGroupElement.classList.add("jspsych-btn-group-flex");
202
+ }
203
+ for (const [choiceIndex, choice] of trial.choices.entries()) {
204
+ buttonGroupElement.insertAdjacentHTML("beforeend", trial.button_html(choice, choiceIndex));
205
+ const buttonElement = buttonGroupElement.lastChild;
206
+ buttonElement.dataset.choice = choiceIndex.toString();
207
+ buttonElement.addEventListener("click", () => {
208
+ this.after_response(choiceIndex);
209
+ });
210
+ this.buttonElements.push(buttonElement);
211
+ }
212
+ display_element.appendChild(buttonGroupElement);
213
+ if (trial.prompt !== null) {
214
+ display_element.insertAdjacentHTML("beforeend", trial.prompt);
215
+ }
216
+ if (trial.response_allowed_while_playing) {
217
+ if (trial.enable_button_after > 0) {
218
+ this.disable_buttons();
219
+ this.enable_buttons();
220
+ }
221
+ } else {
222
+ this.disable_buttons();
223
+ }
224
+ this.startTime = performance.now();
225
+ if (trial.trial_duration !== null) {
226
+ this.jsPsych.pluginAPI.setTimeout(() => {
227
+ this.end_trial();
228
+ }, trial.trial_duration);
229
+ }
230
+ on_load();
231
+ this.audio.play();
232
+ return new Promise((resolve) => {
233
+ this.trial_complete = resolve;
234
+ });
235
+ }
236
+ disable_buttons = () => {
237
+ for (const button of this.buttonElements) {
238
+ button.setAttribute("disabled", "disabled");
239
+ }
240
+ };
241
+ enable_buttons_without_delay = () => {
242
+ for (const button of this.buttonElements) {
243
+ button.removeAttribute("disabled");
244
+ }
245
+ };
246
+ enable_buttons_with_delay = (delay) => {
247
+ this.jsPsych.pluginAPI.setTimeout(this.enable_buttons_without_delay, delay);
248
+ };
249
+ enable_buttons() {
250
+ if (this.params.enable_button_after > 0) {
251
+ this.enable_buttons_with_delay(this.params.enable_button_after);
252
+ } else {
253
+ this.enable_buttons_without_delay();
254
+ }
255
+ }
256
+ after_response = (choice) => {
257
+ var endTime = performance.now();
258
+ var rt = Math.round(endTime - this.startTime);
259
+ if (this.context !== null) {
260
+ endTime = this.context.currentTime;
261
+ rt = Math.round((endTime - this.startTime) * 1e3);
262
+ }
263
+ this.response.button = parseInt(choice);
264
+ this.response.rt = rt;
265
+ this.disable_buttons();
266
+ if (this.params.response_ends_trial) {
267
+ this.end_trial();
268
+ }
269
+ };
270
+ end_trial = () => {
271
+ this.audio.stop();
272
+ this.audio.removeEventListener("ended", this.end_trial);
273
+ this.audio.removeEventListener("ended", this.enable_buttons);
274
+ var trial_data = {
275
+ rt: this.response.rt,
276
+ stimulus: this.params.stimulus,
277
+ response: this.response.button
278
+ };
279
+ this.trial_complete(trial_data);
280
+ };
281
+ async simulate(trial, simulation_mode, simulation_options, load_callback) {
282
+ if (simulation_mode == "data-only") {
283
+ load_callback();
284
+ this.simulate_data_only(trial, simulation_options);
285
+ }
286
+ if (simulation_mode == "visual") {
287
+ this.simulate_visual(trial, simulation_options, load_callback);
288
+ }
289
+ }
290
+ create_simulation_data(trial, simulation_options) {
291
+ const default_data = {
292
+ stimulus: trial.stimulus,
293
+ rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true) + trial.enable_button_after,
294
+ response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1)
295
+ };
296
+ const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
297
+ this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
298
+ return data;
299
+ }
300
+ simulate_data_only(trial, simulation_options) {
301
+ const data = this.create_simulation_data(trial, simulation_options);
302
+ this.jsPsych.finishTrial(data);
303
+ }
304
+ simulate_visual(trial, simulation_options, load_callback) {
305
+ const data = this.create_simulation_data(trial, simulation_options);
306
+ const display_element = this.jsPsych.getDisplayElement();
307
+ const respond = () => {
308
+ if (data.rt !== null) {
309
+ this.jsPsych.pluginAPI.clickTarget(
310
+ display_element.querySelector(
311
+ `#jspsych-audio-button-response-btngroup [data-choice="${data.response}"]`
312
+ ),
313
+ data.rt
314
+ );
315
+ }
316
+ };
317
+ this.trial(display_element, trial, () => {
318
+ load_callback();
319
+ if (!trial.response_allowed_while_playing) {
320
+ this.audio.addEventListener("ended", respond);
321
+ } else {
322
+ respond();
323
+ }
324
+ });
325
+ }
326
+ }
327
+
328
+ return AudioButtonResponsePlugin;
322
329
 
323
330
  })(jsPsychModule);
324
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-audio-button-response@1.2.0/dist/index.browser.js.map
331
+ //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-audio-button-response@2.0.0/dist/index.browser.js.map