jspsych 7.3.4 → 8.0.1
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.
- package/README.md +1 -1
- package/css/jspsych.css +18 -8
- package/dist/index.browser.js +3097 -4286
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +6 -2
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +2331 -4066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +984 -6
- package/dist/index.js +2330 -4066
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/ExtensionManager.spec.ts +121 -0
- package/src/ExtensionManager.ts +100 -0
- package/src/JsPsych.ts +195 -690
- package/src/ProgressBar.spec.ts +60 -0
- package/src/ProgressBar.ts +60 -0
- package/src/index.scss +29 -8
- package/src/index.ts +4 -9
- package/src/modules/data/DataCollection.ts +1 -1
- package/src/modules/data/DataColumn.ts +12 -1
- package/src/modules/data/index.ts +92 -103
- package/src/modules/extensions.ts +4 -0
- package/src/modules/plugin-api/AudioPlayer.ts +101 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
- package/src/modules/plugin-api/MediaAPI.ts +48 -106
- package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
- package/src/modules/plugin-api/index.ts +11 -14
- package/src/modules/plugins.ts +26 -27
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +921 -0
- package/src/timeline/Timeline.ts +342 -0
- package/src/timeline/TimelineNode.ts +174 -0
- package/src/timeline/Trial.spec.ts +897 -0
- package/src/timeline/Trial.ts +419 -0
- package/src/timeline/index.ts +232 -0
- package/src/timeline/util.spec.ts +124 -0
- package/src/timeline/util.ts +146 -0
- package/dist/JsPsych.d.ts +0 -112
- package/dist/TimelineNode.d.ts +0 -34
- package/dist/migration.d.ts +0 -3
- package/dist/modules/data/DataCollection.d.ts +0 -46
- package/dist/modules/data/DataColumn.d.ts +0 -15
- package/dist/modules/data/index.d.ts +0 -25
- package/dist/modules/data/utils.d.ts +0 -3
- package/dist/modules/extensions.d.ts +0 -22
- package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
- package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
- package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
- package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
- package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
- package/dist/modules/plugin-api/index.d.ts +0 -8
- package/dist/modules/plugins.d.ts +0 -136
- package/dist/modules/randomization.d.ts +0 -42
- package/dist/modules/turk.d.ts +0 -40
- package/dist/modules/utils.d.ts +0 -13
- package/src/TimelineNode.ts +0 -544
- package/src/modules/plugin-api/HardwareAPI.ts +0 -32
package/src/TimelineNode.ts
DELETED
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
import { JsPsych } from "./JsPsych";
|
|
2
|
-
import {
|
|
3
|
-
repeat,
|
|
4
|
-
sampleWithReplacement,
|
|
5
|
-
sampleWithoutReplacement,
|
|
6
|
-
shuffle,
|
|
7
|
-
shuffleAlternateGroups,
|
|
8
|
-
} from "./modules/randomization";
|
|
9
|
-
import { deepCopy } from "./modules/utils";
|
|
10
|
-
|
|
11
|
-
export class TimelineNode {
|
|
12
|
-
// a unique ID for this node, relative to the parent
|
|
13
|
-
relative_id;
|
|
14
|
-
|
|
15
|
-
// store the parent for this node
|
|
16
|
-
parent_node;
|
|
17
|
-
|
|
18
|
-
// parameters for the trial if the node contains a trial
|
|
19
|
-
trial_parameters;
|
|
20
|
-
|
|
21
|
-
// parameters for nodes that contain timelines
|
|
22
|
-
timeline_parameters;
|
|
23
|
-
|
|
24
|
-
// stores trial information on a node that contains a timeline
|
|
25
|
-
// used for adding new trials
|
|
26
|
-
node_trial_data;
|
|
27
|
-
|
|
28
|
-
// track progress through the node
|
|
29
|
-
progress = <any>{
|
|
30
|
-
current_location: -1, // where on the timeline (which timelinenode)
|
|
31
|
-
current_variable_set: 0, // which set of variables to use from timeline_variables
|
|
32
|
-
current_repetition: 0, // how many times through the variable set on this run of the node
|
|
33
|
-
current_iteration: 0, // how many times this node has been revisited
|
|
34
|
-
done: false,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
end_message?: string;
|
|
38
|
-
|
|
39
|
-
// constructor
|
|
40
|
-
constructor(private jsPsych: JsPsych, parameters, parent?, relativeID?) {
|
|
41
|
-
// store a link to the parent of this node
|
|
42
|
-
this.parent_node = parent;
|
|
43
|
-
|
|
44
|
-
// create the ID for this node
|
|
45
|
-
this.relative_id = typeof parent === "undefined" ? 0 : relativeID;
|
|
46
|
-
|
|
47
|
-
// check if there is a timeline parameter
|
|
48
|
-
// if there is, then this node has its own timeline
|
|
49
|
-
if (typeof parameters.timeline !== "undefined") {
|
|
50
|
-
// create timeline properties
|
|
51
|
-
this.timeline_parameters = {
|
|
52
|
-
timeline: [],
|
|
53
|
-
loop_function: parameters.loop_function,
|
|
54
|
-
conditional_function: parameters.conditional_function,
|
|
55
|
-
sample: parameters.sample,
|
|
56
|
-
randomize_order:
|
|
57
|
-
typeof parameters.randomize_order == "undefined" ? false : parameters.randomize_order,
|
|
58
|
-
repetitions: typeof parameters.repetitions == "undefined" ? 1 : parameters.repetitions,
|
|
59
|
-
timeline_variables:
|
|
60
|
-
typeof parameters.timeline_variables == "undefined"
|
|
61
|
-
? [{}]
|
|
62
|
-
: parameters.timeline_variables,
|
|
63
|
-
on_timeline_finish: parameters.on_timeline_finish,
|
|
64
|
-
on_timeline_start: parameters.on_timeline_start,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
this.setTimelineVariablesOrder();
|
|
68
|
-
|
|
69
|
-
// extract all of the node level data and parameters
|
|
70
|
-
// but remove all of the timeline-level specific information
|
|
71
|
-
// since this will be used to copy things down hierarchically
|
|
72
|
-
var node_data = Object.assign({}, parameters);
|
|
73
|
-
delete node_data.timeline;
|
|
74
|
-
delete node_data.conditional_function;
|
|
75
|
-
delete node_data.loop_function;
|
|
76
|
-
delete node_data.randomize_order;
|
|
77
|
-
delete node_data.repetitions;
|
|
78
|
-
delete node_data.timeline_variables;
|
|
79
|
-
delete node_data.sample;
|
|
80
|
-
delete node_data.on_timeline_start;
|
|
81
|
-
delete node_data.on_timeline_finish;
|
|
82
|
-
this.node_trial_data = node_data; // store for later...
|
|
83
|
-
|
|
84
|
-
// create a TimelineNode for each element in the timeline
|
|
85
|
-
for (var i = 0; i < parameters.timeline.length; i++) {
|
|
86
|
-
// merge parameters
|
|
87
|
-
var merged_parameters = Object.assign({}, node_data, parameters.timeline[i]);
|
|
88
|
-
// merge any data from the parent node into child nodes
|
|
89
|
-
if (typeof node_data.data == "object" && typeof parameters.timeline[i].data == "object") {
|
|
90
|
-
var merged_data = Object.assign({}, node_data.data, parameters.timeline[i].data);
|
|
91
|
-
merged_parameters.data = merged_data;
|
|
92
|
-
}
|
|
93
|
-
this.timeline_parameters.timeline.push(
|
|
94
|
-
new TimelineNode(this.jsPsych, merged_parameters, this, i)
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// if there is no timeline parameter, then this node is a trial node
|
|
99
|
-
else {
|
|
100
|
-
// check to see if a valid trial type is defined
|
|
101
|
-
if (typeof parameters.type === "undefined") {
|
|
102
|
-
console.error(
|
|
103
|
-
'Trial level node is missing the "type" parameter. The parameters for the node are: ' +
|
|
104
|
-
JSON.stringify(parameters)
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
// create a deep copy of the parameters for the trial
|
|
108
|
-
this.trial_parameters = { ...parameters };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// recursively get the next trial to run.
|
|
113
|
-
// if this node is a leaf (trial), then return the trial.
|
|
114
|
-
// otherwise, recursively find the next trial in the child timeline.
|
|
115
|
-
trial() {
|
|
116
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
117
|
-
// returns a clone of the trial_parameters to
|
|
118
|
-
// protect functions.
|
|
119
|
-
return deepCopy(this.trial_parameters);
|
|
120
|
-
} else {
|
|
121
|
-
if (this.progress.current_location >= this.timeline_parameters.timeline.length) {
|
|
122
|
-
return null;
|
|
123
|
-
} else {
|
|
124
|
-
return this.timeline_parameters.timeline[this.progress.current_location].trial();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
markCurrentTrialComplete() {
|
|
130
|
-
if (typeof this.timeline_parameters === "undefined") {
|
|
131
|
-
this.progress.done = true;
|
|
132
|
-
} else {
|
|
133
|
-
this.timeline_parameters.timeline[this.progress.current_location].markCurrentTrialComplete();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
nextRepetiton() {
|
|
138
|
-
this.setTimelineVariablesOrder();
|
|
139
|
-
this.progress.current_location = -1;
|
|
140
|
-
this.progress.current_variable_set = 0;
|
|
141
|
-
this.progress.current_repetition++;
|
|
142
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
143
|
-
this.timeline_parameters.timeline[i].reset();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// set the order for going through the timeline variables array
|
|
148
|
-
setTimelineVariablesOrder() {
|
|
149
|
-
const timeline_parameters = this.timeline_parameters;
|
|
150
|
-
|
|
151
|
-
// check to make sure this node has variables
|
|
152
|
-
if (
|
|
153
|
-
typeof timeline_parameters === "undefined" ||
|
|
154
|
-
typeof timeline_parameters.timeline_variables === "undefined"
|
|
155
|
-
) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
var order = [];
|
|
160
|
-
for (var i = 0; i < timeline_parameters.timeline_variables.length; i++) {
|
|
161
|
-
order.push(i);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (typeof timeline_parameters.sample !== "undefined") {
|
|
165
|
-
if (timeline_parameters.sample.type == "custom") {
|
|
166
|
-
order = timeline_parameters.sample.fn(order);
|
|
167
|
-
} else if (timeline_parameters.sample.type == "with-replacement") {
|
|
168
|
-
order = sampleWithReplacement(
|
|
169
|
-
order,
|
|
170
|
-
timeline_parameters.sample.size,
|
|
171
|
-
timeline_parameters.sample.weights
|
|
172
|
-
);
|
|
173
|
-
} else if (timeline_parameters.sample.type == "without-replacement") {
|
|
174
|
-
order = sampleWithoutReplacement(order, timeline_parameters.sample.size);
|
|
175
|
-
} else if (timeline_parameters.sample.type == "fixed-repetitions") {
|
|
176
|
-
order = repeat(order, timeline_parameters.sample.size, false);
|
|
177
|
-
} else if (timeline_parameters.sample.type == "alternate-groups") {
|
|
178
|
-
order = shuffleAlternateGroups(
|
|
179
|
-
timeline_parameters.sample.groups,
|
|
180
|
-
timeline_parameters.sample.randomize_group_order
|
|
181
|
-
);
|
|
182
|
-
} else {
|
|
183
|
-
console.error(
|
|
184
|
-
'Invalid type in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"'
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (timeline_parameters.randomize_order) {
|
|
190
|
-
order = shuffle(order);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
this.progress.order = order;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// next variable set
|
|
197
|
-
nextSet() {
|
|
198
|
-
this.progress.current_location = -1;
|
|
199
|
-
this.progress.current_variable_set++;
|
|
200
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
201
|
-
this.timeline_parameters.timeline[i].reset();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// update the current trial node to be completed
|
|
206
|
-
// returns true if the node is complete after advance (all subnodes are also complete)
|
|
207
|
-
// returns false otherwise
|
|
208
|
-
advance() {
|
|
209
|
-
const progress = this.progress;
|
|
210
|
-
const timeline_parameters = this.timeline_parameters;
|
|
211
|
-
const internal = this.jsPsych.internal;
|
|
212
|
-
|
|
213
|
-
// first check to see if done
|
|
214
|
-
if (progress.done) {
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// if node has not started yet (progress.current_location == -1),
|
|
219
|
-
// then try to start the node.
|
|
220
|
-
if (progress.current_location == -1) {
|
|
221
|
-
// check for on_timeline_start and conditonal function on nodes with timelines
|
|
222
|
-
if (typeof timeline_parameters !== "undefined") {
|
|
223
|
-
// only run the conditional function if this is the first repetition of the timeline when
|
|
224
|
-
// repetitions > 1, and only when on the first variable set
|
|
225
|
-
if (
|
|
226
|
-
typeof timeline_parameters.conditional_function !== "undefined" &&
|
|
227
|
-
progress.current_repetition == 0 &&
|
|
228
|
-
progress.current_variable_set == 0
|
|
229
|
-
) {
|
|
230
|
-
internal.call_immediate = true;
|
|
231
|
-
var conditional_result = timeline_parameters.conditional_function();
|
|
232
|
-
internal.call_immediate = false;
|
|
233
|
-
// if the conditional_function() returns false, then the timeline
|
|
234
|
-
// doesn't run and is marked as complete.
|
|
235
|
-
if (conditional_result == false) {
|
|
236
|
-
progress.done = true;
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// if we reach this point then the node has its own timeline and will start
|
|
242
|
-
// so we need to check if there is an on_timeline_start function if we are on the first variable set
|
|
243
|
-
if (
|
|
244
|
-
typeof timeline_parameters.on_timeline_start !== "undefined" &&
|
|
245
|
-
progress.current_variable_set == 0
|
|
246
|
-
) {
|
|
247
|
-
timeline_parameters.on_timeline_start();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// if we reach this point, then either the node doesn't have a timeline of the
|
|
251
|
-
// conditional function returned true and it can start
|
|
252
|
-
progress.current_location = 0;
|
|
253
|
-
// call advance again on this node now that it is pointing to a new location
|
|
254
|
-
return this.advance();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// if this node has a timeline, propogate down to the current trial.
|
|
258
|
-
if (typeof timeline_parameters !== "undefined") {
|
|
259
|
-
var have_node_to_run = false;
|
|
260
|
-
// keep incrementing the location in the timeline until one of the nodes reached is incomplete
|
|
261
|
-
while (
|
|
262
|
-
progress.current_location < timeline_parameters.timeline.length &&
|
|
263
|
-
have_node_to_run == false
|
|
264
|
-
) {
|
|
265
|
-
// check to see if the node currently pointed at is done
|
|
266
|
-
var target_complete = timeline_parameters.timeline[progress.current_location].advance();
|
|
267
|
-
if (!target_complete) {
|
|
268
|
-
have_node_to_run = true;
|
|
269
|
-
return false;
|
|
270
|
-
} else {
|
|
271
|
-
progress.current_location++;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// if we've reached the end of the timeline (which, if the code is here, we have)
|
|
276
|
-
|
|
277
|
-
// there are a few steps to see what to do next...
|
|
278
|
-
|
|
279
|
-
// first, check the timeline_variables to see if we need to loop through again
|
|
280
|
-
// with a new set of variables
|
|
281
|
-
if (progress.current_variable_set < progress.order.length - 1) {
|
|
282
|
-
// reset the progress of the node to be with the new set
|
|
283
|
-
this.nextSet();
|
|
284
|
-
// then try to advance this node again.
|
|
285
|
-
return this.advance();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// if we're all done with the timeline_variables, then check to see if there are more repetitions
|
|
289
|
-
else if (progress.current_repetition < timeline_parameters.repetitions - 1) {
|
|
290
|
-
this.nextRepetiton();
|
|
291
|
-
// check to see if there is an on_timeline_finish function
|
|
292
|
-
if (typeof timeline_parameters.on_timeline_finish !== "undefined") {
|
|
293
|
-
timeline_parameters.on_timeline_finish();
|
|
294
|
-
}
|
|
295
|
-
return this.advance();
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// if we're all done with the repetitions...
|
|
299
|
-
else {
|
|
300
|
-
// check to see if there is an on_timeline_finish function
|
|
301
|
-
if (typeof timeline_parameters.on_timeline_finish !== "undefined") {
|
|
302
|
-
timeline_parameters.on_timeline_finish();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// if we're all done with the repetitions, check if there is a loop function.
|
|
306
|
-
if (typeof timeline_parameters.loop_function !== "undefined") {
|
|
307
|
-
internal.call_immediate = true;
|
|
308
|
-
if (timeline_parameters.loop_function(this.generatedData())) {
|
|
309
|
-
this.reset();
|
|
310
|
-
internal.call_immediate = false;
|
|
311
|
-
return this.parent_node.advance();
|
|
312
|
-
} else {
|
|
313
|
-
progress.done = true;
|
|
314
|
-
internal.call_immediate = false;
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// no more loops on this timeline, we're done!
|
|
321
|
-
progress.done = true;
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// check the status of the done flag
|
|
327
|
-
isComplete() {
|
|
328
|
-
return this.progress.done;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// getter method for timeline variables
|
|
332
|
-
getTimelineVariableValue(variable_name: string) {
|
|
333
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
|
-
var v =
|
|
337
|
-
this.timeline_parameters.timeline_variables[
|
|
338
|
-
this.progress.order[this.progress.current_variable_set]
|
|
339
|
-
][variable_name];
|
|
340
|
-
return v;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// recursive upward search for timeline variables
|
|
344
|
-
findTimelineVariable(variable_name) {
|
|
345
|
-
var v = this.getTimelineVariableValue(variable_name);
|
|
346
|
-
if (typeof v == "undefined") {
|
|
347
|
-
if (typeof this.parent_node !== "undefined") {
|
|
348
|
-
return this.parent_node.findTimelineVariable(variable_name);
|
|
349
|
-
} else {
|
|
350
|
-
return undefined;
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
return v;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// recursive downward search for active trial to extract timeline variable
|
|
358
|
-
timelineVariable(variable_name: string) {
|
|
359
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
360
|
-
const val = this.findTimelineVariable(variable_name);
|
|
361
|
-
if (typeof val === "undefined") {
|
|
362
|
-
console.warn("Timeline variable " + variable_name + " not found.");
|
|
363
|
-
}
|
|
364
|
-
return val;
|
|
365
|
-
} else {
|
|
366
|
-
// if progress.current_location is -1, then the timeline variable is being evaluated
|
|
367
|
-
// in a function that runs prior to the trial starting, so we should treat that trial
|
|
368
|
-
// as being the active trial for purposes of finding the value of the timeline variable
|
|
369
|
-
var loc = Math.max(0, this.progress.current_location);
|
|
370
|
-
// if loc is greater than the number of elements on this timeline, then the timeline
|
|
371
|
-
// variable is being evaluated in a function that runs after the trial on the timeline
|
|
372
|
-
// are complete but before advancing to the next (like a loop_function).
|
|
373
|
-
// treat the last active trial as the active trial for this purpose.
|
|
374
|
-
if (loc == this.timeline_parameters.timeline.length) {
|
|
375
|
-
loc = loc - 1;
|
|
376
|
-
}
|
|
377
|
-
// now find the variable
|
|
378
|
-
const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
|
|
379
|
-
if (typeof val === "undefined") {
|
|
380
|
-
console.warn("Timeline variable " + variable_name + " not found.");
|
|
381
|
-
}
|
|
382
|
-
return val;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// recursively get all the timeline variables for this trial
|
|
387
|
-
allTimelineVariables() {
|
|
388
|
-
var all_tvs = this.allTimelineVariablesNames();
|
|
389
|
-
var all_tvs_vals = <any>{};
|
|
390
|
-
for (var i = 0; i < all_tvs.length; i++) {
|
|
391
|
-
all_tvs_vals[all_tvs[i]] = this.timelineVariable(all_tvs[i]);
|
|
392
|
-
}
|
|
393
|
-
return all_tvs_vals;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// helper to get all the names at this stage.
|
|
397
|
-
allTimelineVariablesNames(so_far = []) {
|
|
398
|
-
if (typeof this.timeline_parameters !== "undefined") {
|
|
399
|
-
so_far = so_far.concat(
|
|
400
|
-
Object.keys(
|
|
401
|
-
this.timeline_parameters.timeline_variables[
|
|
402
|
-
this.progress.order[this.progress.current_variable_set]
|
|
403
|
-
]
|
|
404
|
-
)
|
|
405
|
-
);
|
|
406
|
-
// if progress.current_location is -1, then the timeline variable is being evaluated
|
|
407
|
-
// in a function that runs prior to the trial starting, so we should treat that trial
|
|
408
|
-
// as being the active trial for purposes of finding the value of the timeline variable
|
|
409
|
-
var loc = Math.max(0, this.progress.current_location);
|
|
410
|
-
// if loc is greater than the number of elements on this timeline, then the timeline
|
|
411
|
-
// variable is being evaluated in a function that runs after the trial on the timeline
|
|
412
|
-
// are complete but before advancing to the next (like a loop_function).
|
|
413
|
-
// treat the last active trial as the active trial for this purpose.
|
|
414
|
-
if (loc == this.timeline_parameters.timeline.length) {
|
|
415
|
-
loc = loc - 1;
|
|
416
|
-
}
|
|
417
|
-
// now find the variable
|
|
418
|
-
return this.timeline_parameters.timeline[loc].allTimelineVariablesNames(so_far);
|
|
419
|
-
}
|
|
420
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
421
|
-
return so_far;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// recursively get the number of **trials** contained in the timeline
|
|
426
|
-
// assuming that while loops execute exactly once and if conditionals
|
|
427
|
-
// always run
|
|
428
|
-
length() {
|
|
429
|
-
var length = 0;
|
|
430
|
-
if (typeof this.timeline_parameters !== "undefined") {
|
|
431
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
432
|
-
length += this.timeline_parameters.timeline[i].length();
|
|
433
|
-
}
|
|
434
|
-
} else {
|
|
435
|
-
return 1;
|
|
436
|
-
}
|
|
437
|
-
return length;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// return the percentage of trials completed, grouped at the first child level
|
|
441
|
-
// counts a set of trials as complete when the child node is done
|
|
442
|
-
percentComplete() {
|
|
443
|
-
var total_trials = this.length();
|
|
444
|
-
var completed_trials = 0;
|
|
445
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
446
|
-
if (this.timeline_parameters.timeline[i].isComplete()) {
|
|
447
|
-
completed_trials += this.timeline_parameters.timeline[i].length();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return (completed_trials / total_trials) * 100;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// resets the node and all subnodes to original state
|
|
454
|
-
// but increments the current_iteration counter
|
|
455
|
-
reset() {
|
|
456
|
-
this.progress.current_location = -1;
|
|
457
|
-
this.progress.current_repetition = 0;
|
|
458
|
-
this.progress.current_variable_set = 0;
|
|
459
|
-
this.progress.current_iteration++;
|
|
460
|
-
this.progress.done = false;
|
|
461
|
-
this.setTimelineVariablesOrder();
|
|
462
|
-
if (typeof this.timeline_parameters != "undefined") {
|
|
463
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
464
|
-
this.timeline_parameters.timeline[i].reset();
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// mark this node as finished
|
|
470
|
-
end() {
|
|
471
|
-
this.progress.done = true;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// recursively end whatever sub-node is running the current trial
|
|
475
|
-
endActiveNode() {
|
|
476
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
477
|
-
this.end();
|
|
478
|
-
this.parent_node.end();
|
|
479
|
-
} else {
|
|
480
|
-
this.timeline_parameters.timeline[this.progress.current_location].endActiveNode();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// get a unique ID associated with this node
|
|
485
|
-
// the ID reflects the current iteration through this node.
|
|
486
|
-
ID() {
|
|
487
|
-
var id = "";
|
|
488
|
-
if (typeof this.parent_node == "undefined") {
|
|
489
|
-
return "0." + this.progress.current_iteration;
|
|
490
|
-
} else {
|
|
491
|
-
id += this.parent_node.ID() + "-";
|
|
492
|
-
id += this.relative_id + "." + this.progress.current_iteration;
|
|
493
|
-
return id;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// get the ID of the active trial
|
|
498
|
-
activeID() {
|
|
499
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
500
|
-
return this.ID();
|
|
501
|
-
} else {
|
|
502
|
-
return this.timeline_parameters.timeline[this.progress.current_location].activeID();
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// get all the data generated within this node
|
|
507
|
-
generatedData() {
|
|
508
|
-
return this.jsPsych.data.getDataByTimelineNode(this.ID());
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// get all the trials of a particular type
|
|
512
|
-
trialsOfType(type) {
|
|
513
|
-
if (typeof this.timeline_parameters == "undefined") {
|
|
514
|
-
if (this.trial_parameters.type == type) {
|
|
515
|
-
return this.trial_parameters;
|
|
516
|
-
} else {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
} else {
|
|
520
|
-
var trials = [];
|
|
521
|
-
for (var i = 0; i < this.timeline_parameters.timeline.length; i++) {
|
|
522
|
-
var t = this.timeline_parameters.timeline[i].trialsOfType(type);
|
|
523
|
-
trials = trials.concat(t);
|
|
524
|
-
}
|
|
525
|
-
return trials;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// add new trials to end of this timeline
|
|
530
|
-
insert(parameters) {
|
|
531
|
-
if (typeof this.timeline_parameters === "undefined") {
|
|
532
|
-
console.error("Cannot add new trials to a trial-level node.");
|
|
533
|
-
} else {
|
|
534
|
-
this.timeline_parameters.timeline.push(
|
|
535
|
-
new TimelineNode(
|
|
536
|
-
this.jsPsych,
|
|
537
|
-
{ ...this.node_trial_data, ...parameters },
|
|
538
|
-
this,
|
|
539
|
-
this.timeline_parameters.timeline.length
|
|
540
|
-
)
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export class HardwareAPI {
|
|
2
|
-
/**
|
|
3
|
-
* Indicates whether this instance of jspsych has opened a hardware connection through our browser
|
|
4
|
-
* extension
|
|
5
|
-
**/
|
|
6
|
-
hardwareConnected = false;
|
|
7
|
-
|
|
8
|
-
constructor() {
|
|
9
|
-
//it might be useful to open up a line of communication from the extension back to this page
|
|
10
|
-
//script, again, this will have to pass through DOM events. For now speed is of no concern so I
|
|
11
|
-
//will use jQuery
|
|
12
|
-
document.addEventListener("jspsych-activate", (evt) => {
|
|
13
|
-
this.hardwareConnected = true;
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Allows communication with user hardware through our custom Google Chrome extension + native C++ program
|
|
19
|
-
* @param mess The message to be passed to our extension, see its documentation for the expected members of this object.
|
|
20
|
-
* @author Daniel Rivas
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
hardware(mess) {
|
|
24
|
-
//since Chrome extension content-scripts do not share the javascript environment with the page
|
|
25
|
-
//script that loaded jspsych, we will need to use hacky methods like communicating through DOM
|
|
26
|
-
//events.
|
|
27
|
-
const jspsychEvt = new CustomEvent("jspsych", { detail: mess });
|
|
28
|
-
document.dispatchEvent(jspsychEvt);
|
|
29
|
-
//And voila! it will be the job of the content script injected by the extension to listen for
|
|
30
|
-
//the event and do the appropriate actions.
|
|
31
|
-
}
|
|
32
|
-
}
|