mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 02:30:53 +00:00
various fixes and feature improvements
This commit is contained in:
parent
c3a2b4b9f6
commit
51674ab073
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "psychojs",
|
"name": "psychojs",
|
||||||
"version": "2024.4.1",
|
"version": "2024.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -187,13 +187,26 @@ export class GUI
|
|||||||
{
|
{
|
||||||
atLeastOneIncludedKey = true;
|
atLeastOneIncludedKey = true;
|
||||||
|
|
||||||
markup += `<label for='${keyId}'> ${key} </label>`;
|
// deal with field options:
|
||||||
|
// - if the field is required:
|
||||||
// if the field is required:
|
if (key.slice(-4) === "|req")
|
||||||
|
{
|
||||||
|
key = `${key.slice(0, -4)}*`;
|
||||||
|
}
|
||||||
if (key.slice(-1) === "*")
|
if (key.slice(-1) === "*")
|
||||||
{
|
{
|
||||||
self._requiredKeys.push(keyId);
|
self._requiredKeys.push(keyId);
|
||||||
}
|
}
|
||||||
|
// - all other new options are currently discarded
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// remove the new option extensions:
|
||||||
|
if (key.slice(-4) === "|req" || key.slice(-4) === "|cfg" || key.slice(-4) === "|fix" || key.slice(-4) === "|opt")
|
||||||
|
{
|
||||||
|
key = key.slice(0, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
markup += `<label for='${keyId}'> ${key} </label>`;
|
||||||
|
|
||||||
// if value is an array, we create a select drop-down menu:
|
// if value is an array, we create a select drop-down menu:
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value))
|
||||||
@ -240,7 +253,6 @@ export class GUI
|
|||||||
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
|
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
|
||||||
|
|
||||||
// buttons:
|
// buttons:
|
||||||
markup += "<hr>";
|
|
||||||
markup += "<div class='dialog-button-group'>";
|
markup += "<div class='dialog-button-group'>";
|
||||||
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Cancel Experiment'>Cancel</button>";
|
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Cancel Experiment'>Cancel</button>";
|
||||||
if (self._requireParticipantClick)
|
if (self._requireParticipantClick)
|
||||||
@ -417,13 +429,13 @@ export class GUI
|
|||||||
markup += "</div>";
|
markup += "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showOK || showCancel)
|
// if (showOK || showCancel)
|
||||||
{
|
// {
|
||||||
markup += "<hr>";
|
// markup += "<hr>";
|
||||||
}
|
// }
|
||||||
if (showCancel || showOK)
|
if (showCancel || showOK)
|
||||||
{
|
{
|
||||||
markup += "<div class='button-group'>";
|
markup += "<div class='dialog-button-group'>";
|
||||||
if (showCancel)
|
if (showCancel)
|
||||||
{
|
{
|
||||||
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Close dialog'>Cancel</button>";
|
markup += "<button id='dialogCancel' class='dialog-button' aria-label='Close dialog'>Cancel</button>";
|
||||||
@ -493,11 +505,15 @@ export class GUI
|
|||||||
markup += "<div class='dialog-overlay'></div>";
|
markup += "<div class='dialog-overlay'></div>";
|
||||||
markup += "<div class='dialog-content'>";
|
markup += "<div class='dialog-content'>";
|
||||||
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-warning'><p>Warning</p></div>`;
|
markup += `<div id='experiment-dialog-title' class='dialog-title dialog-warning'><p>Warning</p></div>`;
|
||||||
|
|
||||||
|
markup += "<div class='scrollable-container'>";
|
||||||
markup += `<p>${text}</p>`;
|
markup += `<p>${text}</p>`;
|
||||||
|
markup += "</div>";
|
||||||
|
|
||||||
// progress bar:
|
// progress bar:
|
||||||
markup += `<hr><div id='progressMsg' class='progress-msg'> </div>`;
|
markup += `<hr><div id='progressMsg' class='progress-msg'> </div>`;
|
||||||
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
|
markup += "<div class='progress-container'><div id='progressBar' class='progress-bar'></div></div>";
|
||||||
|
markup += "<div class='dialog-button-group'></div>";
|
||||||
|
|
||||||
markup += "</div></div>";
|
markup += "</div></div>";
|
||||||
|
|
||||||
@ -608,8 +624,11 @@ export class GUI
|
|||||||
// clear all events (and keypresses) accumulated until now:
|
// clear all events (and keypresses) accumulated until now:
|
||||||
this._psychoJS.eventManager.clearEvents();
|
this._psychoJS.eventManager.clearEvents();
|
||||||
|
|
||||||
this._dialog.hide();
|
if (this._dialog)
|
||||||
this._dialog = null;
|
{
|
||||||
|
this._dialog.hide();
|
||||||
|
this._dialog = null;
|
||||||
|
}
|
||||||
this._dialogComponent.status = PsychoJS.Status.FINISHED;
|
this._dialogComponent.status = PsychoJS.Status.FINISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -710,7 +729,6 @@ export class GUI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// if all requirements are fulfilled and the participant is not required to click on the OK button,
|
// if all requirements are fulfilled and the participant is not required to click on the OK button,
|
||||||
// then we close the dialog box and move on with the experiment:
|
// then we close the dialog box and move on with the experiment:
|
||||||
if (allRequirementsFulfilled)
|
if (allRequirementsFulfilled)
|
||||||
|
@ -144,8 +144,8 @@ export class PsychoJS
|
|||||||
});
|
});
|
||||||
|
|
||||||
// add the pavlovia server to the list of hosts:
|
// add the pavlovia server to the list of hosts:
|
||||||
const hostsWithPavlovia = new Set([...hosts, "https://pavlovia.org/run/", "https://run.pavlovia.org/"]);
|
const pavloviaHosts = new Set([...hosts, "https://pavlovia.org/run/", "https://run.pavlovia.org/", "https://devlovia.org/run/", "https://run.devlovia.org/"]);
|
||||||
this._hosts = Array.from(hostsWithPavlovia);
|
this._hosts = Array.from(pavloviaHosts);
|
||||||
|
|
||||||
// GUI:
|
// GUI:
|
||||||
this._gui = new GUI(this);
|
this._gui = new GUI(this);
|
||||||
@ -166,6 +166,9 @@ export class PsychoJS
|
|||||||
this._cancellationUrl = undefined;
|
this._cancellationUrl = undefined;
|
||||||
this._completionUrl = undefined;
|
this._completionUrl = undefined;
|
||||||
|
|
||||||
|
// survey id, if applicable:
|
||||||
|
this._surveyId = undefined;
|
||||||
|
|
||||||
// status:
|
// status:
|
||||||
this.status = PsychoJS.Status.NOT_CONFIGURED;
|
this.status = PsychoJS.Status.NOT_CONFIGURED;
|
||||||
|
|
||||||
@ -377,6 +380,7 @@ export class PsychoJS
|
|||||||
if (typeof surveyId !== "undefined")
|
if (typeof surveyId !== "undefined")
|
||||||
{
|
{
|
||||||
params.surveyId = surveyId;
|
params.surveyId = surveyId;
|
||||||
|
this._surveyId = surveyId;
|
||||||
}
|
}
|
||||||
await this._serverManager.openSession(params);
|
await this._serverManager.openSession(params);
|
||||||
|
|
||||||
@ -411,7 +415,9 @@ export class PsychoJS
|
|||||||
{
|
{
|
||||||
// note: we set lastUploadTimestamp to undefined to prevent uploadData from throttling this call
|
// note: we set lastUploadTimestamp to undefined to prevent uploadData from throttling this call
|
||||||
delete self._config.experiment.resultsUpload.lastUploadTimestamp;
|
delete self._config.experiment.resultsUpload.lastUploadTimestamp;
|
||||||
self._experiment.save({ sync: true });
|
self._experiment.save({
|
||||||
|
sync: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// close the session:
|
// close the session:
|
||||||
@ -634,7 +640,7 @@ export class PsychoJS
|
|||||||
|
|
||||||
if (showOK)
|
if (showOK)
|
||||||
{
|
{
|
||||||
let text = "Thank you for your patience.";
|
let text = "Thank you for your patience. ";
|
||||||
text += (typeof message !== "undefined") ? message : "Goodbye!";
|
text += (typeof message !== "undefined") ? message : "Goodbye!";
|
||||||
this._gui.dialog({
|
this._gui.dialog({
|
||||||
message: text,
|
message: text,
|
||||||
|
@ -134,7 +134,7 @@ export class ServerManager extends PsychObject
|
|||||||
* @property {Object.<string, *>} [error] an error message if we could not open the session
|
* @property {Object.<string, *>} [error] an error message if we could not open the session
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Open a session for this experiment on the remote PsychoJS manager.
|
* Open a session for this experiment on the pavlovia server.
|
||||||
*
|
*
|
||||||
* @param {Object} params - the open session parameters
|
* @param {Object} params - the open session parameters
|
||||||
*
|
*
|
||||||
@ -236,10 +236,10 @@ export class ServerManager extends PsychObject
|
|||||||
* previously been opened)
|
* previously been opened)
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Close the session for this experiment on the remote PsychoJS manager.
|
* Close the session for this experiment on the pavlovia server.
|
||||||
*
|
*
|
||||||
* @param {boolean} [isCompleted= false] - whether or not the experiment was completed
|
* @param {boolean} [isCompleted= false] - whether the experiment was completed
|
||||||
* @param {boolean} [sync= false] - whether or not to communicate with the server in a synchronous manner
|
* @param {boolean} [sync= false] - whether to communicate with the server in a synchronous manner
|
||||||
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
* @returns {Promise<ServerManager.CloseSessionPromise> | void} the response
|
||||||
*/
|
*/
|
||||||
async closeSession(isCompleted = false, sync = false)
|
async closeSession(isCompleted = false, sync = false)
|
||||||
@ -260,6 +260,10 @@ export class ServerManager extends PsychObject
|
|||||||
+ "/sessions/" + this._psychoJS.config.session.token + "/delete";
|
+ "/sessions/" + this._psychoJS.config.session.token + "/delete";
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("isCompleted", isCompleted);
|
formData.append("isCompleted", isCompleted);
|
||||||
|
if (typeof this._psychoJS._surveyId !== "undefined")
|
||||||
|
{
|
||||||
|
formData.append("surveyId", this._psychoJS._surveyId);
|
||||||
|
}
|
||||||
|
|
||||||
navigator.sendBeacon(url, formData);
|
navigator.sendBeacon(url, formData);
|
||||||
this._psychoJS.config.session.status = "CLOSED";
|
this._psychoJS.config.session.status = "CLOSED";
|
||||||
@ -272,10 +276,18 @@ export class ServerManager extends PsychObject
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
const data = {
|
||||||
|
isCompleted
|
||||||
|
};
|
||||||
|
if (typeof this._psychoJS._surveyId !== "undefined")
|
||||||
|
{
|
||||||
|
data["surveyId"] = this._psychoJS._surveyId;
|
||||||
|
}
|
||||||
|
|
||||||
const deleteResponse = await this._queryServerAPI(
|
const deleteResponse = await this._queryServerAPI(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}`,
|
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}`,
|
||||||
{ isCompleted },
|
data,
|
||||||
"FORM"
|
"FORM"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -591,18 +603,24 @@ export class ServerManager extends PsychObject
|
|||||||
{
|
{
|
||||||
// add the SurveyJS and PsychoJS Survey .js and .css resources:
|
// add the SurveyJS and PsychoJS Survey .js and .css resources:
|
||||||
resources[r] = {
|
resources[r] = {
|
||||||
name: "jquery-3.6.0.min.js",
|
name: "jquery-3.5.1.min.js",
|
||||||
path: "./lib/vendors/jquery-3.6.0.min.js",
|
path: "./lib/vendors/jquery-3.5.1.min.js",
|
||||||
|
// name: "jquery-3.6.0.min.js",
|
||||||
|
// path: "./lib/vendors/jquery-3.6.0.min.js",
|
||||||
download: true
|
download: true
|
||||||
};
|
};
|
||||||
resources.push({
|
resources.push({
|
||||||
name: "survey.jquery-1.9.50.min.js",
|
name: "surveyjs.jquery-1.9.126.min.js",
|
||||||
path: "./lib/vendors/survey.jquery-1.9.50.min.js",
|
path: "./lib/vendors/surveyjs.jquery-1.9.126.min.js",
|
||||||
|
// name: "survey.jquery-1.9.50.min.js",
|
||||||
|
// path: "./lib/vendors/survey.jquery-1.9.50.min.js",
|
||||||
download: true
|
download: true
|
||||||
});
|
});
|
||||||
resources.push({
|
resources.push({
|
||||||
name: "survey.defaultV2-1.9.50.min.css",
|
name: "surveyjs.defaultV2-1.9.126-OST.min.css",
|
||||||
path: "./lib/vendors/survey.defaultV2-1.9.50.min.css",
|
path: "./lib/vendors/surveyjs.defaultV2-1.9.126-OST.min.css",
|
||||||
|
// name: "survey.defaultV2-1.9.50.min.css",
|
||||||
|
// path: "./lib/vendors/survey.defaultV2-1.9.50.min.css",
|
||||||
download: true
|
download: true
|
||||||
});
|
});
|
||||||
resources.push({
|
resources.push({
|
||||||
@ -807,9 +825,8 @@ export class ServerManager extends PsychObject
|
|||||||
|
|
||||||
// data upload throttling:
|
// data upload throttling:
|
||||||
const now = MonotonicClock.getReferenceTime();
|
const now = MonotonicClock.getReferenceTime();
|
||||||
if ( (typeof this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp !== "undefined") &&
|
const checkThrottling = (typeof this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp !== "undefined");
|
||||||
(now - this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp < this._uploadThrottlePeriod * 60)
|
if (checkThrottling && (now - this._psychoJS.config.experiment.resultsUpload.lastUploadTimestamp < this._uploadThrottlePeriod * 60))
|
||||||
)
|
|
||||||
{
|
{
|
||||||
return Promise.reject({ ...response, error: "upload canceled by throttling"});
|
return Promise.reject({ ...response, error: "upload canceled by throttling"});
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,9 @@ export class ExperimentHandler extends PsychObject
|
|||||||
this._trialsData = [];
|
this._trialsData = [];
|
||||||
this._currentTrialData = {};
|
this._currentTrialData = {};
|
||||||
|
|
||||||
|
// whether a header for the .csv result file is necessary:
|
||||||
|
this._isCsvHeaderNeeded = true;
|
||||||
|
|
||||||
this._experimentEnded = false;
|
this._experimentEnded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,9 +239,9 @@ export class ExperimentHandler extends PsychObject
|
|||||||
*
|
*
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
* @param {Array.<Object>} [options.attributes] - the attributes to be saved
|
||||||
* @param {boolean} [options.sync=false] - whether or not to communicate with the server in a synchronous manner
|
* @param {boolean} [options.sync=false] - whether to communicate with the server in a synchronous manner
|
||||||
* @param {string} [options.tag=''] - an optional tag to add to the filename to which the data is saved (for CSV and XLSX saving options)
|
* @param {string} [options.tag=''] - an optional tag to add to the filename to which the data is saved (for CSV and XLSX saving options)
|
||||||
* @param {boolean} [options.clear=false] - whether or not to clear all experiment results immediately after they are saved (this is useful when saving data in separate chunks, throughout an experiment)
|
* @param {boolean} [options.clear=false] - whether to clear all experiment results immediately after they are saved (this is useful when saving data in separate chunks, throughout an experiment)
|
||||||
*/
|
*/
|
||||||
async save({
|
async save({
|
||||||
attributes = [],
|
attributes = [],
|
||||||
@ -289,10 +292,24 @@ export class ExperimentHandler extends PsychObject
|
|||||||
{
|
{
|
||||||
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
// note: we use the XLSX library as it automatically deals with header, takes care of quotes,
|
||||||
// newlines, etc.
|
// newlines, etc.
|
||||||
|
|
||||||
|
// we need a header if it is asked for and there is actual data to save:
|
||||||
|
const withHeader = this._isCsvHeaderNeeded && (data.length > 0);
|
||||||
|
|
||||||
|
// if we are outputting a header on this occasion, we won't need one thereafter:
|
||||||
|
if (this._isCsvHeaderNeeded)
|
||||||
|
{
|
||||||
|
this._isCsvHeaderNeeded = !withHeader;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO only save the given attributes
|
// TODO only save the given attributes
|
||||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
const worksheet = XLSX.utils.json_to_sheet(data, {skipHeader: !withHeader});
|
||||||
// prepend BOM
|
// note: start with a BOM if necessary
|
||||||
const csv = "\ufeff" + XLSX.utils.sheet_to_csv(worksheet);
|
let csv = ( (withHeader) ? "\ufeff" : "" ) + XLSX.utils.sheet_to_csv(worksheet);
|
||||||
|
if (data.length > 0)
|
||||||
|
{
|
||||||
|
csv += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
// upload data to the pavlovia server or offer them for download:
|
// upload data to the pavlovia server or offer them for download:
|
||||||
const filenameWithoutPath = this._dataFileName.split(/[\\/]/).pop();
|
const filenameWithoutPath = this._dataFileName.split(/[\\/]/).pop();
|
||||||
|
@ -514,31 +514,105 @@ export class Shelf extends PsychObject
|
|||||||
/**
|
/**
|
||||||
* Get the name of a group, using a counterbalanced design.
|
* Get the name of a group, using a counterbalanced design.
|
||||||
*
|
*
|
||||||
|
* @note the participant token returned by this call is useful when confirming or cancelling that participant's
|
||||||
|
* participation with counterBalanceConfirm/Cancel
|
||||||
|
*
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {string[]} options.key key as an array of key components
|
* @param {string[]} options.key key as an array of key components
|
||||||
* @param {string[]} options.groups the names of the groups
|
* @return {Promise<{string, boolean, string}>} an object with the name of the selected group,
|
||||||
* @param {number[]} options.groupSizes the size of the groups
|
* whether all groups have been depleted, and a participant token
|
||||||
* @return {Promise<{string, boolean}>} an object with the name of the selected group and whether all groups
|
|
||||||
* have been depleted
|
|
||||||
*/
|
*/
|
||||||
async counterBalanceSelect({key, groups, groupSizes} = {})
|
async counterbalanceSelect({
|
||||||
|
key,
|
||||||
|
reserveTimeout
|
||||||
|
} = {})
|
||||||
{
|
{
|
||||||
const response = {
|
const response = {
|
||||||
origin: 'Shelf.counterBalanceSelect',
|
origin: 'Shelf.counterbalanceSelect',
|
||||||
context: `when getting the name of a group, using a counterbalanced design, with key: ${JSON.stringify(key)}`
|
context: `when getting the name of a group, using a counterbalanced design with key: ${JSON.stringify(key)}`
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await this._checkAvailability("counterBalanceSelect");
|
await this._checkAvailability("counterbalanceSelect");
|
||||||
this._checkKey(key);
|
this._checkKey(key);
|
||||||
|
|
||||||
// prepare the request:
|
// prepare the request:
|
||||||
const url = `${this._psychoJS.config.pavlovia.URL}/api/v2/shelf/${this._psychoJS.config.session.token}/counterbalance`;
|
const url = `${this._psychoJS.config.pavlovia.URL}/api/v2/shelf/${this._psychoJS.config.session.token}/counterbalance/select`;
|
||||||
|
const data = {
|
||||||
|
key
|
||||||
|
};
|
||||||
|
if (typeof reserveTimeout !== "undefined")
|
||||||
|
{
|
||||||
|
data.reserveTimeout = reserveTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the server:
|
||||||
|
const putResponse = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
redirect: 'follow',
|
||||||
|
referrerPolicy: 'no-referrer',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
// convert the response to json:
|
||||||
|
const document = await putResponse.json();
|
||||||
|
|
||||||
|
if (putResponse.status !== 200)
|
||||||
|
{
|
||||||
|
throw ('error' in document) ? document.error : document;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the result:
|
||||||
|
this._status = Shelf.Status.READY;
|
||||||
|
return {
|
||||||
|
group: document.group,
|
||||||
|
finished: document.finished,
|
||||||
|
participantToken: document.participantToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
this._status = Shelf.Status.ERROR;
|
||||||
|
throw {...response, error};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm or cancel a participant's participation to a counterbalanced design.
|
||||||
|
*
|
||||||
|
* @note the required participant token is the one returned by a call to counterBalanceSelect
|
||||||
|
*
|
||||||
|
* @param {string[]} key - key as an array of key components
|
||||||
|
* @param {string} participantToken - the participant token
|
||||||
|
* @param {boolean} confirmed - when the participant's participation is confirmed or cancelled
|
||||||
|
* @return {Promise<{string, boolean, string}>} an object with the name of the participant group, and
|
||||||
|
* whether all groups have been depleted
|
||||||
|
*/
|
||||||
|
async counterbalanceConfirm(key, participantToken, confirmed)
|
||||||
|
{
|
||||||
|
const response = {
|
||||||
|
origin: 'Shelf.counterBalanceConfirm',
|
||||||
|
context: `when confirming or cancelling a participant's participation to the counterbalanced design with key: ${JSON.stringify(key)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this._checkAvailability("counterbalanceConfirm");
|
||||||
|
this._checkKey(key);
|
||||||
|
|
||||||
|
// prepare the request:
|
||||||
|
const url = `${this._psychoJS.config.pavlovia.URL}/api/v2/shelf/${this._psychoJS.config.session.token}/counterbalance/confirm`;
|
||||||
const data = {
|
const data = {
|
||||||
key,
|
key,
|
||||||
groups,
|
participantToken,
|
||||||
groupSizes
|
confirmed
|
||||||
};
|
};
|
||||||
|
|
||||||
// query the server:
|
// query the server:
|
||||||
@ -563,7 +637,7 @@ export class Shelf extends PsychObject
|
|||||||
throw ('error' in document) ? document.error : document;
|
throw ('error' in document) ? document.error : document;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the updated value:
|
// return the result:
|
||||||
this._status = Shelf.Status.READY;
|
this._status = Shelf.Status.READY;
|
||||||
return {
|
return {
|
||||||
group: document.group,
|
group: document.group,
|
||||||
@ -577,7 +651,6 @@ export class Shelf extends PsychObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the value associated with the given key.
|
* Update the value associated with the given key.
|
||||||
*
|
*
|
||||||
|
@ -36,16 +36,16 @@ body {
|
|||||||
|
|
||||||
.dialog-container input.text,
|
.dialog-container input.text,
|
||||||
.dialog-container select.text {
|
.dialog-container select.text {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border: 1px solid #767676;
|
border: 1px solid #767676;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-container fieldset {
|
.dialog-container fieldset {
|
||||||
@ -90,7 +90,7 @@ body {
|
|||||||
/*max-height: 90vh;*/
|
/*max-height: 90vh;*/
|
||||||
max-height: 93%;
|
max-height: 93%;
|
||||||
|
|
||||||
padding: 0.5em;
|
padding: 2px; /*0.5em;*/
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: 'Open Sans', sans-serif;
|
||||||
@ -103,22 +103,29 @@ body {
|
|||||||
|
|
||||||
.dialog-content .scrollable-container {
|
.dialog-content .scrollable-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 0.5em;
|
padding: 0.5em; /*0 0.5em;*/
|
||||||
|
|
||||||
|
box-shadow: inset rgba(0, 0, 0, 0.2) 1px 1px 2px, inset rgba(255, 255, 255, 1) -1px -1px 2px;
|
||||||
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-content .scrollable-container p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-content hr {
|
.dialog-content hr {
|
||||||
width: 100%;
|
margin: 0.5em 2px 2px 2px;
|
||||||
|
width: calc(100% - 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-title {
|
.dialog-title {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 2px; /*0.5em;*/
|
||||||
|
|
||||||
background-color: #00dd00;
|
background-color: #008500;
|
||||||
/*background-color: #009900;*/
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +140,8 @@ body {
|
|||||||
.dialog-title p {
|
.dialog-title p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
color: #FFFFFF;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,17 +149,17 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1.1em;
|
line-height: 1em;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.7em;
|
top: 5px;
|
||||||
right: 0.7em;
|
right: 5px;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
width: 1.1em;
|
width: calc(2em - 12px);
|
||||||
height: 1.1em;
|
height: calc(2em - 12px);
|
||||||
|
|
||||||
color: #333333;
|
color: #333333;
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
@ -163,12 +172,14 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-msg {
|
.progress-msg {
|
||||||
|
margin: 0 0.2em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-container {
|
.progress-container {
|
||||||
padding: 0.2em;
|
margin: 0 0.2em;
|
||||||
|
padding: 2px;
|
||||||
border: 1px solid #555555;
|
border: 1px solid #555555;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
@ -208,11 +219,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-button-group {
|
.dialog-button-group {
|
||||||
|
margin: 0.5em 0 0 0;
|
||||||
|
padding: 0.5em 1em calc(0.5em - 2px) 0.2em;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
column-gap: 0.5em;
|
column-gap: 0.5em;
|
||||||
|
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
@ -227,8 +243,16 @@ body {
|
|||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
height: 100%;
|
width: calc(100% - 1em);
|
||||||
width: auto;
|
height: 100%;
|
||||||
|
max-height: 25vh;
|
||||||
|
object-fit: contain;
|
||||||
|
/*width: auto;*/
|
||||||
|
/*margin: 0 auto;*/
|
||||||
|
padding: 0.5em 0.5em 1em 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
/*display: block;
|
/*display: block;
|
||||||
margin: 0 auto 1em;
|
margin: 0 auto 1em;
|
||||||
|
@ -71,6 +71,7 @@ export class ButtonStim extends TextBox
|
|||||||
win,
|
win,
|
||||||
name,
|
name,
|
||||||
text,
|
text,
|
||||||
|
placeholder: text,
|
||||||
font,
|
font,
|
||||||
pos,
|
pos,
|
||||||
size,
|
size,
|
||||||
|
@ -198,8 +198,8 @@ export class Survey extends VisualStim
|
|||||||
model = {
|
model = {
|
||||||
surveys: [model],
|
surveys: [model],
|
||||||
embeddedData: [],
|
embeddedData: [],
|
||||||
surveysMap: {},
|
// surveysMap: {},
|
||||||
questionMapsBySurvey: {},
|
// questionMapsBySurvey: {},
|
||||||
surveyFlow: {
|
surveyFlow: {
|
||||||
name: "root",
|
name: "root",
|
||||||
type: "SEQUENTIAL_GROUP",
|
type: "SEQUENTIAL_GROUP",
|
||||||
@ -211,7 +211,7 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
surveySettings: { showPrevButton: false },
|
surveySettings: { showPrevButton: false },
|
||||||
|
|
||||||
surveyRunLogic: {},
|
// surveyRunLogic: {},
|
||||||
inQuestionRandomization: {},
|
inQuestionRandomization: {},
|
||||||
questionsOrderRandomization: [],
|
questionsOrderRandomization: [],
|
||||||
questionSkipLogic: {},
|
questionSkipLogic: {},
|
||||||
@ -224,6 +224,9 @@ export class Survey extends VisualStim
|
|||||||
this.psychoJS.logger.debug(`converted the legacy model to the new super-flow model: ${JSON.stringify(model)}`);
|
this.psychoJS.logger.debug(`converted the legacy model to the new super-flow model: ${JSON.stringify(model)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mark the root (top-most) node:
|
||||||
|
model.surveyFlow.isRootNode = true;
|
||||||
|
|
||||||
this._surveyData = model;
|
this._surveyData = model;
|
||||||
this._setAttribute("model", model, log);
|
this._setAttribute("model", model, log);
|
||||||
this._onChange(true, true)();
|
this._onChange(true, true)();
|
||||||
@ -478,15 +481,12 @@ export class Survey extends VisualStim
|
|||||||
// if a survey div does not exist, create it:
|
// if a survey div does not exist, create it:
|
||||||
if (document.getElementById(this._surveyDivId) === null)
|
if (document.getElementById(this._surveyDivId) === null)
|
||||||
{
|
{
|
||||||
document.body.insertAdjacentHTML("beforeend", `<div id=${this._surveyDivId} class='survey'></div>`)
|
document.body.insertAdjacentHTML("beforeend", `<div id=${this._surveyDivId} class='survey'></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the survey flow:
|
// start the survey flow:
|
||||||
if (typeof this._surveyData !== "undefined")
|
if (typeof this._surveyData !== "undefined")
|
||||||
{
|
{
|
||||||
// this._startSurvey(surveyId, this._surveyModel);
|
|
||||||
// jQuery(`#${surveyId}`).Survey({model: this._surveyModel});
|
|
||||||
|
|
||||||
this._runSurveyFlow(this._surveyData.surveyFlow, this._surveyData);
|
this._runSurveyFlow(this._surveyData.surveyFlow, this._surveyData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -701,7 +701,6 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
_applyInQuestionRandomization (questionData, inQuestionRandomizationSettings, surveyData)
|
_applyInQuestionRandomization (questionData, inQuestionRandomizationSettings, surveyData)
|
||||||
{
|
{
|
||||||
let t = performance.now();
|
|
||||||
let choicesFieldName;
|
let choicesFieldName;
|
||||||
let valueFieldName;
|
let valueFieldName;
|
||||||
if (questionData.rows !== undefined)
|
if (questionData.rows !== undefined)
|
||||||
@ -721,7 +720,7 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.log("[Survey runner]: Uknown choicesFieldName for", questionData);
|
console.log("[Survey runner]: Unknown choicesFieldName for", questionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inQuestionRandomizationSettings.randomizeAll)
|
if (inQuestionRandomizationSettings.randomizeAll)
|
||||||
@ -745,8 +744,7 @@ export class Survey extends VisualStim
|
|||||||
let choicesMap = {};
|
let choicesMap = {};
|
||||||
// TODO: generalize further i.e. figure out how to calculate the length of array based on availability of sets.
|
// TODO: generalize further i.e. figure out how to calculate the length of array based on availability of sets.
|
||||||
const setIndices = [0, 0, 0];
|
const setIndices = [0, 0, 0];
|
||||||
let i;
|
for (let i = 0; i < questionData[choicesFieldName].length; i++)
|
||||||
for (i = 0; i < questionData[choicesFieldName].length; i++)
|
|
||||||
{
|
{
|
||||||
choicesMap[questionData[choicesFieldName][i][valueFieldName]] = questionData[choicesFieldName][i];
|
choicesMap[questionData[choicesFieldName][i][valueFieldName]] = questionData[choicesFieldName][i];
|
||||||
}
|
}
|
||||||
@ -758,7 +756,7 @@ export class Survey extends VisualStim
|
|||||||
// const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0);
|
// const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0);
|
||||||
// const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1);
|
// const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1);
|
||||||
const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder;
|
const reversedSet = Math.round(Math.random()) === 1 ? inQuestionRandomizationSettings.reverseOrder.reverse() : inQuestionRandomizationSettings.reverseOrder;
|
||||||
for (i = 0; i < inQuestionRandomizationSettings.layout.length; i++)
|
for (let i = 0; i < inQuestionRandomizationSettings.layout.length; i++)
|
||||||
{
|
{
|
||||||
if (inQuestionRandomizationSettings.layout[i] === "set0")
|
if (inQuestionRandomizationSettings.layout[i] === "set0")
|
||||||
{
|
{
|
||||||
@ -797,53 +795,58 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("applying question randomization took", performance.now() - t);
|
|
||||||
// console.log(questionData);
|
// console.log(questionData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc: Go over required surveyModelData and apply randomization settings.
|
* Go over required surveyModelData and apply randomization settings.
|
||||||
|
* @protected
|
||||||
*/
|
*/
|
||||||
_processSurveyData (surveyData, surveyIdx)
|
_processSurveyData(surveyData, surveyIdx)
|
||||||
{
|
{
|
||||||
let t = performance.now();
|
|
||||||
let i, j;
|
|
||||||
let newSurveyModel = undefined;
|
let newSurveyModel = undefined;
|
||||||
if (surveyData.questionsOrderRandomization[surveyIdx] !== undefined)
|
|
||||||
|
// Qualtrics's in-block randomization ignores the presence of page breaks within the block.
|
||||||
|
// Hence creating a fresh survey data object with shuffled question order.
|
||||||
|
if (typeof surveyData.questionsOrderRandomization[surveyIdx] !== "undefined")
|
||||||
{
|
{
|
||||||
// Qualtrics's in-block randomization ignores presense of page breaks within the block.
|
newSurveyModel = this._composeModelWithRandomizedQuestions(
|
||||||
// Hence creating a fresh survey data object with shuffled question order.
|
surveyData.surveys[surveyIdx],
|
||||||
newSurveyModel = this._composeModelWithRandomizedQuestions(surveyData.surveys[surveyIdx], surveyData.questionsOrderRandomization[surveyIdx]);
|
surveyData.questionsOrderRandomization[surveyIdx]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if there's in-question randomization that needs to be applied.
|
// note: we need to check whether the survey model has a "pages" field since empty surveys do not:
|
||||||
for (i = 0; i < surveyData.surveys[surveyIdx].pages.length; i++)
|
if ("pages" in surveyData.surveys[surveyIdx])
|
||||||
{
|
{
|
||||||
for (j = 0; j < surveyData.surveys[surveyIdx].pages[i].elements.length; j++)
|
// checking whether in-question randomization needs to be applied:
|
||||||
|
for (let i = 0; i < surveyData.surveys[surveyIdx].pages.length; ++i)
|
||||||
{
|
{
|
||||||
if (surveyData.inQuestionRandomization[surveyData.surveys[surveyIdx].pages[i].elements[j].name] !== undefined)
|
for (let j = 0; j < surveyData.surveys[surveyIdx].pages[i].elements.length; ++j)
|
||||||
{
|
{
|
||||||
if (newSurveyModel === undefined)
|
if (typeof surveyData.inQuestionRandomization[surveyData.surveys[surveyIdx].pages[i].elements[j].name] !== "undefined")
|
||||||
{
|
{
|
||||||
// Marking a deep copy of survey model input data, to avoid data loss if randomization returns a subset of choices.
|
if (typeof newSurveyModel === "undefined")
|
||||||
// TODO: think of somehting more optimal.
|
{
|
||||||
newSurveyModel = JSON.parse(JSON.stringify(surveyData.surveys[surveyIdx]));
|
// Marking a deep copy of survey model input data, to avoid data loss if randomization returns a subset of choices.
|
||||||
|
// TODO: think of something more optimal.
|
||||||
|
newSurveyModel = JSON.parse(JSON.stringify(surveyData.surveys[surveyIdx]));
|
||||||
|
}
|
||||||
|
this._applyInQuestionRandomization(
|
||||||
|
newSurveyModel.pages[i].elements[j],
|
||||||
|
surveyData.inQuestionRandomization[newSurveyModel.pages[i].elements[j].name],
|
||||||
|
surveyData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this._applyInQuestionRandomization(
|
|
||||||
newSurveyModel.pages[i].elements[j],
|
|
||||||
surveyData.inQuestionRandomization[newSurveyModel.pages[i].elements[j].name],
|
|
||||||
surveyData
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSurveyModel === undefined)
|
if (typeof newSurveyModel === "undefined")
|
||||||
{
|
{
|
||||||
// No changes were made, just return original data.
|
// No changes were made, just return original data.
|
||||||
newSurveyModel = surveyData.surveys[surveyIdx];
|
newSurveyModel = surveyData.surveys[surveyIdx];
|
||||||
}
|
}
|
||||||
console.log("survey model preprocessing took", performance.now() - t);
|
|
||||||
return newSurveyModel;
|
return newSurveyModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -975,7 +978,12 @@ export class Survey extends VisualStim
|
|||||||
this._surveyRunningPromiseResolve(completionCode);
|
this._surveyRunningPromiseResolve(completionCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFlowComplete ()
|
/**
|
||||||
|
* Callback triggered when we have run through the whole flow.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
_onFlowComplete()
|
||||||
{
|
{
|
||||||
this.isFinished = true;
|
this.isFinished = true;
|
||||||
this._onFinishedCallback();
|
this._onFinishedCallback();
|
||||||
@ -1042,32 +1050,41 @@ export class Survey extends VisualStim
|
|||||||
return this._surveyRunningPromise;
|
return this._surveyRunningPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {})
|
/**
|
||||||
|
*
|
||||||
|
* @param node
|
||||||
|
* @param surveyData
|
||||||
|
* @param prevBlockResults
|
||||||
|
* @return {Promise<number>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _runSurveyFlow(node, surveyData, prevBlockResults = {})
|
||||||
{
|
{
|
||||||
let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL;
|
let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL;
|
||||||
|
|
||||||
if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL)
|
if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL)
|
||||||
{
|
{
|
||||||
const dataset = Object.assign({}, this._overallSurveyResults, this._variables);
|
const dataset = Object.assign({}, this._overallSurveyResults, this._variables);
|
||||||
this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition);
|
this._expressionsRunner.expressionExecutor.setExpression(node.condition);
|
||||||
if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined)
|
if (this._expressionsRunner.run(dataset) && node.nodes[0] !== undefined)
|
||||||
{
|
{
|
||||||
nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[0], surveyData, prevBlockResults);
|
nodeExitCode = await this._runSurveyFlow(node.nodes[0], surveyData, prevBlockResults);
|
||||||
}
|
}
|
||||||
else if (surveyBlock.nodes[1] !== undefined)
|
else if (node.nodes[1] !== undefined)
|
||||||
{
|
{
|
||||||
nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[1], surveyData, prevBlockResults);
|
nodeExitCode = await this._runSurveyFlow(node.nodes[1], surveyData, prevBlockResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER)
|
|
||||||
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER)
|
||||||
{
|
{
|
||||||
util.shuffle(surveyBlock.nodes, Math.random, 0, surveyBlock.nodes.length - 1);
|
util.shuffle(node.nodes, Math.random, 0, node.nodes.length - 1);
|
||||||
// this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1);
|
|
||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA)
|
|
||||||
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.EMBEDDED_DATA)
|
||||||
{
|
{
|
||||||
let t = performance.now();
|
let t = performance.now();
|
||||||
const surveyBlockData = surveyData.embeddedData[surveyBlock.dataIdx];
|
const surveyBlockData = surveyData.embeddedData[node.dataIdx];
|
||||||
for (let j = 0; j < surveyBlockData.length; j++)
|
for (let j = 0; j < surveyBlockData.length; j++)
|
||||||
{
|
{
|
||||||
// TODO: handle the rest data types.
|
// TODO: handle the rest data types.
|
||||||
@ -1090,7 +1107,8 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
console.log("embedded data variables accumulation took", performance.now() - t);
|
console.log("embedded data variables accumulation took", performance.now() - t);
|
||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY)
|
|
||||||
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY)
|
||||||
{
|
{
|
||||||
if (this._surveyModel)
|
if (this._surveyModel)
|
||||||
{
|
{
|
||||||
@ -1099,9 +1117,10 @@ export class Survey extends VisualStim
|
|||||||
console.log("EndSurvey block encountered, exiting.");
|
console.log("EndSurvey block encountered, exiting.");
|
||||||
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
|
||||||
|
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
||||||
{
|
{
|
||||||
const surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock);
|
const surveyCompletionCode = await this._beginSurvey(surveyData, node);
|
||||||
Object.assign({}, prevBlockResults, this._surveyModel.data);
|
Object.assign({}, prevBlockResults, this._surveyModel.data);
|
||||||
|
|
||||||
// SkipLogic had destination set to ENDOFSURVEY.
|
// SkipLogic had destination set to ENDOFSURVEY.
|
||||||
@ -1111,13 +1130,14 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run through the children nodes of this node:
|
||||||
if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL &&
|
if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL &&
|
||||||
surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL &&
|
node.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL &&
|
||||||
surveyBlock.nodes instanceof Array)
|
node.nodes instanceof Array)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < surveyBlock.nodes.length; i++)
|
for (const childNode of node.nodes)
|
||||||
{
|
{
|
||||||
nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults);
|
nodeExitCode = await this._runSurveyFlow(childNode, surveyData, prevBlockResults);
|
||||||
if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW)
|
if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@ -1125,9 +1145,9 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (surveyBlock.name === "root")
|
// if we have just run through the top node, mark the whole flow as completed:
|
||||||
|
if (node.isRootNode)
|
||||||
{
|
{
|
||||||
// At this point we went through the entire survey flow tree.
|
|
||||||
this._onFlowComplete();
|
this._onFlowComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -573,21 +573,16 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
{
|
{
|
||||||
this._needPixiUpdate = false;
|
this._needPixiUpdate = false;
|
||||||
|
|
||||||
let enteredText = "";
|
// note: destroying _pixi will get rid of _pixi.text, which will, in turn, remove information about
|
||||||
// at this point this._pixi might exist but is removed from the scene, in such cases this._pixi.text
|
// new lines etc., so we get a copy here, which we will restore on the new _pixi
|
||||||
// does not retain the information about new lines etc. so we go with a local copy of entered text
|
const prevText = (this._pixi !== undefined && this._pixi.parent !== null) ? this._pixi.text : this._text;
|
||||||
if (this._pixi !== undefined && this._pixi.parent !== null) {
|
|
||||||
enteredText = this._pixi.text;
|
|
||||||
} else {
|
|
||||||
enteredText = this._text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this._pixi !== "undefined")
|
if (typeof this._pixi !== "undefined")
|
||||||
{
|
{
|
||||||
this._pixi.destroy(true);
|
this._pixi.destroy(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new TextInput
|
// create a new TextInput
|
||||||
this._pixi = new TextInput(this._getTextInputOptions());
|
this._pixi = new TextInput(this._getTextInputOptions());
|
||||||
|
|
||||||
// listeners required for regular textboxes, but may cause problems with button stimuli
|
// listeners required for regular textboxes, but may cause problems with button stimuli
|
||||||
@ -610,7 +605,7 @@ export class TextBox extends util.mix(VisualStim).with(ColorMixin)
|
|||||||
}
|
}
|
||||||
if (this._editable)
|
if (this._editable)
|
||||||
{
|
{
|
||||||
this.text = enteredText;
|
this.text = prevText;
|
||||||
this._pixi.placeholder = this._placeholder;
|
this._pixi.placeholder = this._placeholder;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
Loading…
Reference in New Issue
Block a user