diff --git a/LICENSE.md b/LICENSE.md
index 662475c..d4f4bba 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright 2019 Ilixa Ltd.
+Copyright 2020 Ilixa Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/js/core/EventManager.js b/js/core/EventManager.js
index cab00c4..071c1e1 100644
--- a/js/core/EventManager.js
+++ b/js/core/EventManager.js
@@ -1,33 +1,37 @@
/**
* Manager handling the keyboard and mouse/touch events.
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { MonotonicClock, Clock } from '../util/Clock';
-import { PsychoJS } from './PsychoJS';
+import {MonotonicClock, Clock} from '../util/Clock';
+import {PsychoJS} from './PsychoJS';
/**
* @class
*
This manager handles all participant interactions with the experiment, i.e. keyboard, mouse and touch events.
- *
+ *
* @name module:core.EventManager
* @class
* @param {Object} options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
*/
-export class EventManager {
+export class EventManager
+{
- constructor(psychoJS) {
+ constructor(psychoJS)
+ {
this._psychoJS = psychoJS;
// populate the reverse pyglet map:
for (const keyName in EventManager._pygletMap)
+ {
EventManager._reversePygletMap[EventManager._pygletMap[keyName]] = keyName;
+ }
// add key listeners:
this._keyBuffer = [];
@@ -53,9 +57,9 @@ export class EventManager {
/**
* Get the list of keys pressed by the participant.
- *
+ *
*
Note: The w3c [key-event viewer]{@link https://w3c.github.io/uievents/tools/key-event-viewer.html} can be used to see possible values for the items in the keyList given the user's keyboard and chosen layout. The "key" and "code" columns in the UI Events fields are the relevant values for the keyList argument.
- *
+ *
* @name module:core.EventManager#getKeys
* @function
* @public
@@ -65,36 +69,54 @@ export class EventManager {
* @return {string[]} the list of keys that were pressed.
*/
getKeys({
- keyList = null,
- timeStamped = false
- } = {}) {
+ keyList = null,
+ timeStamped = false
+ } = {})
+ {
if (keyList != null)
+ {
keyList = EventManager.pyglet2w3c(keyList);
+ }
let newBuffer = [];
let keys = [];
- for (let i = 0; i < this._keyBuffer.length; ++i) {
+ for (let i = 0; i < this._keyBuffer.length; ++i)
+ {
const key = this._keyBuffer[i];
let keyId = null;
- if (keyList != null) {
+ if (keyList != null)
+ {
let index = keyList.indexOf(key.code);
if (index < 0)
+ {
index = keyList.indexOf(EventManager._keycodeMap[key.keyCode]);
+ }
if (index >= 0)
+ {
keyId = EventManager._reversePygletMap[keyList[index]];
+ }
}
else
+ {
keyId = EventManager._reversePygletMap[key.code];
+ }
- if (keyId != null) {
+ if (keyId != null)
+ {
if (timeStamped)
+ {
keys.push([keyId, key.timestamp]);
+ }
else
+ {
keys.push(keyId);
+ }
}
else
- newBuffer.push(key); // keep key press in buffer
+ {
+ newBuffer.push(key);
+ } // keep key press in buffer
}
this._keyBuffer = newBuffer;
@@ -108,6 +130,7 @@ export class EventManager {
* @property {Array.Clock} clocks - the clocks associated to the mouse buttons, reset whenever the button is pressed
* @property {Array.number} times - the time elapsed since the last rest of the associated clock
*/
+
/**
* @typedef EventManager.MouseInfo
* @property {Array.number} pos - the position of the mouse [x, y]
@@ -117,94 +140,102 @@ export class EventManager {
*/
/**
* Get the mouse info.
- *
+ *
* @name module:core.EventManager#getMouseInfo
* @function
* @public
* @return {EventManager.MouseInfo} the mouse info.
*/
- getMouseInfo() {
+ getMouseInfo()
+ {
return this._mouseInfo;
}
-
+
/**
* Clear all events from the event buffer.
- *
+ *
* @name module:core.EventManager#clearEvents
* @function
* @public
- *
+ *
* @todo handle the attribs argument
*/
- clearEvents(attribs) {
+ clearEvents(attribs)
+ {
this.clearKeys();
}
/**
* Clear all keys from the key buffer.
- *
+ *
* @name module:core.EventManager#clearKeys
* @function
* @public
*/
- clearKeys() {
+ clearKeys()
+ {
this._keyBuffer = [];
}
/**
* Start the move clock.
- *
+ *
* @name module:core.EventManager#startMoveClock
* @function
* @public
- *
+ *
* @todo not implemented
*/
- startMoveClock() {
+ startMoveClock()
+ {
}
/**
* Stop the move clock.
- *
+ *
* @name module:core.EventManager#stopMoveClock
* @function
* @public
- *
+ *
* @todo not implemented
*/
- stopMoveClock() {
+ stopMoveClock()
+ {
}
/**
* Reset the move clock.
- *
+ *
* @name module:core.EventManager#resetMoveClock
* @function
* @public
- *
+ *
* @todo not implemented
*/
- resetMoveClock() {
+ resetMoveClock()
+ {
}
/**
* Add various mouse listeners to the Pixi renderer of the {@link Window}.
- *
+ *
* @name module:core.EventManager#addMouseListeners
* @function
* @public
* @param {PIXI.Renderer} renderer - The Pixi renderer
*/
- addMouseListeners(renderer) {
+ addMouseListeners(renderer)
+ {
const self = this;
- renderer.view.addEventListener("pointerdown", (event) => {
+ renderer.view.addEventListener("pointerdown", (event) =>
+ {
event.preventDefault();
self._mouseInfo.buttons.pressed[event.button] = 1;
@@ -216,7 +247,8 @@ export class EventManager {
}, false);
- renderer.view.addEventListener("touchstart", (event) => {
+ renderer.view.addEventListener("touchstart", (event) =>
+ {
event.preventDefault();
self._mouseInfo.buttons.pressed[0] = 1;
@@ -230,7 +262,8 @@ export class EventManager {
}, false);
- renderer.view.addEventListener("pointerup", (event) => {
+ renderer.view.addEventListener("pointerup", (event) =>
+ {
event.preventDefault();
self._mouseInfo.buttons.pressed[event.button] = 0;
@@ -241,7 +274,8 @@ export class EventManager {
}, false);
- renderer.view.addEventListener("touchend", (event) => {
+ renderer.view.addEventListener("touchend", (event) =>
+ {
event.preventDefault();
self._mouseInfo.buttons.pressed[0] = 0;
@@ -255,7 +289,8 @@ export class EventManager {
}, false);
- renderer.view.addEventListener("pointermove", (event) => {
+ renderer.view.addEventListener("pointermove", (event) =>
+ {
event.preventDefault();
self._mouseInfo.moveClock.reset();
@@ -263,11 +298,12 @@ export class EventManager {
}, false);
- renderer.view.addEventListener("touchmove", (event) => {
+ renderer.view.addEventListener("touchmove", (event) =>
+ {
event.preventDefault();
self._mouseInfo.moveClock.reset();
-
+
// we use the first touch, discarding all others:
const touches = event.changedTouches;
self._mouseInfo.pos = [touches[0].pageX, touches[0].pageY];
@@ -275,19 +311,20 @@ export class EventManager {
// (*) wheel
- renderer.view.addEventListener("wheel", event => {
+ renderer.view.addEventListener("wheel", event =>
+ {
self._mouseInfo.wheelRel[0] += event.deltaX;
self._mouseInfo.wheelRel[1] += event.deltaY;
-
+
this._psychoJS.experimentLogger.data("Mouse: wheel shift=(" + event.deltaX + "," + event.deltaY + "), pos=(" + self._mouseInfo.pos[0] + "," + self._mouseInfo.pos[1] + ")");
}, false);
-
+
}
/**
* Add key listeners to the document.
- *
+ *
* @name module:core.EventManager#_addKeyListeners
* @function
* @private
@@ -307,7 +344,9 @@ export class EventManager {
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === 'undefined')
+ {
code = EventManager.keycode2w3c(event.keyCode);
+ }
self._keyBuffer.push({
code,
@@ -324,24 +363,29 @@ export class EventManager {
}
-
/**
* Convert a keylist that uses pyglet key names to one that uses W3C KeyboardEvent.code values.
*
This allows key lists that work in the builder environment to work in psychoJS web experiments.
Unfortunately, it is not very fine-grained: for instance, there is no difference between Alt Left and Alt
* Right, or between Enter and Numpad Enter. Use at your own risk (or upgrade your browser...).
';
break;
}
}
@@ -371,7 +415,8 @@ export class GUI
}
// we are displaying a message:
- else if (typeof message !== 'undefined') {
+ else if (typeof message !== 'undefined')
+ {
htmlCode = '
' +
'
' + message + '
' +
'
';
@@ -379,7 +424,8 @@ export class GUI
}
// we are displaying a warning:
- else if (typeof warning !== 'undefined') {
+ else if (typeof warning !== 'undefined')
+ {
htmlCode = '
' +
'
' + warning + '
' +
'
';
@@ -395,7 +441,7 @@ export class GUI
this._estimateDialogScalingFactor();
const dialogSize = this._getDialogSize();
const self = this;
- $('#msgDialog').dialog({
+ $("#msgDialog").dialog({
dialogClass: 'no-close',
width: dialogSize[0],
@@ -405,15 +451,18 @@ export class GUI
modal: true,
closeOnEscape: false,
- buttons: (!showOK)?[]:[{
+ buttons: (!showOK) ? [] : [{
id: "buttonOk",
text: "Ok",
- click: function() {
+ click: function ()
+ {
$(this).dialog("destroy").remove();
// execute callback function:
if (typeof onOK !== 'undefined')
+ {
onOK();
+ }
}
}],
@@ -422,7 +471,7 @@ export class GUI
})
// change colour of title bar
- .prev(".ui-dialog-titlebar").css("background", titleColour);
+ .prev(".ui-dialog-titlebar").css("background", titleColour);
// when the browser window is resize, we redimension and reposition the dialog:
@@ -471,10 +520,12 @@ export class GUI
* @param {String} dialogId - the dialog ID
* @private
*/
- _dialogResize(dialogId) {
+ _dialogResize(dialogId)
+ {
const self = this;
- $(window).resize( function() {
+ $(window).resize(function ()
+ {
const parent = $(dialogId).parent();
const windowSize = [$(window).width(), $(window).height()];
@@ -486,7 +537,8 @@ export class GUI
});
const isDifferent = self._estimateDialogScalingFactor();
- if (!isDifferent) {
+ if (!isDifferent)
+ {
$(dialogId).css({
width: dialogSize[0] - self._contentDelta[0],
maxHeight: dialogSize[1] - self._contentDelta[1]
@@ -499,7 +551,7 @@ export class GUI
left: Math.max(0, (windowSize[0] - parent.outerWidth()) / 2.0),
top: Math.max(0, (windowSize[1] - parent.outerHeight()) / 2.0),
});
- } );
+ });
}
@@ -511,11 +563,13 @@ export class GUI
* @private
* @param {Object.} signal the signal
*/
- _onResourceEvents(signal) {
+ _onResourceEvents(signal)
+ {
this._psychoJS.logger.debug('signal: ' + util.toString(signal));
// all resources have been registered:
- if (signal.message === ServerManager.Event.RESOURCES_REGISTERED) {
+ if (signal.message === ServerManager.Event.RESOURCES_REGISTERED)
+ {
// for each resource, we have a 'downloading resource' and a 'resource downloaded' message:
this._progressBarMax = signal.count * 2;
$("#progressbar").progressbar("option", "max", this._progressBarMax);
@@ -525,7 +579,8 @@ export class GUI
}
// all the resources have been downloaded: show the ok button
- else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED) {
+ else if (signal.message === ServerManager.Event.DOWNLOAD_COMPLETED)
+ {
this._allResourcesDownloaded = true;
$("#progressMsg").text('all resources downloaded.');
this._updateOkButtonStatus();
@@ -535,21 +590,27 @@ export class GUI
else if (signal.message === ServerManager.Event.DOWNLOADING_RESOURCE || signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
{
if (typeof this._progressBarCurrentIncrement === 'undefined')
+ {
this._progressBarCurrentIncrement = 0;
- ++ this._progressBarCurrentIncrement;
+ }
+ ++this._progressBarCurrentIncrement;
if (signal.message === ServerManager.Event.RESOURCE_DOWNLOADED)
- $("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement/2 + ' / ' + this._progressBarMax/2);
+ {
+ $("#progressMsg").text('downloaded ' + this._progressBarCurrentIncrement / 2 + ' / ' + this._progressBarMax / 2);
+ }
// $("#progressMsg").text(signal.resource + ': downloaded.');
// else
- // $("#progressMsg").text(signal.resource + ': downloading...');
+ // $("#progressMsg").text(signal.resource + ': downloading...');
$("#progressbar").progressbar("option", "value", this._progressBarCurrentIncrement);
}
// unknown message: we just display it
else
+ {
$("#progressMsg").text(signal.message);
+ }
}
@@ -562,15 +623,21 @@ export class GUI
*/
_updateOkButtonStatus()
{
- if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length) )
+ if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.LOCAL || (this._allResourcesDownloaded && this._setRequiredKeys.size >= this._requiredKeys.length))
{
$("#buttonOk").button("option", "disabled", false);
- } else
+ }
+ else
+ {
$("#buttonOk").button("option", "disabled", true);
+ }
// strangely, changing the disabled option sometimes fails to update the ui,
// so we need to hide it and show it again:
- $("#buttonOk").hide(0, () => { $("#buttonOk").show(); });
+ $("#buttonOk").hide(0, () =>
+ {
+ $("#buttonOk").show();
+ });
}
@@ -582,20 +649,25 @@ export class GUI
* @private
* @returns {boolean} whether or not the scaling factor is different from the previously estimated one
*/
- _estimateDialogScalingFactor() {
+ _estimateDialogScalingFactor()
+ {
const windowSize = [$(window).width(), $(window).height()];
// desktop:
let dialogScalingFactor = 1.0;
// mobile or tablet:
- if (windowSize[0] < 1080) {
+ if (windowSize[0] < 1080)
+ {
// landscape:
if (windowSize[0] > windowSize[1])
+ {
dialogScalingFactor = 1.5;
- // portrait:
+ }// portrait:
else
+ {
dialogScalingFactor = 2.0;
+ }
}
const isDifferent = (dialogScalingFactor !== this._dialogScalingFactor);
@@ -612,13 +684,14 @@ export class GUI
* @private
* @returns {number[]} the size of the popup dialog window
*/
- _getDialogSize() {
+ _getDialogSize()
+ {
const windowSize = [$(window).width(), $(window).height()];
this._estimateDialogScalingFactor();
return [
- Math.min(GUI.dialogMaxSize[0], (windowSize[0]-GUI.dialogMargin[0]) / this._dialogScalingFactor),
- Math.min(GUI.dialogMaxSize[1], (windowSize[1]-GUI.dialogMargin[1]) / this._dialogScalingFactor)];
+ Math.min(GUI.dialogMaxSize[0], (windowSize[0] - GUI.dialogMargin[0]) / this._dialogScalingFactor),
+ Math.min(GUI.dialogMaxSize[1], (windowSize[1] - GUI.dialogMargin[1]) / this._dialogScalingFactor)];
}
@@ -632,14 +705,19 @@ export class GUI
* @param {module:core.GUI} gui - this GUI
* @param {Event} event - event
*/
- static _onKeyChange(gui, event) {
+ static _onKeyChange(gui, event)
+ {
const element = event.target;
const value = element.value;
if (typeof value !== 'undefined' && value.length > 0)
+ {
gui._setRequiredKeys.set(event.target, true);
+ }
else
+ {
gui._setRequiredKeys.delete(event.target);
+ }
gui._updateOkButtonStatus();
}
diff --git a/js/core/Keyboard.js b/js/core/Keyboard.js
index 8e772df..42e2773 100644
--- a/js/core/Keyboard.js
+++ b/js/core/Keyboard.js
@@ -2,7 +2,7 @@
* Manager handling the keyboard events.
*
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
@@ -21,8 +21,10 @@ import {EventManager} from "./EventManager";
* @param {number} tDown - time of key press (keydown event) relative to the global Monotonic Clock
* @param {string | undefined} name - pyglet key name
*/
-export class KeyPress {
- constructor(code, tDown, name) {
+export class KeyPress
+{
+ constructor(code, tDown, name)
+ {
this.code = code;
this.tDown = tDown;
this.name = (typeof name !== 'undefined') ? name : EventManager.w3c2pyglet(code);
@@ -49,7 +51,8 @@ export class KeyPress {
* @param {Clock} [options.clock= undefined] - an optional clock
* @param {boolean} options.autoLog - whether or not to log
*/
-export class Keyboard extends PsychObject {
+export class Keyboard extends PsychObject
+{
constructor({
psychoJS,
@@ -57,16 +60,19 @@ export class Keyboard extends PsychObject {
waitForStart = false,
clock,
autoLog = false,
- } = {}) {
+ } = {})
+ {
super(psychoJS);
if (typeof clock === 'undefined')
- clock = new Clock(); //this._psychoJS.monotonicClock;
+ {
+ clock = new Clock();
+ } //this._psychoJS.monotonicClock;
this._addAttributes(Keyboard, bufferSize, waitForStart, clock, autoLog);
// start recording key events if need be:
- this._addAttribute('status', (waitForStart)?PsychoJS.Status.NOT_STARTED:PsychoJS.Status.STARTED);
+ this._addAttribute('status', (waitForStart) ? PsychoJS.Status.NOT_STARTED : PsychoJS.Status.STARTED);
// setup circular buffer:
this.clearEvents();
@@ -85,7 +91,8 @@ export class Keyboard extends PsychObject {
* @public
*
*/
- start() {
+ start()
+ {
this._status = PsychoJS.Status.STARTED;
}
@@ -98,7 +105,8 @@ export class Keyboard extends PsychObject {
* @public
*
*/
- stop() {
+ stop()
+ {
this._status = PsychoJS.Status.STOPPED;
}
@@ -121,20 +129,26 @@ export class Keyboard extends PsychObject {
* @public
* @return {Keyboard.KeyEvent[]} the list of events still in the buffer
*/
- getEvents() {
+ getEvents()
+ {
if (this._bufferLength === 0)
+ {
return [];
+ }
+
-
// iterate over the buffer, from start to end, and discard the null event:
let filteredEvents = [];
const bufferWrap = (this._bufferLength === this._bufferSize);
let i = bufferWrap ? this._bufferIndex : -1;
- do {
+ do
+ {
i = (i + 1) % this._bufferSize;
const keyEvent = this._circularBuffer[i];
if (keyEvent)
+ {
filteredEvents.push(keyEvent);
+ }
} while (i !== this._bufferIndex);
return filteredEvents;
@@ -160,29 +174,37 @@ export class Keyboard extends PsychObject {
keyList = [],
waitRelease = true,
clear = true
- } = {}) {
+ } = {})
+ {
// if nothing in the buffer, return immediately:
if (this._bufferLength === 0)
+ {
return [];
+ }
let keyPresses = [];
// iterate over the circular buffer, looking for keyup events:
const bufferWrap = (this._bufferLength === this._bufferSize);
let i = bufferWrap ? this._bufferIndex : -1;
- do {
+ do
+ {
i = (i + 1) % this._bufferSize;
const keyEvent = this._circularBuffer[i];
- if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP) {
+ if (keyEvent && keyEvent.status === Keyboard.KeyStatus.KEY_UP)
+ {
// check that the key is in the keyList:
- if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
+ if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
+ {
// look for a corresponding, preceding keydown event:
const precedingKeydownIndex = keyEvent.keydownIndex;
- if (typeof precedingKeydownIndex !== 'undefined') {
+ if (typeof precedingKeydownIndex !== 'undefined')
+ {
const precedingKeydownEvent = this._circularBuffer[precedingKeydownIndex];
- if (precedingKeydownEvent) {
+ if (precedingKeydownEvent)
+ {
// prepare KeyPress and add it to the array:
const tDown = precedingKeydownEvent.timestamp;
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
@@ -191,7 +213,9 @@ export class Keyboard extends PsychObject {
keyPresses.push(keyPress);
if (clear)
+ {
this._circularBuffer[precedingKeydownIndex] = null;
+ }
}
}
@@ -220,7 +244,9 @@ export class Keyboard extends PsychObject {
} while ((bufferWrap && j !== i) || (j > -1));*/
if (clear)
+ {
this._circularBuffer[i] = null;
+ }
}
}
@@ -229,18 +255,23 @@ export class Keyboard extends PsychObject {
// if waitRelease = false, we iterate again over the map of unmatched keydown events:
- if (!waitRelease) {
- for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values()) {
+ if (!waitRelease)
+ {
+ for (const unmatchedKeyDownIndex of this._unmatchedKeydownMap.values())
+ {
const keyEvent = this._circularBuffer[unmatchedKeyDownIndex];
- if (keyEvent) {
+ if (keyEvent)
+ {
// check that the key is in the keyList:
- if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey)) {
+ if (keyList.length === 0 || keyList.includes(keyEvent.pigletKey))
+ {
const tDown = keyEvent.timestamp;
const keyPress = new KeyPress(keyEvent.code, tDown, keyEvent.pigletKey);
keyPress.rt = tDown - this._clock.getLastResetTime();
keyPresses.push(keyPress);
- if (clear) {
+ if (clear)
+ {
this._unmatchedKeydownMap.delete(keyEvent.code);
this._circularBuffer[unmatchedKeyDownIndex] = null;
}
@@ -272,14 +303,15 @@ export class Keyboard extends PsychObject {
// if clear = true and the keyList is empty, we clear all the events:
if (clear && keyList.length === 0)
+ {
this.clearEvents();
+ }
return keyPresses;
}
-
/**
* Clear all events and resets the circular buffers.
*
@@ -299,7 +331,6 @@ export class Keyboard extends PsychObject {
}
-
/**
* Test whether a list of KeyPress's contains one with a particular name.
*
@@ -317,12 +348,11 @@ export class Keyboard extends PsychObject {
return false;
}
- const value = keypressList.find( (keypress) => keypress.name === keyName );
+ const value = keypressList.find((keypress) => keypress.name === keyName);
return (typeof value !== 'undefined');
}
-
/**
* Add key listeners to the document.
*
@@ -338,17 +368,21 @@ export class Keyboard extends PsychObject {
// add a keydown listener:
window.addEventListener("keydown", (event) =>
- // document.addEventListener("keydown", (event) =>
+ // document.addEventListener("keydown", (event) =>
{
// only consider non-repeat events, i.e. only the first keydown event associated with a participant
// holding a key down:
if (event.repeat)
+ {
return;
+ }
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
if (this._status !== PsychoJS.Status.STARTED)
+ {
return;
+ }
/**
* DEPRECATED: we now use event.repeat
@@ -362,7 +396,9 @@ export class Keyboard extends PsychObject {
// take care of legacy Microsoft browsers (IE11 and pre-Chromium Edge):
if (typeof code === 'undefined')
+ {
code = EventManager.keycode2w3c(event.keyCode);
+ }
let pigletKey = EventManager.w3c2pyglet(code);
@@ -387,12 +423,14 @@ export class Keyboard extends PsychObject {
// add a keyup listener:
window.addEventListener("keyup", (event) =>
- // document.addEventListener("keyup", (event) =>
+ // document.addEventListener("keyup", (event) =>
{
const timestamp = MonotonicClock.getReferenceTime(); // timestamp in seconds
if (this._status !== PsychoJS.Status.STARTED)
+ {
return;
+ }
self._previousKeydownKey = undefined;
@@ -400,7 +438,9 @@ export class Keyboard extends PsychObject {
// take care of legacy Microsoft Edge:
if (typeof code === 'undefined')
+ {
code = EventManager.keycode2w3c(event.keyCode);
+ }
let pigletKey = EventManager.w3c2pyglet(code);
@@ -418,7 +458,8 @@ export class Keyboard extends PsychObject {
// note: if more keys are down than there are slots in the circular buffer, there might
// not be a corresponding keydown event
const correspondingKeydownIndex = self._unmatchedKeydownMap.get(event.code);
- if (typeof correspondingKeydownIndex !== 'undefined') {
+ if (typeof correspondingKeydownIndex !== 'undefined')
+ {
self._circularBuffer[self._bufferIndex].keydownIndex = correspondingKeydownIndex;
self._unmatchedKeydownMap.delete(event.code);
}
diff --git a/js/core/Logger.js b/js/core/Logger.js
index 5966df9..c94ae1c 100644
--- a/js/core/Logger.js
+++ b/js/core/Logger.js
@@ -1,8 +1,8 @@
/**
* Logger
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
@@ -10,20 +10,21 @@
import * as util from '../util/Util';
import {MonotonicClock} from '../util/Clock';
-import { ExperimentHandler } from '../data/ExperimentHandler';
+import {ExperimentHandler} from '../data/ExperimentHandler';
/**
*
This class handles a variety of loggers, e.g. a browser console one (mostly for debugging),
* a remote one, etc.
- *
+ *
*
Note: we use log4javascript for the console logger, and our own for the server logger.
- *
+ *
* @name module:core.Logger
* @class
* @param {*} threshold - the logging threshold, e.g. log4javascript.Level.ERROR
*/
-export class Logger {
+export class Logger
+{
constructor(psychoJS, threshold)
{
@@ -80,11 +81,10 @@ export class Logger {
}
-
/**
* Log a server message at the DATA level.
*
- * @name module:core.Logger#data
+ * @name module:core.Logger#data
* @public
* @param {string} msg - the message to be logged.
* @param {number} [time] - the logging time
@@ -96,7 +96,6 @@ export class Logger {
}
-
/**
* Log a server message.
*
@@ -110,7 +109,9 @@ export class Logger {
log(msg, level, time, obj)
{
if (typeof time === 'undefined')
+ {
time = MonotonicClock.getReferenceTime();
+ }
this._serverLogs.push({
msg,
@@ -122,7 +123,6 @@ export class Logger {
}
-
/**
* Flush all server logs to the server.
*
@@ -149,7 +149,9 @@ export class Logger {
'\t' + Symbol.keyFor(log.level) +
'\t' + log.msg;
if (log.obj !== 'undefined')
+ {
formattedLog += '\t' + log.obj;
+ }
formattedLog += '\n';
formattedLogs += formattedLog;
@@ -157,7 +159,8 @@ export class Logger {
// send logs to the server or display them in the console:
if (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER &&
- this._psychoJS.config.experiment.status === 'RUNNING')
+ this._psychoJS.config.experiment.status === 'RUNNING' &&
+ !this._psychoJS._serverMsg.has('__pilotToken'))
{
// if the pako compression library is present, we compress the logs:
if (typeof pako !== 'undefined')
@@ -189,10 +192,9 @@ export class Logger {
}
-
/**
* Create a custom console layout.
- *
+ *
* @name module:core.Logger#_customConsoleLayout
* @private
* @return {*} the custom layout
@@ -219,10 +221,13 @@ export class Logger {
{
// look for entry immediately after those of log4javascript:
for (let entry of stackEntries)
- if (entry.indexOf('log4javascript.min.js') <= 0) {
+ {
+ if (entry.indexOf('log4javascript.min.js') <= 0)
+ {
relevantEntry = entry;
break;
}
+ }
const buf = relevantEntry.split(':');
const line = buf[buf.length - 2];
@@ -242,15 +247,17 @@ export class Logger {
let buf = relevantEntry.split(' ');
let fileLine = buf.pop();
const method = buf.pop();
- buf = fileLine.split(':'); buf.pop();
+ buf = fileLine.split(':');
+ buf.pop();
const line = buf.pop();
const file = buf.pop().split('/').pop();
return method + ' ' + file + ' ' + line;
-
}
else
+ {
return 'unknown';
+ }
}
});
diff --git a/js/core/MinimalStim.js b/js/core/MinimalStim.js
index ced6edf..83ea680 100644
--- a/js/core/MinimalStim.js
+++ b/js/core/MinimalStim.js
@@ -1,19 +1,19 @@
/**
* Base class for all stimuli.
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { PsychObject } from '../util/PsychObject';
-import { PsychoJS } from './PsychoJS';
+import {PsychObject} from '../util/PsychObject';
+import {PsychoJS} from './PsychoJS';
/**
*
MinimalStim is the base class for all stimuli.
- *
+ *
* @name module:core.MinimalStim
* @class
* @extends PsychObject
@@ -26,11 +26,11 @@ import { PsychoJS } from './PsychoJS';
export class MinimalStim extends PsychObject
{
constructor({
- name,
- win,
- autoDraw = false,
- autoLog = win.autoLog
- } = {})
+ name,
+ win,
+ autoDraw = false,
+ autoLog = win.autoLog
+ } = {})
{
super(win._psychoJS, name);
@@ -55,31 +55,41 @@ export class MinimalStim extends PsychObject
*/
setAutoDraw(autoDraw, log = false)
{
- let response = { origin : 'MinimalStim.setAutoDraw', context: 'when setting the autoDraw attribute of stimulus: ' + this._name };
+ let response = {
+ origin: 'MinimalStim.setAutoDraw',
+ context: 'when setting the autoDraw attribute of stimulus: ' + this._name
+ };
this._setAttribute('autoDraw', autoDraw, log);
const index = this.win._drawList.indexOf(this);
// autoDraw = true: add the stimulus to the draw list if it's not there already
- if (this._autoDraw) {
- if (this.win) {
+ if (this._autoDraw)
+ {
+ if (this.win)
+ {
// if the stimilus is not already in the draw list:
- if (index < 0) {
+ if (index < 0)
+ {
// update the stimulus if need be before we add its PIXI representation to the window container:
this._updateIfNeeded();
if (typeof this._pixi === 'undefined')
+ {
this.psychoJS.logger.warn('the Pixi.js representation of this stimulus is undefined.');
- // throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
- else {
+ }// throw Object.assign(response, { error: 'the PIXI representation of the stimulus is unavailable'});
+ else
+ {
this.win._rootContainer.addChild(this._pixi);
this.win._drawList.push(this);
}
- } else
+ }
+ else
{
// the stimulus is already in the list, if it needs to be updated, we remove it
// from the window container, update it, then put it back:
- if (this._needUpdate && typeof this._pixi !== 'undefined') {
+ if (this._needUpdate && typeof this._pixi !== 'undefined')
+ {
this.win._rootContainer.removeChild(this._pixi);
this._updateIfNeeded();
this.win._rootContainer.addChild(this._pixi);
@@ -91,13 +101,18 @@ export class MinimalStim extends PsychObject
}
// autoDraw = false: remove the stimulus from the draw list and window container if it's already there
- else {
- if (this.win) {
+ else
+ {
+ if (this.win)
+ {
// if the stimulus is in the draw list, remove it from the list and from the window container:
- if (index >= 0) {
+ if (index >= 0)
+ {
this.win._drawList.splice(index, 1);
if (typeof this._pixi !== 'undefined')
+ {
this.win._rootContainer.removeChild(this._pixi);
+ }
}
}
@@ -108,7 +123,7 @@ export class MinimalStim extends PsychObject
/**
* Draw this stimulus on the next frame draw.
- *
+ *
* @name module:core.MinimalStim#draw
* @function
* @public
@@ -127,7 +142,7 @@ export class MinimalStim extends PsychObject
/**
* Determine whether an object is inside this stimulus.
- *
+ *
* @name module:core.MinimalStim#contains
* @function
* @abstract
@@ -137,7 +152,11 @@ export class MinimalStim extends PsychObject
*/
contains(object, units)
{
- throw {origin: 'MinimalStim.contains', context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`, error: 'this method is abstract and should not be called.'};
+ throw {
+ origin: 'MinimalStim.contains',
+ context: `when determining whether stimulus: ${this._name} contains object: ${util.toString(object)}`,
+ error: 'this method is abstract and should not be called.'
+ };
}
@@ -145,7 +164,7 @@ export class MinimalStim extends PsychObject
* Update the stimulus, if necessary.
*
* Note: this is an abstract function, which should not be called.
- *
+ *
* @name module:core.MinimalStim#_updateIfNeeded
* @function
* @abstract
@@ -153,6 +172,10 @@ export class MinimalStim extends PsychObject
*/
_updateIfNeeded()
{
- throw {origin: 'MinimalStim._updateIfNeeded', context: 'when updating stimulus: ' + this._name, error: 'this method is abstract and should not be called.'};
+ throw {
+ origin: 'MinimalStim._updateIfNeeded',
+ context: 'when updating stimulus: ' + this._name,
+ error: 'this method is abstract and should not be called.'
+ };
}
}
diff --git a/js/core/Mouse.js b/js/core/Mouse.js
index 20caa18..4b3baa5 100644
--- a/js/core/Mouse.js
+++ b/js/core/Mouse.js
@@ -1,21 +1,21 @@
/**
* Manager responsible for the interactions between the experiment's stimuli and the mouse.
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { PsychoJS } from './PsychoJS';
-import { PsychObject } from '../util/PsychObject';
+import {PsychoJS} from './PsychoJS';
+import {PsychObject} from '../util/PsychObject';
import * as util from '../util/Util';
/**
*
This manager handles the interactions between the experiment's stimuli and the mouse.
*
Note: the unit of Mouse is that of its associated Window.
- *
+ *
* @name module:core.Mouse
* @class
* @extends PsychObject
@@ -23,16 +23,18 @@ import * as util from '../util/Util';
* @param {String} options.name - the name used when logging messages from this stimulus
* @param {Window} options.win - the associated Window
* @param {boolean} [options.autoLog= true] - whether or not to log
- *
+ *
* @todo visible is not handled at the moment (mouse is always visible)
*/
-export class Mouse extends PsychObject {
+export class Mouse extends PsychObject
+{
constructor({
- name,
- win,
- autoLog = true
- } = {}) {
+ name,
+ win,
+ autoLog = true
+ } = {})
+ {
super(win._psychoJS, name);
// note: those are in window units:
@@ -50,13 +52,14 @@ export class Mouse extends PsychObject {
/**
* Get the current position of the mouse in mouse/Window units.
- *
+ *
* @name module:core.Mouse#getPos
* @function
* @public
* @return {Array.number} the position of the mouse in mouse/Window units
*/
- getPos() {
+ getPos()
+ {
// get mouse position in the canvas:
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
let pos_px = mouseInfo.pos.slice();
@@ -75,16 +78,20 @@ export class Mouse extends PsychObject {
/**
* Get the position of the mouse relative to that at the last call to getRel
* or getPos, in mouse/Window units.
- *
+ *
* @name module:core.Mouse#getRel
* @function
* @public
* @return {Array.number} the relation position of the mouse in mouse/Window units.
*/
- getRel() {
+ getRel()
+ {
if (typeof this._lastPos === 'undefined')
+ {
return this.getPos();
- else {
+ }
+ else
+ {
// note: (this.getPos()-lastPos) would not work here since getPos changes this._lastPos
const lastPos = this._lastPos;
const pos = this.getPos();
@@ -95,10 +102,10 @@ export class Mouse extends PsychObject {
/**
* Get the travel of the mouse scroll wheel since the last call to getWheelRel.
- *
+ *
*
Note: Even though this method returns a [x, y] array, for most wheels/systems y is the only
* value that varies.
- *
+ *
* @name module:core.Mouse#getWheelRel
* @function
* @public
@@ -119,20 +126,24 @@ export class Mouse extends PsychObject {
/**
* Get the status of each button (pressed or released) and, optionally, the time elapsed between the last call to [clickReset]{@link module:core.Mouse#clickReset} and the pressing or releasing of the buttons.
- *
+ *
*
Note: clickReset is typically called at stimulus onset. When the participant presses a button, the time elapsed since the clickReset is stored internally and can be accessed any time afterwards with getPressed.
- *
+ *
* @name module:core.Mouse#getPressed
* @function
* @public
* @param {boolean} [getTime= false] whether or not to also return timestamps
* @return {Array.number | Array.} either an array of size 3 with the status (1 for pressed, 0 for released) of each mouse button [left, center, right], or a tuple with that array and another array of size 3 with the timestamps.
*/
- getPressed(getTime = false) {
+ getPressed(getTime = false)
+ {
const buttonPressed = this.psychoJS.eventManager.getMouseInfo().buttons.pressed.slice();
if (!getTime)
+ {
return buttonPressed;
- else {
+ }
+ else
+ {
const buttonTimes = this.psychoJS.eventManager.getMouseInfo().buttons.times.slice();
return [buttonPressed, buttonTimes];
}
@@ -141,7 +152,7 @@ export class Mouse extends PsychObject {
/**
* Determine whether the mouse has moved beyond a certain distance.
- *
+ *
*
distance
*
*
mouseMoved() or mouseMoved(undefined, false): determine whether the mouse has moved at all since the last
@@ -149,14 +160,14 @@ export class Mouse extends PsychObject {
*
mouseMoved(distance: number, false): determine whether the mouse has travelled further than distance, in terms of line of sight
*
mouseMoved(distance: [number,number], false): determine whether the mouse has travelled horizontally or vertically further then the given horizontal and vertical distances
*
- *
+ *
*
reset
*
*
mouseMoved(distance, true): reset the mouse move clock, return false
*
mouseMoved(distance, 'here'): return false
*
mouseMoved(distance, [x: number, y: number]: artifically set the previous mouse position to the given coordinates and determine whether the mouse moved further than the given distance
*
- *
+ *
* @name module:core.Mouse#mouseMoved
* @function
* @public
@@ -164,92 +175,120 @@ export class Mouse extends PsychObject {
* @param {boolean|String|Array.number} [reset= false] - see above for a full description
* @return {boolean} see above for a full description
*/
- mouseMoved(distance, reset = false) {
+ mouseMoved(distance, reset = false)
+ {
// make sure that _lastPos is defined:
if (typeof this._lastPos === 'undefined')
+ {
this.getPos();
+ }
this._prevPos = this._lastPos.slice();
this.getPos();
- if (typeof reset === 'boolean' && reset == false) {
+ if (typeof reset === 'boolean' && reset == false)
+ {
if (typeof distance === 'undefined')
+ {
return (this._prevPos[0] != this._lastPos[0]) || (this._prevPos[1] != this._lastPos[1]);
- else {
- if (typeof distance === 'number') {
+ }
+ else
+ {
+ if (typeof distance === 'number')
+ {
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
return (this._movedistance > distance);
}
if (this._prevPos[0] + distance[0] - this._lastPos[0] > 0.0)
- return true; // moved on X-axis
+ {
+ return true;
+ } // moved on X-axis
if (this._prevPos[1] + distance[1] - this._lastPos[0] > 0.0)
- return true; // moved on Y-axis
+ {
+ return true;
+ } // moved on Y-axis
return false;
}
}
- else if (typeof reset === 'boolean' && reset == true) {
+ else if (typeof reset === 'boolean' && reset == true)
+ {
// reset the moveClock:
this.psychoJS.eventManager.getMouseInfo().moveClock.reset();
return false;
}
- else if (reset === 'here') {
+ else if (reset === 'here')
+ {
// set to wherever we are
this._prevPos = this._lastPos.clone();
return false;
}
- else if (reset instanceof Array) {
+ else if (reset instanceof Array)
+ {
// an (x,y) array
// reset to (x,y) to check movement from there
this._prevPos = reset.slice();
if (!distance)
- return false; // just resetting prevPos, not checking distance
- else {
+ {
+ return false;
+ }// just resetting prevPos, not checking distance
+ else
+ {
// checking distance of current pos to newly reset prevposition
- if (typeof distance === 'number') {
+ if (typeof distance === 'number')
+ {
this._movedistance = Math.sqrt((this._prevPos[0] - this._lastPos[0]) * (this._prevPos[0] - this._lastPos[0]) + (this._prevPos[1] - this._lastPos[1]) * (this._prevPos[1] - this._lastPos[1]));
return (this._movedistance > distance);
}
if (Math.abs(this._lastPos[0] - this._prevPos[0]) > distance[0])
- return true; // moved on X-axis
+ {
+ return true;
+ } // moved on X-axis
if (Math.abs(this._lastPos[1] - this._prevPos[1]) > distance[1])
- return true; // moved on Y-axis
+ {
+ return true;
+ } // moved on Y-axis
return false;
}
}
else
+ {
return false;
+ }
}
/**
* Get the amount of time elapsed since the last mouse movement.
- *
+ *
* @name module:core.Mouse#mouseMoveTime
* @function
* @public
* @return {number} the time elapsed since the last mouse movement
*/
- mouseMoveTime() {
+ mouseMoveTime()
+ {
return this.psychoJS.eventManager.getMouseInfo().moveClock.getTime();
}
/**
* Reset the clocks associated to the given mouse buttons.
- *
+ *
* @name module:core.Mouse#clickReset
* @function
* @public
* @param {Array.number} [buttons= [0,1,2]] the buttons to reset (0: left, 1: center, 2: right)
*/
- clickReset(buttons = [0, 1, 2]) {
+ clickReset(buttons = [0, 1, 2])
+ {
const mouseInfo = this.psychoJS.eventManager.getMouseInfo();
- for (const b of buttons) {
+ for (const b of buttons)
+ {
mouseInfo.buttons.clocks[b].reset();
mouseInfo.buttons.times[b] = 0.0;
}
diff --git a/js/core/PsychoJS.js b/js/core/PsychoJS.js
index 9335ce3..91488e9 100644
--- a/js/core/PsychoJS.js
+++ b/js/core/PsychoJS.js
@@ -3,24 +3,23 @@
* Main component of the PsychoJS library.
*
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { Scheduler } from '../util/Scheduler';
-import { ServerManager } from './ServerManager';
-import { ExperimentHandler } from '../data/ExperimentHandler';
-import { EventManager } from './EventManager';
-import { Window } from './Window';
-import { GUI } from './GUI';
-import { MonotonicClock } from '../util/Clock';
-import { Logger } from './Logger';
+import {Scheduler} from '../util/Scheduler';
+import {ServerManager} from './ServerManager';
+import {ExperimentHandler} from '../data/ExperimentHandler';
+import {EventManager} from './EventManager';
+import {Window} from './Window';
+import {GUI} from './GUI';
+import {MonotonicClock} from '../util/Clock';
+import {Logger} from './Logger';
import * as util from '../util/Util';
-
/**
*
PsychoJS manages the lifecycle of an experiment. It initialises the PsychoJS library and its various components (e.g. the {@link ServerManager}, the {@link EventManager}), and is used by the experiment to schedule the various tasks.
*
@@ -34,25 +33,81 @@ export class PsychoJS
/**
* Properties
*/
- get status() { return this._status; }
- set status(status) {
+ get status()
+ {
+ return this._status;
+ }
+
+ set status(status)
+ {
this._status = status;
}
- get config() { return this._config; }
- get window() { return this._window; }
- get serverManager() { return this._serverManager; }
- get experiment() { return this._experiment; }
- get scheduler() { return this._scheduler; }
- get monotonicClock() { return this._monotonicClock; }
- get logger() { return this._logger.consoleLogger; }
- get experimentLogger() { return this._logger; }
- get eventManager() { return this._eventManager; }
- get gui() { return this._gui; }
- get IP() { return this._IP; }
- // this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
- get serverMsg() { return this._serverMsg; }
- get browser() { return this._browser; }
+ get config()
+ {
+ return this._config;
+ }
+
+ get window()
+ {
+ return this._window;
+ }
+
+ get serverManager()
+ {
+ return this._serverManager;
+ }
+
+ get experiment()
+ {
+ return this._experiment;
+ }
+
+ get scheduler()
+ {
+ return this._scheduler;
+ }
+
+ get monotonicClock()
+ {
+ return this._monotonicClock;
+ }
+
+ get logger()
+ {
+ return this._logger.consoleLogger;
+ }
+
+ get experimentLogger()
+ {
+ return this._logger;
+ }
+
+ get eventManager()
+ {
+ return this._eventManager;
+ }
+
+ get gui()
+ {
+ return this._gui;
+ }
+
+ get IP()
+ {
+ return this._IP;
+ }
+
+ // this._serverMsg is a bi-directional message board for communications with the pavlovia.org server:
+ get serverMsg()
+ {
+ return this._serverMsg;
+ }
+
+ get browser()
+ {
+ return this._browser;
+ }
/**
@@ -63,7 +118,7 @@ export class PsychoJS
debug = true,
collectIP = false,
topLevelStatus = true
- } = {})
+ } = {})
{
// logging:
this._logger = new Logger(this, (debug) ? log4javascript.Level.DEBUG : log4javascript.Level.INFO);
@@ -104,13 +159,15 @@ export class PsychoJS
// make the PsychoJS.Status accessible from the top level of the generated experiment script
// in order to accommodate PsychoPy's Code Components
if (topLevelStatus)
+ {
this._makeStatusTopLevel();
+ }
this.logger.info('[PsychoJS] Initialised.');
+ this.logger.info('[PsychoJS] @version 2020.5');
}
-
/**
* Get the experiment's environment.
*
@@ -119,10 +176,13 @@ export class PsychoJS
getEnvironment()
{
if (typeof this._config === 'undefined')
+ {
return undefined;
+ }
return this._config.environment;
}
+
/**
* Open a PsychoJS Window.
*
@@ -142,17 +202,24 @@ export class PsychoJS
* @public
*/
openWindow({
- name,
- fullscr,
- color,
- units,
- waitBlanking,
- autoLog
- } = {}) {
+ name,
+ fullscr,
+ color,
+ units,
+ waitBlanking,
+ autoLog
+ } = {})
+ {
this.logger.info('[PsychoJS] Open Window.');
if (typeof this._window !== 'undefined')
- throw { origin : 'PsychoJS.openWindow', context : 'when opening a Window', error : 'A Window has already been opened.' };
+ {
+ throw {
+ origin: 'PsychoJS.openWindow',
+ context: 'when opening a Window',
+ error: 'A Window has already been opened.'
+ };
+ }
this._window = new Window({
psychoJS: this,
@@ -172,7 +239,8 @@ export class PsychoJS
* @param {string} completionUrl - the completion URL
* @param {string} cancellationUrl - the cancellation URL
*/
- setRedirectUrls(completionUrl, cancellationUrl) {
+ setRedirectUrls(completionUrl, cancellationUrl)
+ {
this._completionUrl = completionUrl;
this._cancellationUrl = cancellationUrl;
}
@@ -185,7 +253,8 @@ export class PsychoJS
* @param args - arguments for that task
* @public
*/
- schedule(task, args) {
+ schedule(task, args)
+ {
this.logger.debug('schedule task: ', task.toString().substring(0, 50), '...');
this._scheduler.add(task, args);
@@ -204,7 +273,8 @@ export class PsychoJS
* @param {Scheduler} elseScheduler scheduler to run if the condition is false
* @public
*/
- scheduleCondition(condition, thenScheduler, elseScheduler) {
+ scheduleCondition(condition, thenScheduler, elseScheduler)
+ {
this.logger.debug('schedule condition: ', condition.toString().substring(0, 50), '...');
this._scheduler.addConditional(condition, thenScheduler, elseScheduler);
@@ -224,27 +294,31 @@ export class PsychoJS
*
* @todo: close session on window or tab close
*/
- async start({ configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = [] } = {})
+ async start({configURL = 'config.json', expName = 'UNKNOWN', expInfo, resources = []} = {})
{
this.logger.debug();
- const response = { origin: 'PsychoJS.start', context: 'when starting the experiment' };
+ const response = {origin: 'PsychoJS.start', context: 'when starting the experiment'};
- try {
+ try
+ {
// configure the experiment:
await this._configure(configURL, expName);
// get the participant IP:
if (this._collectIP)
+ {
this._getParticipantIPInfo();
- else {
+ }
+ else
+ {
this._IP = {
IP: 'X',
- hostname : 'X',
- city : 'X',
- region : 'X',
- country : 'X',
- location : 'X'
+ hostname: 'X',
+ city: 'X',
+ region: 'X',
+ country: 'X',
+ location: 'X'
};
}
@@ -264,21 +338,42 @@ export class PsychoJS
// open a session:
await this._serverManager.openSession();
- // attempt to close the session on beforeunload/unload (we use a synchronous request since
- // the Beacon API only allows POST and we need DELETE ) and release the WebGL context:
- const self = this;
- window.onbeforeunload = () => {
- self._serverManager.closeSession(false, true);
+ // warn the user when they attempt to close the tab or browser:
+ this.beforeunloadCallback = (event) =>
+ {
+ // preventDefault should ensure that the user gets prompted:
+ event.preventDefault();
- if (typeof self._window !== 'undefined')
- self._window.close();
+ // Chrome requires returnValue to be set:
+ event.returnValue = '';
};
- window.addEventListener('unload', function(event) {
- self._serverManager.closeSession(false, true);
+ window.addEventListener('beforeunload', this.beforeunloadCallback);
+
+
+ // when the user closes the tab or browser, we attempt to close the session, optionally save the results,
+ // and release the WebGL context
+ // note: we communicate with the server using the Beacon API
+ const self = this;
+ window.addEventListener('unload', (event) =>
+ {
+ if (self._config.session.status === 'OPEN')
+ {
+ // save the incomplete results if need be:
+ if (self._config.experiment.saveIncompleteResults)
+ {
+ self._experiment.save({sync: true});
+ }
+
+ // close the session:
+ self._serverManager.closeSession(false, true);
+ }
if (typeof self._window !== 'undefined')
+ {
self._window.close();
+ }
});
+
}
@@ -289,9 +384,10 @@ export class PsychoJS
this.logger.info('[PsychoJS] Start Experiment.');
this._scheduler.start();
}
- catch (error) {
+ catch (error)
+ {
// this._gui.dialog({ error: { ...response, error } });
- this._gui.dialog({ error: Object.assign(response, { error }) });
+ this._gui.dialog({error: Object.assign(response, {error})});
}
}
@@ -310,13 +406,16 @@ export class PsychoJS
* @async
* @public
*/
- async downloadResources(resources = []) {
- try {
+ async downloadResources(resources = [])
+ {
+ try
+ {
await this.serverManager.downloadResources(resources);
}
- catch (error) {
+ catch (error)
+ {
// this._gui.dialog({ error: { ...response, error } });
- this._gui.dialog({ error: Object.assign(response, { error }) });
+ this._gui.dialog({error: Object.assign(response, {error})});
}
}
@@ -328,13 +427,17 @@ export class PsychoJS
* @param {Object.} obj the object whose attributes we will mirror
* @public
*/
- importAttributes(obj) {
+ importAttributes(obj)
+ {
this.logger.debug('import attributes from: ', util.toString(obj));
if (typeof obj === 'undefined')
+ {
return;
+ }
- for (const attribute in obj) {
+ for (const attribute in obj)
+ {
// this[attribute] = obj[attribute];
window[attribute] = obj[attribute];
}
@@ -354,26 +457,38 @@ export class PsychoJS
* @async
* @public
*/
- async quit({ message, isCompleted = false } = {}) {
+ async quit({message, isCompleted = false} = {})
+ {
this.logger.info('[PsychoJS] Quit.');
this._experiment.experimentEnded = true;
this._status = PsychoJS.Status.FINISHED;
- try {
+ try
+ {
// stop the main scheduler:
this._scheduler.stop();
+ // remove the beforeunload listener:
+ if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
+ {
+ window.removeEventListener('beforeunload', this.beforeunloadCallback);
+ }
+
// save the results and the logs of the experiment:
this.gui.dialog({
warning: 'Closing the session. Please wait a few moments.',
showOK: false
});
- await this._experiment.save();
- await this._logger.flush();
+ if (isCompleted || this._config.experiment.saveIncompleteResults)
+ {
+ await this._experiment.save();
+ await this._logger.flush();
+ }
// close the session:
- if (this.getEnvironment() === ExperimentHandler.Environment.SERVER) {
+ if (this.getEnvironment() === ExperimentHandler.Environment.SERVER)
+ {
await this._serverManager.closeSession(isCompleted);
}
@@ -383,29 +498,37 @@ export class PsychoJS
const self = this;
this._gui.dialog({
message: text,
- onOK: () => {
+ onOK: () =>
+ {
// close the window:
self._window.close();
// remove everything from the browser window:
while (document.body.hasChildNodes())
+ {
document.body.removeChild(document.body.lastChild);
+ }
// return from fullscreen if we were there:
this._window.closeFullScreen();
// redirect if redirection URLs have been provided:
if (isCompleted && typeof self._completionUrl !== 'undefined')
+ {
window.location = self._completionUrl;
+ }
else if (!isCompleted && typeof self._cancellationUrl !== 'undefined')
+ {
window.location = self._cancellationUrl;
+ }
}
});
}
- catch (error) {
+ catch (error)
+ {
console.error(error);
- this._gui.dialog({ error });
+ this._gui.dialog({error});
}
}
@@ -418,21 +541,25 @@ export class PsychoJS
* @param {string} configURL - the URL of the configuration file
* @param {string} name - the name of the experiment
*/
- async _configure(configURL, name) {
- const response = { origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment' };
+ async _configure(configURL, name)
+ {
+ const response = {origin: 'PsychoJS.configure', context: 'when configuring PsychoJS for the experiment'};
- try {
+ try
+ {
this.status = PsychoJS.Status.CONFIGURING;
// if the experiment is running from the pavlovia.org server, we read the configuration file:
const experimentUrl = window.location.href;
- if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0) {
+ if (experimentUrl.indexOf('https://run.pavlovia.org/') === 0 || experimentUrl.indexOf('https://pavlovia.org/run/') === 0)
+ {
const serverResponse = await this._serverManager.getConfiguration(configURL);
this._config = serverResponse.config;
// legacy experiments had a psychoJsManager block instead of a pavlovia block, and the URL
// pointed to https://pavlovia.org/server
- if ('psychoJsManager' in this._config) {
+ if ('psychoJsManager' in this._config)
+ {
delete this._config.psychoJsManager;
this._config.pavlovia = {
URL: 'https://pavlovia.org'
@@ -441,41 +568,56 @@ export class PsychoJS
// tests for the presence of essential blocks in the configuration:
if (!('experiment' in this._config))
+ {
throw 'missing experiment block in configuration';
+ }
if (!('name' in this._config.experiment))
+ {
throw 'missing name in experiment block in configuration';
+ }
if (!('fullpath' in this._config.experiment))
+ {
throw 'missing fullpath in experiment block in configuration';
+ }
if (!('pavlovia' in this._config))
+ {
throw 'missing pavlovia block in configuration';
+ }
if (!('URL' in this._config.pavlovia))
+ {
throw 'missing URL in pavlovia block in configuration';
+ }
this._config.environment = ExperimentHandler.Environment.SERVER;
- } else
+ }
+ else
// otherwise we create an ad-hoc configuration:
{
this._config = {
environment: ExperimentHandler.Environment.LOCAL,
- experiment: { name, saveFormat: ExperimentHandler.SaveFormat.CSV }
+ experiment: {name, saveFormat: ExperimentHandler.SaveFormat.CSV}
};
}
// get the server parameters (those starting with a double underscore):
this._serverMsg = new Map();
- util.getUrlParameters().forEach((value, key) => {
+ util.getUrlParameters().forEach((value, key) =>
+ {
if (key.indexOf('__') === 0)
+ {
this._serverMsg.set(key, value);
+ }
});
this.status = PsychoJS.Status.CONFIGURED;
this.logger.debug('configuration:', util.toString(this._config));
}
- catch (error) {
+ catch (error)
+ {
// throw { ...response, error };
- throw Object.assign(response, { error });
+ throw Object.assign(response, {error});
}
}
@@ -486,13 +628,18 @@ export class PsychoJS
*
Note: we use [http://www.geoplugin.net/json.gp]{@link http://www.geoplugin.net/json.gp}.
* @protected
*/
- async _getParticipantIPInfo() {
- const response = { origin: 'PsychoJS._getParticipantIPInfo', context: 'when getting the IP information of the participant' };
+ async _getParticipantIPInfo()
+ {
+ const response = {
+ origin: 'PsychoJS._getParticipantIPInfo',
+ context: 'when getting the IP information of the participant'
+ };
this.logger.debug('getting the IP information of the participant');
this._IP = {};
- try {
+ try
+ {
const geoResponse = await $.get('http://www.geoplugin.net/json.gp');
const geoData = JSON.parse(geoResponse);
this._IP = {
@@ -503,9 +650,10 @@ export class PsychoJS
};
this.logger.debug('IP information of the participant: ' + util.toString(this._IP));
}
- catch (error) {
+ catch (error)
+ {
// throw { ...response, error };
- throw Object.assign(response, { error });
+ throw Object.assign(response, {error});
}
}
@@ -515,13 +663,15 @@ export class PsychoJS
*
* @protected
*/
- _captureErrors() {
+ _captureErrors()
+ {
this.logger.debug('capturing all errors using window.onerror');
const self = this;
- window.onerror = function (message, source, lineno, colno, error) {
+ window.onerror = function (message, source, lineno, colno, error)
+ {
console.error(error);
- self._gui.dialog({ "error": error });
+ self._gui.dialog({"error": error});
return true;
};
@@ -539,8 +689,10 @@ export class PsychoJS
* Make the various Status top level, in order to accommodate PsychoPy's Code Components.
* @private
*/
- _makeStatusTopLevel() {
- for (const status in PsychoJS.Status) {
+ _makeStatusTopLevel()
+ {
+ for (const status in PsychoJS.Status)
+ {
window[status] = PsychoJS.Status[status];
}
}
diff --git a/js/core/ServerManager.js b/js/core/ServerManager.js
index 3e9bffa..12df547 100644
--- a/js/core/ServerManager.js
+++ b/js/core/ServerManager.js
@@ -2,19 +2,19 @@
* Manager responsible for the communication between the experiment running in the participant's browser and the remote PsychoJS manager running on the remote https://pavlovia.org server.
*
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { PsychoJS } from './PsychoJS';
-import { PsychObject } from '../util/PsychObject';
+import {PsychoJS} from './PsychoJS';
+import {PsychObject} from '../util/PsychObject';
import * as util from '../util/Util';
import {ExperimentHandler} from "../data/ExperimentHandler";
import {MonotonicClock} from "../util/Clock";
-// import { Howl } from 'howler';
+// import { Howl } from 'howler';
/**
@@ -29,12 +29,14 @@ import {MonotonicClock} from "../util/Clock";
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
-export class ServerManager extends PsychObject {
+export class ServerManager extends PsychObject
+{
constructor({
- psychoJS,
- autoLog = false
- } = {}) {
+ psychoJS,
+ autoLog = false
+ } = {})
+ {
super(psychoJS);
// session:
@@ -66,15 +68,22 @@ export class ServerManager extends PsychObject {
*
* @returns {Promise} the response
*/
- getConfiguration(configURL) {
- const response = { origin: 'ServerManager.getConfiguration', context: 'when reading the configuration file: ' + configURL };
+ getConfiguration(configURL)
+ {
+ const response = {
+ origin: 'ServerManager.getConfiguration',
+ context: 'when reading the configuration file: ' + configURL
+ };
this._psychoJS.logger.debug('reading the configuration file: ' + configURL);
- return new Promise((resolve, reject) => {
+ const self = this;
+ return new Promise((resolve, reject) =>
+ {
$.get(configURL, 'json')
- .done((config, textStatus) => {
+ .done((config, textStatus) =>
+ {
// resolve({ ...response, config });
- resolve(Object.assign(response, { config }));
+ resolve(Object.assign(response, {config}));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
@@ -83,7 +92,7 @@ export class ServerManager extends PsychObject {
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error('error:', errorMsg);
- reject(Object.assign(response, { error: errorMsg }));
+ reject(Object.assign(response, {error: errorMsg}));
});
});
}
@@ -118,7 +127,9 @@ export class ServerManager extends PsychObject {
// prepare POST query:
let data = {};
if (this._psychoJS._serverMsg.has('__pilotToken'))
+ {
data.pilotToken = this._psychoJS._serverMsg.get('__pilotToken');
+ }
// query pavlovia server:
const self = this;
@@ -126,38 +137,44 @@ export class ServerManager extends PsychObject {
{
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) + '/sessions';
$.post(url, data, null, 'json')
- .done((data, textStatus) =>
- {
- if (!('token' in data)) {
+ .done((data, textStatus) =>
+ {
+ if (!('token' in data))
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ reject(Object.assign(response, {error: 'unexpected answer from server: no token'}));
+ // reject({...response, error: 'unexpected answer from server: no token'});
+ }
+ if (!('experiment' in data))
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+ // reject({...response, error: 'unexpected answer from server: no experiment'});
+ reject(Object.assign(response, {error: 'unexpected answer from server: no experiment'}));
+ }
+
+ self._psychoJS.config.session = {
+ token: data.token,
+ status: 'OPEN'
+ };
+ self._psychoJS.config.experiment.status = data.experiment.status2;
+ self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
+ self._psychoJS.config.experiment.saveIncompleteResults = data.experiment.saveIncompleteResults;
+ self._psychoJS.config.experiment.license = data.experiment.license;
+ self._psychoJS.config.experiment.runMode = data.experiment.runMode;
+
+ self.setStatus(ServerManager.Status.READY);
+ // resolve({ ...response, token: data.token, status: data.status });
+ resolve(Object.assign(response, {token: data.token, status: data.status}));
+ })
+ .fail((jqXHR, textStatus, errorThrown) =>
+ {
self.setStatus(ServerManager.Status.ERROR);
- reject(Object.assign(response, { error: 'unexpected answer from server: no token'}));
- // reject({...response, error: 'unexpected answer from server: no token'});
- }
- self._psychoJS.config.session = { token: data.token };
- if (!('experiment' in data)) {
- self.setStatus(ServerManager.Status.ERROR);
- // reject({...response, error: 'unexpected answer from server: no experiment'});
- reject(Object.assign(response, { error: 'unexpected answer from server: no experiment'}));
- }
- self._psychoJS.config.experiment.status = data.experiment.status2;
- self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
- self._psychoJS.config.experiment.license = data.experiment.license;
- self._psychoJS.config.experiment.runMode = data.experiment.runMode;
+ const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
+ console.error('error:', errorMsg);
- self.setStatus(ServerManager.Status.READY);
- // resolve({ ...response, token: data.token, status: data.status });
- resolve(Object.assign(response, { token: data.token, status: data.status }));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
- {
- self.setStatus(ServerManager.Status.ERROR);
-
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
-
- reject(Object.assign(response, { error: errorMsg }));
- });
+ reject(Object.assign(response, {error: errorMsg}));
+ });
});
}
@@ -178,8 +195,12 @@ export class ServerManager extends PsychObject {
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
* @returns {Promise | void} the response
*/
- closeSession(isCompleted = false, sync = false) {
- const response = { origin: 'ServerManager.closeSession', context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath };
+ async closeSession(isCompleted = false, sync = false)
+ {
+ const response = {
+ origin: 'ServerManager.closeSession',
+ context: 'when closing the session for experiment: ' + this._psychoJS.config.experiment.fullpath
+ };
this._psychoJS.logger.debug('closing the session for experiment: ' + this._psychoJS.config.experiment.name);
@@ -187,44 +208,61 @@ export class ServerManager extends PsychObject {
// prepare DELETE query:
const url = this._psychoJS.config.pavlovia.URL + '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) + '/sessions/' + this._psychoJS.config.session.token;
- const data = { isCompleted };
// synchronous query the pavlovia server:
if (sync)
{
+ /* This is now deprecated in most browsers.
const request = new XMLHttpRequest();
request.open("DELETE", url, false);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(data));
-
- return;
- }
-
-
- // asynchronously query the pavlovia server:
- const self = this;
- return new Promise((resolve, reject) => {
- $.ajax({
- url,
- type: 'delete',
- data,
- dataType: 'json'
- })
- .done((data, textStatus) => {
- self.setStatus(ServerManager.Status.READY);
- // resolve({ ...response, data });
- resolve(Object.assign(response, { data }));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
- {
- self.setStatus(ServerManager.Status.ERROR);
-
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
-
- reject(Object.assign(response, { error: errorMsg }));
+ */
+ /* This does not work in Chrome before of a CORS bug
+ await fetch(url, {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json;charset=UTF-8' },
+ body: JSON.stringify(data),
+ // keepalive makes it possible for the request to outlive the page (e.g. when the participant closes the tab)
+ keepalive: true
});
- });
+ */
+ const formData = new FormData();
+ formData.append('isCompleted', isCompleted);
+ navigator.sendBeacon(url + '/delete', formData);
+ this._psychoJS.config.session.status = 'CLOSED';
+ }
+ // asynchronously query the pavlovia server:
+ else
+ {
+ const self = this;
+ return new Promise((resolve, reject) =>
+ {
+ $.ajax({
+ url,
+ type: 'delete',
+ data: {isCompleted},
+ dataType: 'json'
+ })
+ .done((data, textStatus) =>
+ {
+ self.setStatus(ServerManager.Status.READY);
+ self._psychoJS.config.session.status = 'CLOSED';
+
+ // resolve({ ...response, data });
+ resolve(Object.assign(response, {data}));
+ })
+ .fail((jqXHR, textStatus, errorThrown) =>
+ {
+ self.setStatus(ServerManager.Status.ERROR);
+
+ const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
+ console.error('error:', errorMsg);
+
+ reject(Object.assign(response, {error: errorMsg}));
+ });
+ });
+ }
}
@@ -238,13 +276,19 @@ export class ServerManager extends PsychObject {
* @return {Object} value of the resource
* @throws {Object.} exception if no resource with that name has previously been registered
*/
- getResource(name) {
- const response = { origin: 'ServerManager.getResource', context: 'when getting the value of resource: ' + name };
+ getResource(name)
+ {
+ const response = {
+ origin: 'ServerManager.getResource',
+ context: 'when getting the value of resource: ' + name
+ };
const path_data = this._resources.get(name);
if (typeof path_data === 'undefined')
- // throw { ...response, error: 'unknown resource' };
- throw Object.assign(response, { error: 'unknown resource' });
+ // throw { ...response, error: 'unknown resource' };
+ {
+ throw Object.assign(response, {error: 'unknown resource'});
+ }
return path_data.data;
}
@@ -257,17 +301,25 @@ export class ServerManager extends PsychObject {
* @function
* @public
*/
- setStatus(status) {
- const response = { origin: 'ServerManager.setStatus', context: 'when changing the status of the server manager to: ' + util.toString(status) };
+ setStatus(status)
+ {
+ const response = {
+ origin: 'ServerManager.setStatus',
+ context: 'when changing the status of the server manager to: ' + util.toString(status)
+ };
// check status:
const statusKey = (typeof status === 'symbol') ? Symbol.keyFor(status) : null;
if (!statusKey)
- // throw { ...response, error: 'status must be a symbol' };
- throw Object.assign(response, { error: 'status must be a symbol' });
+ // throw { ...response, error: 'status must be a symbol' };
+ {
+ throw Object.assign(response, {error: 'status must be a symbol'});
+ }
if (!ServerManager.Status.hasOwnProperty(statusKey))
- // throw { ...response, error: 'unknown status' };
- throw Object.assign(response, { error: 'unknown status' });
+ // throw { ...response, error: 'unknown status' };
+ {
+ throw Object.assign(response, {error: 'unknown status'});
+ }
this._status = status;
@@ -286,7 +338,8 @@ export class ServerManager extends PsychObject {
* @public
* @return {ServerManager.Status.READY} the new status
*/
- resetStatus() {
+ resetStatus()
+ {
return this.setStatus(ServerManager.Status.READY);
}
@@ -307,8 +360,12 @@ export class ServerManager extends PsychObject {
* @function
* @public
*/
- downloadResources(resources = []) {
- const response = { origin: 'ServerManager.downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
+ downloadResources(resources = [])
+ {
+ const response = {
+ origin: 'ServerManager.downloadResources',
+ context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
+ };
this._psychoJS.logger.debug('downloading resources for experiment: ' + this._psychoJS.config.experiment.name);
@@ -316,26 +373,37 @@ export class ServerManager extends PsychObject {
// but we want to run the asynchronous _listResources and _downloadResources in sequence
const self = this;
const newResources = new Map();
- let download = async () => {
- try {
- if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER) {
+ let download = async () =>
+ {
+ try
+ {
+ if (self._psychoJS.config.environment === ExperimentHandler.Environment.SERVER)
+ {
// no resources specified, we register them all:
- if (resources.length === 0) {
+ if (resources.length === 0)
+ {
// list the resources from the resources directory of the experiment on the server:
const serverResponse = await self._listResources();
for (const name of serverResponse.resources)
- self._resources.set(name, { path: serverResponse.resourceDirectory + '/' + name });
+ {
+ self._resources.set(name, {path: serverResponse.resourceDirectory + '/' + name});
+ }
}
- else {
+ else
+ {
// only registered the specified resources:
- for (const {name, path} of resources) {
+ for (const {name, path} of resources)
+ {
self._resources.set(name, {path});
newResources.set(name, {path});
}
}
- } else {
+ }
+ else
+ {
// register the specified resources:
- for (const {name, path} of resources) {
+ for (const {name, path} of resources)
+ {
self._resources.set(name, {path});
newResources.set(name, {path});
}
@@ -343,17 +411,23 @@ export class ServerManager extends PsychObject {
self._nbResources = self._resources.size;
for (const name of self._resources.keys())
+ {
this._psychoJS.logger.debug('resource:', name, self._resources.get(name).path);
+ }
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCES_REGISTERED, count: self._nbResources });
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCES_REGISTERED,
+ count: self._nbResources
+ });
// download the registered resources:
await self._downloadRegisteredResources(newResources);
}
- catch (error) {
+ catch (error)
+ {
console.log('error', error);
// throw { ...response, error: error };
- throw Object.assign(response, { error });
+ throw Object.assign(response, {error});
}
};
@@ -375,10 +449,11 @@ export class ServerManager extends PsychObject {
* @public
* @param {string} key - the data key (e.g. the name of .csv file)
* @param {string} value - the data value (e.g. a string containing the .csv header and records)
+ * @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
*
* @returns {Promise} the response
*/
- uploadData(key, value)
+ uploadData(key, value, sync = false)
{
const response = {
origin: 'ServerManager.uploadData',
@@ -388,41 +463,50 @@ export class ServerManager extends PsychObject {
this._psychoJS.logger.debug('uploading data for experiment: ' + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
- // prepare the POST query:
- const data = {
- key,
- value
- };
+ const url = this._psychoJS.config.pavlovia.URL +
+ '/api/v2/experiments/' + encodeURIComponent(this._psychoJS.config.experiment.fullpath) +
+ '/sessions/' + this._psychoJS.config.session.token +
+ '/results';
- // query the pavlovia server:
- const self = this;
- return new Promise((resolve, reject) =>
+ // synchronous query the pavlovia server:
+ if (sync)
{
- const url = self._psychoJS.config.pavlovia.URL +
- '/api/v2/experiments/' + encodeURIComponent(self._psychoJS.config.experiment.fullpath) +
- '/sessions/' + self._psychoJS.config.session.token +
- '/results';
-
- $.post(url, data, null, 'json')
- .done((serverData, textStatus) =>
+ const formData = new FormData();
+ formData.append('key', key);
+ formData.append('value', value);
+ navigator.sendBeacon(url, formData);
+ }
+ // asynchronously query the pavlovia server:
+ else
+ {
+ const self = this;
+ return new Promise((resolve, reject) =>
{
- self.setStatus(ServerManager.Status.READY);
- resolve(Object.assign(response, { serverData }));
- })
- .fail((jqXHR, textStatus, errorThrown) =>
- {
- self.setStatus(ServerManager.Status.ERROR);
+ const data = {
+ key,
+ value
+ };
- const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
- console.error('error:', errorMsg);
+ $.post(url, data, null, 'json')
+ .done((serverData, textStatus) =>
+ {
+ self.setStatus(ServerManager.Status.READY);
+ resolve(Object.assign(response, {serverData}));
+ })
+ .fail((jqXHR, textStatus, errorThrown) =>
+ {
+ self.setStatus(ServerManager.Status.ERROR);
- reject(Object.assign(response, { error: errorMsg }));
+ const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
+ console.error('error:', errorMsg);
+
+ reject(Object.assign(response, {error: errorMsg}));
+ });
});
- });
+ }
}
-
/**
* Asynchronously upload experiment logs to the remote PsychoJS manager.
*
@@ -468,7 +552,7 @@ export class ServerManager extends PsychObject {
.done((serverData, textStatus) =>
{
self.setStatus(ServerManager.Status.READY);
- resolve(Object.assign(response, { serverData }));
+ resolve(Object.assign(response, {serverData}));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
@@ -477,14 +561,12 @@ export class ServerManager extends PsychObject {
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error('error:', errorMsg);
- reject(Object.assign(response, { error: errorMsg }));
+ reject(Object.assign(response, {error: errorMsg}));
});
});
}
-
-
/**
* List the resources available to the experiment.
@@ -524,18 +606,21 @@ export class ServerManager extends PsychObject {
{
self.setStatus(ServerManager.Status.ERROR);
// reject({ ...response, error: 'unexpected answer from server: no resources' });
- reject(Object.assign(response, { error: 'unexpected answer from server: no resources' }));
+ reject(Object.assign(response, {error: 'unexpected answer from server: no resources'}));
}
if (!('resourceDirectory' in data))
{
self.setStatus(ServerManager.Status.ERROR);
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
- reject(Object.assign(response, { error: 'unexpected answer from server: no resourceDirectory' }));
+ reject(Object.assign(response, {error: 'unexpected answer from server: no resourceDirectory'}));
}
self.setStatus(ServerManager.Status.READY);
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
- resolve(Object.assign(response, { resources: data.resources, resourceDirectory: data.resourceDirectory }));
+ resolve(Object.assign(response, {
+ resources: data.resources,
+ resourceDirectory: data.resourceDirectory
+ }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
@@ -544,14 +629,13 @@ export class ServerManager extends PsychObject {
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error('error:', errorMsg);
- reject(Object.assign(response, { error: errorMsg }));
+ reject(Object.assign(response, {error: errorMsg}));
});
});
}
-
/**
* Download the resources previously registered.
*
@@ -563,7 +647,10 @@ export class ServerManager extends PsychObject {
*/
_downloadRegisteredResources(resources = new Map())
{
- const response = { origin: 'ServerManager._downloadResources', context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name };
+ const response = {
+ origin: 'ServerManager._downloadResources',
+ context: 'when downloading the resources for experiment: ' + this._psychoJS.config.experiment.name
+ };
this._psychoJS.logger.debug('downloading the registered resources for experiment: ' + this._psychoJS.config.experiment.name);
@@ -578,32 +665,43 @@ export class ServerManager extends PsychObject {
const filesToDownload = resources.size ? resources : this._resources;
- this._resourceQueue.addEventListener("filestart", event => {
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: event.item.id });
+ this._resourceQueue.addEventListener("filestart", event =>
+ {
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCE,
+ resource: event.item.id
+ });
});
- this._resourceQueue.addEventListener("fileload", event => {
+ this._resourceQueue.addEventListener("fileload", event =>
+ {
++self._nbLoadedResources;
let path_data = self._resources.get(event.item.id);
path_data.data = event.result;
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: event.item.id });
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCE_DOWNLOADED,
+ resource: event.item.id
+ });
});
// loading completed:
- this._resourceQueue.addEventListener("complete", event => {
+ this._resourceQueue.addEventListener("complete", event =>
+ {
self._resourceQueue.close();
- if (self._nbLoadedResources === filesToDownload.size) {
+ if (self._nbLoadedResources === filesToDownload.size)
+ {
self.setStatus(ServerManager.Status.READY);
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
+ self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
}
});
// error: we throw an exception
- this._resourceQueue.addEventListener("error", event => {
+ this._resourceQueue.addEventListener("error", event =>
+ {
self.setStatus(ServerManager.Status.ERROR);
- const resourceId = (typeof event.data !== 'undefined')?event.data.id:'UNKNOWN RESOURCE';
+ const resourceId = (typeof event.data !== 'undefined') ? event.data.id : 'UNKNOWN RESOURCE';
// throw { ...response, error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' };
- throw Object.assign(response, { error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')' });
+ throw Object.assign(response, {error: 'unable to download resource: ' + resourceId + ' (' + event.title + ')'});
});
@@ -623,9 +721,9 @@ export class ServerManager extends PsychObject {
// preload.js with forced binary for xls and xlsx:
if (['csv', 'odp', 'xls', 'xlsx'].indexOf(extension) > -1)
- manifest.push({ id: name, src: path_data.path, type: createjs.Types.BINARY });
-
- /* ascii .csv are adequately handled in binary format
+ {
+ manifest.push({id: name, src: path_data.path, type: createjs.Types.BINARY});
+ }/* ascii .csv are adequately handled in binary format
// forced text for .csv:
else if (['csv'].indexOf(resourceExtension) > -1)
manifest.push({ id: resourceName, src: resourceName, type: createjs.Types.TEXT });
@@ -637,29 +735,41 @@ export class ServerManager extends PsychObject {
soundResources.push(name);
if (extension === 'wav')
+ {
this.psychoJS.logger.warn(`wav files are not supported by all browsers. We recommend you convert "${name}" to another format, e.g. mp3`);
+ }
}
// preload.js for the other extensions (download type decided by preload.js):
else
- manifest.push({ id: name, src: path_data.path });
+ {
+ manifest.push({id: name, src: path_data.path});
+ }
}
// (*) start loading non-sound resources:
if (manifest.length > 0)
+ {
this._resourceQueue.loadManifest(manifest);
- else {
- if (this._nbLoadedResources === filesToDownload.size) {
+ }
+ else
+ {
+ if (this._nbLoadedResources === filesToDownload.size)
+ {
this.setStatus(ServerManager.Status.READY);
- this.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
+ this.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
}
}
// (*) prepare and start loading sound resources:
- for (const name of soundResources) {
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOADING_RESOURCE, resource: name });
+ for (const name of soundResources)
+ {
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.DOWNLOADING_RESOURCE,
+ resource: name
+ });
const path_data = self._resources.get(name);
const howl = new Howl({
src: path_data.path,
@@ -667,20 +777,26 @@ export class ServerManager extends PsychObject {
autoplay: false
});
- howl.on('load', (event) => {
+ howl.on('load', (event) =>
+ {
++self._nbLoadedResources;
path_data.data = howl;
// self._resources.set(resource.name, howl);
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.RESOURCE_DOWNLOADED, resource: name });
+ self.emit(ServerManager.Event.RESOURCE, {
+ message: ServerManager.Event.RESOURCE_DOWNLOADED,
+ resource: name
+ });
- if (self._nbLoadedResources === filesToDownload.size) {
+ if (self._nbLoadedResources === filesToDownload.size)
+ {
self.setStatus(ServerManager.Status.READY);
- self.emit(ServerManager.Event.RESOURCE, { message: ServerManager.Event.DOWNLOAD_COMPLETED });
+ self.emit(ServerManager.Event.RESOURCE, {message: ServerManager.Event.DOWNLOAD_COMPLETED});
}
});
- howl.on('loaderror', (id, error) => {
+ howl.on('loaderror', (id, error) =>
+ {
// throw { ...response, error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' };
- throw Object.assign(response, { error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')' });
+ throw Object.assign(response, {error: 'unable to download resource: ' + name + ' (' + util.toString(error) + ')'});
});
howl.load();
diff --git a/js/core/Window.js b/js/core/Window.js
index 265903c..d63b886 100644
--- a/js/core/Window.js
+++ b/js/core/Window.js
@@ -2,15 +2,15 @@
* Window responsible for displaying the experiment stimuli
*
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { Color } from '../util/Color';
-import { PsychObject } from '../util/PsychObject';
-import { MonotonicClock } from '../util/Clock';
-import { Logger } from "./Logger";
+import {Color} from '../util/Color';
+import {PsychObject} from '../util/PsychObject';
+import {MonotonicClock} from '../util/Clock';
+import {Logger} from "./Logger";
/**
*
Window displays the various stimuli of the experiment.
@@ -29,7 +29,8 @@ import { Logger } from "./Logger";
* before flipping
* @param {boolean} [options.autoLog= true] whether or not to log
*/
-export class Window extends PsychObject {
+export class Window extends PsychObject
+{
/**
* Getter for monitorFramePeriod.
@@ -38,17 +39,20 @@ export class Window extends PsychObject {
* @function
* @public
*/
- get monitorFramePeriod() { return this._monitorFramePeriod; }
+ get monitorFramePeriod()
+ {
+ return this._monitorFramePeriod;
+ }
constructor({
- psychoJS,
- name,
- fullscr = false,
- color = new Color('black'),
- units = 'pix',
- waitBlanking = false,
- autoLog = true
- } = {})
+ psychoJS,
+ name,
+ fullscr = false,
+ color = new Color('black'),
+ units = 'pix',
+ waitBlanking = false,
+ autoLog = true
+ } = {})
{
super(psychoJS, name);
@@ -76,7 +80,8 @@ export class Window extends PsychObject {
// fullscreen listener:
this._windowAlreadyInFullScreen = false;
const self = this;
- document.addEventListener('fullscreenchange', (event) => {
+ document.addEventListener('fullscreenchange', (event) =>
+ {
self._windowAlreadyInFullScreen = !!document.fullscreenElement;
console.log('windowAlreadyInFullScreen:', self._windowAlreadyInFullScreen);
@@ -84,12 +89,16 @@ export class Window extends PsychObject {
// the Window and all of the stimuli need to be updated:
self._needUpdate = true;
for (const stimulus of self._drawList)
+ {
stimulus._needUpdate = true;
+ }
});
if (this._autoLog)
+ {
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
+ }
}
@@ -105,10 +114,14 @@ export class Window extends PsychObject {
close()
{
if (!this._renderer)
+ {
return;
+ }
if (document.body.contains(this._renderer.view))
+ {
document.body.removeChild(this._renderer.view);
+ }
// destroy the renderer and the WebGL context:
if (typeof this._renderer.gl !== 'undefined')
@@ -173,16 +186,21 @@ export class Window extends PsychObject {
});
}
else if (typeof document.documentElement.mozRequestFullScreen === 'function')
+ {
document.documentElement.mozRequestFullScreen();
-
+ }
else if (typeof document.documentElement.webkitRequestFullscreen === 'function')
+ {
document.documentElement.webkitRequestFullscreen();
-
+ }
else if (typeof document.documentElement.msRequestFullscreen === 'function')
+ {
document.documentElement.msRequestFullscreen();
-
+ }
else
+ {
this.psychoJS.logger.warn('Unable to go fullscreen.');
+ }
}
}
@@ -210,16 +228,21 @@ export class Window extends PsychObject {
});
}
else if (typeof document.mozCancelFullScreen === 'function')
+ {
document.mozCancelFullScreen();
-
+ }
else if (typeof document.webkitExitFullscreen === 'function')
+ {
document.webkitExitFullscreen();
-
+ }
else if (typeof document.msExitFullscreen === 'function')
+ {
document.msExitFullscreen();
-
+ }
else
+ {
this.psychoJS.logger.warn('Unable to close fullscreen.');
+ }
}
}
@@ -241,9 +264,10 @@ export class Window extends PsychObject {
logOnFlip({
msg,
level = Logger.ServerLevel.EXP,
- obj} = {})
+ obj
+ } = {})
{
- this._msgToBeLogged.push({ msg, level, obj });
+ this._msgToBeLogged.push({msg, level, obj});
}
@@ -281,7 +305,9 @@ export class Window extends PsychObject {
render()
{
if (!this._renderer)
+ {
return;
+ }
this._frameCount++;
@@ -297,12 +323,16 @@ export class Window extends PsychObject {
// blocks execution until the rendering is fully done:
if (this._waitBlanking)
+ {
this._renderer.gl.finish();
+ }
}
// call the callOnFlip functions and remove them:
for (let callback of this._flipCallbacks)
+ {
callback['function'](...callback['arguments']);
+ }
this._flipCallbacks = [];
// log:
@@ -325,7 +355,9 @@ export class Window extends PsychObject {
if (this._needUpdate)
{
if (this._renderer)
+ {
this._renderer.backgroundColor = this._color.int;
+ }
// we also change the background color of the body since the dialog popup may be longer than the window's height:
document.body.style.backgroundColor = this._color.hex;
@@ -348,11 +380,14 @@ export class Window extends PsychObject {
// if a stimuli needs to be updated, we remove it from the window container, update it, then put it back
for (const stimulus of this._drawList)
- if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined') {
+ {
+ if (stimulus._needUpdate && typeof stimulus._pixi !== 'undefined')
+ {
this._rootContainer.removeChild(stimulus._pixi);
stimulus._updateIfNeeded();
this._rootContainer.addChild(stimulus._pixi);
}
+ }
}
@@ -368,7 +403,9 @@ export class Window extends PsychObject {
this._needUpdate = true;
for (const stimulus of this._drawList)
+ {
stimulus.refresh();
+ }
this._refresh();
}
@@ -413,7 +450,8 @@ export class Window extends PsychObject {
this.psychoJS.eventManager.addMouseListeners(this._renderer);
// update the renderer size and the Window's stimuli whenever the browser's size or orientation change:
- this._resizeCallback = (e) => {
+ this._resizeCallback = (e) =>
+ {
Window._resizePixiRenderer(this, e);
this._fullRefresh();
};
diff --git a/js/core/WindowMixin.js b/js/core/WindowMixin.js
index fea7bb6..44c3c2e 100644
--- a/js/core/WindowMixin.js
+++ b/js/core/WindowMixin.js
@@ -1,8 +1,8 @@
/**
* Mixin implementing various unit-handling measurement methods.
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
@@ -10,150 +10,185 @@
/**
*
This mixin implements various unit-handling measurement methods.
- *
+ *
*
Note: (a) this is the equivalent of PsychoPY's WindowMixin.
* (b) it will most probably be made obsolete by a fully-integrated unit approach.
*
- *
+ *
* @name module:core.WindowMixin
* @mixin
- *
+ *
*/
-export let WindowMixin = (superclass) => class extends superclass {
- constructor(args) {
+export let WindowMixin = (superclass) => class extends superclass
+{
+ constructor(args)
+ {
super(args);
}
/**
* Setter for units attribute.
- *
+ *
* @name module:core.WindowMixin#setUnits
* @function
* @public
* @param {String} [units= this.win.units] - the units
* @param {boolean} [log= false] - whether or not to log
*/
- setUnits(units = this.win.units, log = false) {
+ setUnits(units = this.win.units, log = false)
+ {
this._setAttribute('units', units, log);
}
/**
* Convert the given length from stimulus unit to pixel units.
- *
+ *
* @name module:core.WindowMixin#_getLengthPix
* @function
* @protected
* @param {number} length - the length in stimulus units
* @return {number} - the length in pixel units
*/
- _getLengthPix(length) {
- let response = { origin: 'WindowMixin._getLengthPix', context: 'when converting a length from stimulus unit to pixel units' };
+ _getLengthPix(length)
+ {
+ let response = {
+ origin: 'WindowMixin._getLengthPix',
+ context: 'when converting a length from stimulus unit to pixel units'
+ };
- if (this._units === 'pix') {
+ if (this._units === 'pix')
+ {
return length;
}
- else if (typeof this._units === 'undefined' || this._units === 'norm') {
+ else if (typeof this._units === 'undefined' || this._units === 'norm')
+ {
var winSize = this.win.size;
return length * winSize[1] / 2; // TODO: how do we handle norm when width != height?
}
- else if (this._units === 'height') {
+ else if (this._units === 'height')
+ {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
- else {
+ else
+ {
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
+ throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
}
}
/**
* Convert the given length from pixel units to the stimulus units
- *
+ *
* @name module:core.WindowMixin#_getLengthUnits
* @function
* @protected
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getLengthUnits(length_px) {
- let response = { origin: 'WindowMixin._getLengthUnits', context: 'when converting a length from pixel unit to stimulus units' };
+ _getLengthUnits(length_px)
+ {
+ let response = {
+ origin: 'WindowMixin._getLengthUnits',
+ context: 'when converting a length from pixel unit to stimulus units'
+ };
- if (this._units === 'pix') {
+ if (this._units === 'pix')
+ {
return length_px;
}
- else if (typeof this._units === 'undefined' || this._units === 'norm') {
+ else if (typeof this._units === 'undefined' || this._units === 'norm')
+ {
const winSize = this.win.size;
return length_px / (winSize[1] / 2); // TODO: how do we handle norm when width != height?
}
- else if (this._units === 'height') {
+ else if (this._units === 'height')
+ {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length_px / minSize;
}
- else {
+ else
+ {
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
+ throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
}
}
/**
* Convert the given length from pixel units to the stimulus units
- *
+ *
* @name module:core.WindowMixin#_getHorLengthPix
* @function
* @protected
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getHorLengthPix(length) {
- let response = { origin: 'WindowMixin._getHorLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
+ _getHorLengthPix(length)
+ {
+ let response = {
+ origin: 'WindowMixin._getHorLengthPix',
+ context: 'when converting a length from pixel unit to stimulus units'
+ };
- if (this._units === 'pix') {
+ if (this._units === 'pix')
+ {
return length;
}
- else if (typeof this._units === 'undefined' || this._units === 'norm') {
+ else if (typeof this._units === 'undefined' || this._units === 'norm')
+ {
var winSize = this.win.size;
return length * winSize[0] / 2;
}
- else if (this._units === 'height') {
+ else if (this._units === 'height')
+ {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
- else {
+ else
+ {
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
+ throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
}
}
/**
* Convert the given length from pixel units to the stimulus units
- *
+ *
* @name module:core.WindowMixin#_getVerLengthPix
* @function
* @protected
* @param {number} length_px - the length in pixel units
* @return {number} - the length in stimulus units
*/
- _getVerLengthPix(length) {
- let response = { origin: 'WindowMixin._getVerLengthPix', context: 'when converting a length from pixel unit to stimulus units' };
+ _getVerLengthPix(length)
+ {
+ let response = {
+ origin: 'WindowMixin._getVerLengthPix',
+ context: 'when converting a length from pixel unit to stimulus units'
+ };
- if (this._units === 'pix') {
+ if (this._units === 'pix')
+ {
return length;
}
- else if (typeof this._units === 'undefined' || this._units === 'norm') {
+ else if (typeof this._units === 'undefined' || this._units === 'norm')
+ {
var winSize = this.win.size;
return length * winSize[1] / 2;
}
- else if (this._units === 'height') {
+ else if (this._units === 'height')
+ {
const minSize = Math.min(this.win.size[0], this.win.size[1]);
return length * minSize;
}
- else {
+ else
+ {
// throw { ...response, error: 'unable to deal with unit: ' + this._units };
- throw Object.assign(response, { error: 'unable to deal with unit: ' + this._units });
+ throw Object.assign(response, {error: 'unable to deal with unit: ' + this._units});
}
}
diff --git a/js/data/ExperimentHandler.js b/js/data/ExperimentHandler.js
index fe50bde..9d9a064 100644
--- a/js/data/ExperimentHandler.js
+++ b/js/data/ExperimentHandler.js
@@ -1,15 +1,15 @@
/**
* Experiment Handler
- *
+ *
* @author Alain Pitiot
- * @version 2020.1
+ * @version 2020.5
* @copyright (c) 2020 Ilixa Ltd. ({@link http://ilixa.com})
* @license Distributed under the terms of the MIT License
*/
-import { PsychObject } from '../util/PsychObject';
-import { MonotonicClock } from '../util/Clock';
+import {PsychObject} from '../util/PsychObject';
+import {MonotonicClock} from '../util/Clock';
import * as util from '../util/Util';
@@ -17,48 +17,63 @@ import * as util from '../util/Util';
*
An ExperimentHandler keeps track of multiple loops and handlers. It is particularly useful
* for generating a single data file from an experiment with many different loops (e.g. interleaved
* staircases or loops within loops.
- *
+ *
* @name module:data.ExperimentHandler
- * @class
+ * @class
* @extends PsychObject
* @param {Object} options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {string} options.name - name of the experiment
* @param {Object} options.extraInfo - additional information, such as session name, participant name, etc.
*/
-export class ExperimentHandler extends PsychObject {
+export class ExperimentHandler extends PsychObject
+{
/**
* Getter for experimentEnded.
- *
+ *
* @name module:core.Window#experimentEnded
* @function
* @public
*/
- get experimentEnded() { return this._experimentEnded; }
+ get experimentEnded()
+ {
+ return this._experimentEnded;
+ }
/**
* Setter for experimentEnded.
- *
+ *
* @name module:core.Window#experimentEnded
* @function
* @public
*/
- set experimentEnded(ended) { this._experimentEnded = ended; }
+ set experimentEnded(ended)
+ {
+ this._experimentEnded = ended;
+ }
/**
* Legacy experiment getters.
*/
- get _thisEntry() { return this._currentTrialData; }
- get _entries() { return this._trialsData; }
+ get _thisEntry()
+ {
+ return this._currentTrialData;
+ }
+
+ get _entries()
+ {
+ return this._trialsData;
+ }
constructor({
- psychoJS,
- name,
- extraInfo
- } = {}) {
+ psychoJS,
+ name,
+ extraInfo
+ } = {})
+ {
super(psychoJS, name);
this._addAttributes(ExperimentHandler, extraInfo);
@@ -85,10 +100,13 @@ export class ExperimentHandler extends PsychObject {
* @public
* @returns {boolean} whether or not the current entry is empty
*/
- isEntryEmpty() {
+ isEntryEmpty()
+ {
return (Object.keys(this._currentTrialData).length > 0);
}
- isEntryEmtpy() {
+
+ isEntryEmtpy()
+ {
return (Object.keys(this._currentTrialData).length > 0);
}
@@ -104,7 +122,8 @@ export class ExperimentHandler extends PsychObject {
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
- addLoop(loop) {
+ addLoop(loop)
+ {
this._loops.push(loop);
this._unfinishedLoops.push(loop);
loop.experimentHandler = this;
@@ -119,10 +138,13 @@ export class ExperimentHandler extends PsychObject {
* @public
* @param {Object} loop - the loop, e.g. an instance of TrialHandler or StairHandler
*/
- removeLoop(loop) {
+ removeLoop(loop)
+ {
const index = this._unfinishedLoops.indexOf(loop);
if (index !== -1)
+ {
this._unfinishedLoops.splice(index, 1);
+ }
}
@@ -138,14 +160,18 @@ export class ExperimentHandler extends PsychObject {
* @param {Object} key - the key
* @param {Object} value - the value
*/
- addData(key, value) {
- if (this._trialsKeys.indexOf(key) === -1) {
+ addData(key, value)
+ {
+ if (this._trialsKeys.indexOf(key) === -1)
+ {
this._trialsKeys.push(key);
}
// turn arrays into their json equivalent:
if (Array.isArray(value))
+ {
value = JSON.stringify(value);
+ }
this._currentTrialData[key] = value;
}
@@ -166,13 +192,20 @@ export class ExperimentHandler extends PsychObject {
{
// turn single snapshot into a one-element array:
if (!Array.isArray(snapshots))
+ {
snapshots = [snapshots];
+ }
- for (const snapshot of snapshots) {
+ for (const snapshot of snapshots)
+ {
const attributes = ExperimentHandler._getLoopAttributes(snapshot);
for (let a in attributes)
+ {
if (attributes.hasOwnProperty(a))
+ {
this._currentTrialData[a] = attributes[a];
+ }
+ }
}
}
@@ -184,15 +217,23 @@ export class ExperimentHandler extends PsychObject {
{
const attributes = ExperimentHandler._getLoopAttributes(loop);
for (const a in attributes)
+ {
if (attributes.hasOwnProperty(a))
+ {
this._currentTrialData[a] = attributes[a];
+ }
+ }
}
}
// add the extraInfo dict to the data:
for (let a in this.extraInfo)
+ {
if (this.extraInfo.hasOwnProperty(a))
+ {
this._currentTrialData[a] = this.extraInfo[a];
+ }
+ }
this._trialsData.push(this._currentTrialData);
@@ -214,26 +255,38 @@ export class ExperimentHandler extends PsychObject {
* @public
* @param {Object} options
* @param {Array.