mirror of
https://github.com/psychopy/psychojs.git
synced 2025-05-10 18:50:54 +00:00
Merge pull request #558 from apitiot/2023.1.1
ENH: improved super-flow survey; ENH: release of resources
This commit is contained in:
commit
e92bb527e8
@ -789,7 +789,7 @@ export class PsychoJS
|
|||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
window.onerror = function(message, source, lineno, colno, error)
|
window.onerror = function(message, source, lineno, colno, error)
|
||||||
{console.log('@@@', message)
|
{
|
||||||
// check for ResizeObserver loop limit exceeded error:
|
// check for ResizeObserver loop limit exceeded error:
|
||||||
// ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
// ref: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
||||||
if (message === "ResizeObserver loop limit exceeded" ||
|
if (message === "ResizeObserver loop limit exceeded" ||
|
||||||
|
@ -314,6 +314,34 @@ export class ServerManager extends PsychObject
|
|||||||
return pathStatusData.data;
|
return pathStatusData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a resource.
|
||||||
|
*
|
||||||
|
* @param {string} name - the name of the resource to release
|
||||||
|
* @return {boolean} true if a resource with the given name was previously registered with the manager,
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
releaseResource(name)
|
||||||
|
{
|
||||||
|
const response = {
|
||||||
|
origin: "ServerManager.releaseResource",
|
||||||
|
context: "when releasing resource: " + name,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pathStatusData = this._resources.get(name);
|
||||||
|
|
||||||
|
if (typeof pathStatusData === "undefined")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check the current status: prevent the release of a resources currently downloading
|
||||||
|
|
||||||
|
this._psychoJS.logger.debug(`releasing resource: ${name}`);
|
||||||
|
this._resources.delete(name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the status of a single resource or the reduced status of an array of resources.
|
* Get the status of a single resource or the reduced status of an array of resources.
|
||||||
*
|
*
|
||||||
@ -506,18 +534,18 @@ export class ServerManager extends PsychObject
|
|||||||
// pre-process the resources:
|
// pre-process the resources:
|
||||||
for (let r = 0; r < resources.length; ++r)
|
for (let r = 0; r < resources.length; ++r)
|
||||||
{
|
{
|
||||||
const resource = resources[r];
|
|
||||||
|
|
||||||
// convert those resources that are only a string to an object with name and path:
|
// convert those resources that are only a string to an object with name and path:
|
||||||
if (typeof resource === "string")
|
if (typeof resources[r] === "string")
|
||||||
{
|
{
|
||||||
resources[r] = {
|
resources[r] = {
|
||||||
name: resource,
|
name: resources[r],
|
||||||
path: resource,
|
path: resources[r],
|
||||||
download: true
|
download: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resource = resources[r];
|
||||||
|
|
||||||
// deal with survey models:
|
// deal with survey models:
|
||||||
if ("surveyId" in resource)
|
if ("surveyId" in resource)
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@ body {
|
|||||||
|
|
||||||
/* Initialisation message (which will disappear behind the canvas) */
|
/* Initialisation message (which will disappear behind the canvas) */
|
||||||
#root::after {
|
#root::after {
|
||||||
content: "initialising the experiment...";
|
content: "initialising...";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -90,6 +90,7 @@ export class MonotonicClock
|
|||||||
{
|
{
|
||||||
// yyyy-mm-dd, hh:mm:ss.sss
|
// yyyy-mm-dd, hh:mm:ss.sss
|
||||||
return MonotonicClock.getDate()
|
return MonotonicClock.getDate()
|
||||||
|
.replaceAll("/","-")
|
||||||
// yyyy-mm-dd_hh:mm:ss.sss
|
// yyyy-mm-dd_hh:mm:ss.sss
|
||||||
.replace(", ", "_")
|
.replace(", ", "_")
|
||||||
// yyyy-mm-dd_hh[h]mm:ss.sss
|
// yyyy-mm-dd_hh[h]mm:ss.sss
|
||||||
|
@ -322,24 +322,43 @@ export function IsPointInsidePolygon(point, vertices)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shuffle an array in place using the Fisher-Yastes's modern algorithm
|
* Shuffle an array, or a portion of that array, in place using the Fisher-Yastes's modern algorithm
|
||||||
* <p>See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm</p>
|
* <p>See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm</p>
|
||||||
*
|
*
|
||||||
* @param {Object[]} array - the input 1-D array
|
* @param {Object[]} array - the input 1-D array
|
||||||
* @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random
|
* @param {Function} [randomNumberGenerator= undefined] - A function used to generated random numbers in the interval [0, 1). Defaults to Math.random
|
||||||
|
* @param [startIndex= undefined] - start index in the array
|
||||||
|
* @param [endIndex= undefined] - end index in the array
|
||||||
* @return {Object[]} the shuffled array
|
* @return {Object[]} the shuffled array
|
||||||
*/
|
*/
|
||||||
export function shuffle(array, randomNumberGenerator = undefined)
|
export function shuffle(array, randomNumberGenerator = undefined, startIndex = undefined, endIndex = undefined)
|
||||||
{
|
{
|
||||||
if (randomNumberGenerator === undefined)
|
// if array is not an array, we return it untouched rather than throwing an exception:
|
||||||
|
if (!array || !Array.isArray(array))
|
||||||
|
{
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof startIndex === "undefined")
|
||||||
|
{
|
||||||
|
startIndex = 0;
|
||||||
|
}
|
||||||
|
if (typeof endIndex === "undefined")
|
||||||
|
{
|
||||||
|
endIndex = array.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof randomNumberGenerator === "undefined")
|
||||||
{
|
{
|
||||||
randomNumberGenerator = Math.random;
|
randomNumberGenerator = Math.random;
|
||||||
}
|
}
|
||||||
for (let i = array.length - 1; i > 0; i--)
|
|
||||||
|
for (let i = endIndex; i > startIndex; i--)
|
||||||
{
|
{
|
||||||
const j = Math.floor(randomNumberGenerator() * (i + 1));
|
const j = Math.floor(randomNumberGenerator() * (i + 1));
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,26 +23,7 @@ import MatrixBipolar from "./survey/components/MatrixBipolar.js";
|
|||||||
import DropdownExtensions from "./survey/components/DropdownExtensions.js";
|
import DropdownExtensions from "./survey/components/DropdownExtensions.js";
|
||||||
import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js";
|
import customExpressionFunctionsArray from "./survey/extensions/customExpressionFunctions.js";
|
||||||
|
|
||||||
const CAPTIONS = {
|
|
||||||
NEXT: "Next"
|
|
||||||
};
|
|
||||||
|
|
||||||
const SURVEY_SETTINGS = {
|
|
||||||
minWidth: "100px"
|
|
||||||
};
|
|
||||||
|
|
||||||
const SURVEY_COMPLETION_CODES =
|
|
||||||
{
|
|
||||||
NORMAL: 0,
|
|
||||||
SKIP_TO_END_OF_BLOCK: 1,
|
|
||||||
SKIP_TO_END_OF_SURVEY: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
const NODE_EXIT_CODES =
|
|
||||||
{
|
|
||||||
NORMAL: 0,
|
|
||||||
BREAK_FLOW: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Survey Stimulus.
|
* Survey Stimulus.
|
||||||
@ -63,6 +44,24 @@ export class Survey extends VisualStim
|
|||||||
ENDSURVEY: "END"
|
ENDSURVEY: "END"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static CAPTIONS =
|
||||||
|
{
|
||||||
|
NEXT: "Next"
|
||||||
|
};
|
||||||
|
|
||||||
|
static SURVEY_COMPLETION_CODES =
|
||||||
|
{
|
||||||
|
NORMAL: 0,
|
||||||
|
SKIP_TO_END_OF_BLOCK: 1,
|
||||||
|
SKIP_TO_END_OF_SURVEY: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
static NODE_EXIT_CODES =
|
||||||
|
{
|
||||||
|
NORMAL: 0,
|
||||||
|
BREAK_FLOW: 1
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @memberOf module:visual
|
* @memberOf module:visual
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -83,19 +82,12 @@ export class Survey extends VisualStim
|
|||||||
{
|
{
|
||||||
super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog });
|
super({ name, win, units, ori, depth, pos, size, autoDraw, autoLog });
|
||||||
|
|
||||||
// the default surveyId is an uuid based on the experiment id (or name) and the survey name:
|
|
||||||
// this way, it is always the same within a given experiment
|
|
||||||
this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined");
|
|
||||||
const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ?
|
|
||||||
util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) :
|
|
||||||
util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`);
|
|
||||||
|
|
||||||
// whether the user is done with the survey, independently of whether the survey is completed:
|
// whether the user is done with the survey, independently of whether the survey is completed:
|
||||||
this.isFinished = false;
|
this.isFinished = false;
|
||||||
|
|
||||||
// Accumulated completion flag that is being set after completion of one survey node.
|
// accumulated completion flag updated after each survey node is completed
|
||||||
// This flag allows to track completion progress while moving through the survey flow.
|
// note: this make it possible to track completion as we move through the survey flow.
|
||||||
// Initially set to true and will be flipped if at least one of the survey nodes were not fully completed.
|
// _isCompletedAll will be flipped to false whenever a survey node is not completed
|
||||||
this._isCompletedAll = true;
|
this._isCompletedAll = true;
|
||||||
|
|
||||||
// timestamps associated to each question:
|
// timestamps associated to each question:
|
||||||
@ -103,10 +95,9 @@ export class Survey extends VisualStim
|
|||||||
// timestamps clock:
|
// timestamps clock:
|
||||||
this._questionAnswerTimestampClock = new Clock();
|
this._questionAnswerTimestampClock = new Clock();
|
||||||
|
|
||||||
this._totalSurveyResults = {};
|
this._overallSurveyResults = {};
|
||||||
this._surveyData = undefined;
|
this._surveyData = undefined;
|
||||||
this._surveyModel = undefined;
|
this._surveyModel = undefined;
|
||||||
this._signaturePadRO = undefined;
|
|
||||||
this._expressionsRunner = undefined;
|
this._expressionsRunner = undefined;
|
||||||
this._lastPageSwitchHandledIdx = -1;
|
this._lastPageSwitchHandledIdx = -1;
|
||||||
this._variables = {};
|
this._variables = {};
|
||||||
@ -114,23 +105,36 @@ export class Survey extends VisualStim
|
|||||||
this._surveyRunningPromise = undefined;
|
this._surveyRunningPromise = undefined;
|
||||||
this._surveyRunningPromiseResolve = undefined;
|
this._surveyRunningPromiseResolve = undefined;
|
||||||
this._surveyRunningPromiseReject = undefined;
|
this._surveyRunningPromiseReject = undefined;
|
||||||
|
|
||||||
// callback triggered when the user is done with the survey: nothing to do by default
|
// callback triggered when the user is done with the survey: nothing to do by default
|
||||||
this._onFinishedCallback = () => {};
|
this._onFinishedCallback = () => {};
|
||||||
|
|
||||||
// init SurveyJS
|
// init SurveyJS:
|
||||||
this._initSurveyJS();
|
this._initSurveyJS();
|
||||||
|
|
||||||
|
// default size:
|
||||||
|
if (typeof size === "undefined")
|
||||||
|
{
|
||||||
|
this.size = (this.unit === "norm") ? [2.0, 2.0] : [1.0, 1.0];
|
||||||
|
}
|
||||||
|
|
||||||
this._addAttribute(
|
this._addAttribute(
|
||||||
"model",
|
"model",
|
||||||
model
|
model
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// the default surveyId is an uuid based on the experiment id (or name) and the survey name:
|
||||||
|
// this way, it is always the same within a given experiment
|
||||||
|
this._hasSelfGeneratedSurveyId = (typeof surveyId === "undefined");
|
||||||
|
const defaultSurveyId = (this._psychoJS.getEnvironment() === ExperimentHandler.Environment.SERVER) ?
|
||||||
|
util.makeUuid(`${name}@${this._psychoJS.config.gitlab.projectId}`) :
|
||||||
|
util.makeUuid(`${name}@${this._psychoJS.config.experiment.name}`);
|
||||||
this._addAttribute(
|
this._addAttribute(
|
||||||
"surveyId",
|
"surveyId",
|
||||||
surveyId,
|
surveyId,
|
||||||
defaultSurveyId
|
defaultSurveyId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// estimate the bounding box:
|
// estimate the bounding box:
|
||||||
this._estimateBoundingBox();
|
this._estimateBoundingBox();
|
||||||
|
|
||||||
@ -213,7 +217,7 @@ export class Survey extends VisualStim
|
|||||||
logs: []
|
logs: []
|
||||||
};
|
};
|
||||||
|
|
||||||
this.psychoJS.logger.debug(`converted the old 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)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._surveyData = model;
|
this._surveyData = model;
|
||||||
@ -227,6 +231,24 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setter for the surveyId attribute.
|
||||||
|
*
|
||||||
|
* @param {string} surveyId - the survey Id
|
||||||
|
* @param {boolean} [log= false] - whether to log
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
setSurveyId(surveyId, log = false)
|
||||||
|
{
|
||||||
|
this._setAttribute("surveyId", surveyId, log);
|
||||||
|
|
||||||
|
// only update the model if a genuine surveyId was given as parameter to the Survey:
|
||||||
|
if (!this._hasSelfGeneratedSurveyId)
|
||||||
|
{
|
||||||
|
this.setModel(`${surveyId}.sid`, log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set survey variables.
|
* Set survey variables.
|
||||||
*
|
*
|
||||||
@ -254,7 +276,8 @@ export class Survey extends VisualStim
|
|||||||
{
|
{
|
||||||
if (excludedNames.indexOf(name) === -1)
|
if (excludedNames.indexOf(name) === -1)
|
||||||
{
|
{
|
||||||
this._surveyData.variables[name] = variables[name];
|
this._variables[name] = variables[name];
|
||||||
|
// this._surveyData.variables[name] = variables[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -282,22 +305,6 @@ export class Survey extends VisualStim
|
|||||||
return this._surveyModel.runExpression(expression);
|
return this._surveyModel.runExpression(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Setter for the surveyId attribute.
|
|
||||||
*
|
|
||||||
* @param {string} surveyId - the survey Id
|
|
||||||
* @param {boolean} [log= false] - whether to log
|
|
||||||
* @return {void}
|
|
||||||
*/
|
|
||||||
setSurveyId(surveyId, log = false)
|
|
||||||
{
|
|
||||||
this._setAttribute("surveyId", surveyId, log);
|
|
||||||
if (!this._hasSelfGeneratedSurveyId)
|
|
||||||
{
|
|
||||||
this.setModel(`${surveyId}.sid`, log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a callback that will be triggered when the participant finishes the survey.
|
* Add a callback that will be triggered when the participant finishes the survey.
|
||||||
*
|
*
|
||||||
@ -336,7 +343,7 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
// return this._surveyModel.data;
|
// return this._surveyModel.data;
|
||||||
|
|
||||||
return this._totalSurveyResults;
|
return this._overallSurveyResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -374,7 +381,6 @@ export class Survey extends VisualStim
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// if the response cannot be uploaded, e.g. the experiment is running locally, or
|
// if the response cannot be uploaded, e.g. the experiment is running locally, or
|
||||||
// if it is piloting mode, then we offer the response as a file for download:
|
// if it is piloting mode, then we offer the response as a file for download:
|
||||||
if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
|
if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
|
||||||
@ -420,9 +426,7 @@ export class Survey extends VisualStim
|
|||||||
*/
|
*/
|
||||||
hide()
|
hide()
|
||||||
{
|
{
|
||||||
// if a survey div already does not exist already, create it:
|
const surveyDiv = document.getElementById(this._surveyDivId);
|
||||||
const surveyId = `survey-${this._name}`;
|
|
||||||
const surveyDiv = document.getElementById(surveyId);
|
|
||||||
if (surveyDiv !== null)
|
if (surveyDiv !== null)
|
||||||
{
|
{
|
||||||
document.body.removeChild(surveyDiv);
|
document.body.removeChild(surveyDiv);
|
||||||
@ -468,9 +472,9 @@ export class Survey extends VisualStim
|
|||||||
this._needPixiUpdate = false;
|
this._needPixiUpdate = false;
|
||||||
|
|
||||||
// if a survey div does not exist, create it:
|
// if a survey div does not exist, create it:
|
||||||
if (document.getElementById("_survey") === null)
|
if (document.getElementById(this._surveyDivId) === null)
|
||||||
{
|
{
|
||||||
document.body.insertAdjacentHTML("beforeend", "<div id='_survey' class='survey'></div>")
|
document.body.insertAdjacentHTML("beforeend", `<div id=${this._surveyDivId} class='survey'></div>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the survey flow:
|
// start the survey flow:
|
||||||
@ -513,8 +517,7 @@ export class Survey extends VisualStim
|
|||||||
*/
|
*/
|
||||||
_registerCustomExpressionFunctions (Survey, customFuncs = [])
|
_registerCustomExpressionFunctions (Survey, customFuncs = [])
|
||||||
{
|
{
|
||||||
let i;
|
for (let i = 0; i < customFuncs.length; i++)
|
||||||
for (i = 0; i < customFuncs.length; i++)
|
|
||||||
{
|
{
|
||||||
Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync);
|
Survey.FunctionFactory.Instance.register(customFuncs[i].func.name, customFuncs[i].func, customFuncs[i].isAsync);
|
||||||
}
|
}
|
||||||
@ -579,6 +582,7 @@ export class Survey extends VisualStim
|
|||||||
this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime();
|
this._questionAnswerTimestamps[questionData.name].timestamp = this._questionAnswerTimestampClock.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// This probably needs to be moved to some kind of utils.js.
|
// This probably needs to be moved to some kind of utils.js.
|
||||||
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
|
||||||
_FisherYatesShuffle (targetArray = [])
|
_FisherYatesShuffle (targetArray = [])
|
||||||
@ -613,6 +617,7 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
return inOutArray;
|
return inOutArray;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
_composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings)
|
_composeModelWithRandomizedQuestions (surveyModel, inBlockRandomizationSettings)
|
||||||
{
|
{
|
||||||
@ -621,31 +626,32 @@ export class Survey extends VisualStim
|
|||||||
// Hence creating a fresh survey data object with shuffled question order.
|
// Hence creating a fresh survey data object with shuffled question order.
|
||||||
let questions = [];
|
let questions = [];
|
||||||
let questionsMap = {};
|
let questionsMap = {};
|
||||||
let shuffledQuestions;
|
|
||||||
let newSurveyModel =
|
let newSurveyModel =
|
||||||
{
|
{
|
||||||
pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }]
|
pages:[{ elements: new Array(inBlockRandomizationSettings.questionsPerPage) }]
|
||||||
};
|
};
|
||||||
let i, j, k;
|
for (let i = 0; i < surveyModel.pages.length; i++)
|
||||||
for (i = 0; i < surveyModel.pages.length; i++)
|
|
||||||
{
|
{
|
||||||
for (j = 0; j < surveyModel.pages[i].elements.length; j++)
|
for (let j = 0; j < surveyModel.pages[i].elements.length; j++)
|
||||||
{
|
{
|
||||||
questions.push(surveyModel.pages[i].elements[j]);
|
questions.push(surveyModel.pages[i].elements[j]);
|
||||||
k = questions.length - 1;
|
const k = questions.length - 1;
|
||||||
questionsMap[questions[k].name] = questions[k];
|
questionsMap[questions[k].name] = questions[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inBlockRandomizationSettings.layout.length > 0)
|
if (inBlockRandomizationSettings.layout.length > 0)
|
||||||
{
|
{
|
||||||
j = 0;
|
let j = 0;
|
||||||
k = 0;
|
let k = 0;
|
||||||
let curPage = 0;
|
let curPage = 0;
|
||||||
let curElement = 0;
|
let curElement = 0;
|
||||||
const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0);
|
|
||||||
const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1);
|
const shuffledSet0 = util.shuffle(Array.from(inBlockRandomizationSettings.set0));
|
||||||
for (i = 0; i < inBlockRandomizationSettings.layout.length; i++)
|
const shuffledSet1 = util.shuffle(Array.from(inBlockRandomizationSettings.set1));
|
||||||
|
// const shuffledSet0 = this._FisherYatesShuffle(inBlockRandomizationSettings.set0);
|
||||||
|
// const shuffledSet1 = this._FisherYatesShuffle(inBlockRandomizationSettings.set1);
|
||||||
|
for (let i = 0; i < inBlockRandomizationSettings.layout.length; i++)
|
||||||
{
|
{
|
||||||
// Create new page if questionsPerPage reached.
|
// Create new page if questionsPerPage reached.
|
||||||
if (curElement === inBlockRandomizationSettings.questionsPerPage)
|
if (curElement === inBlockRandomizationSettings.questionsPerPage)
|
||||||
@ -675,12 +681,14 @@ export class Survey extends VisualStim
|
|||||||
else if (inBlockRandomizationSettings.showOnly > 0)
|
else if (inBlockRandomizationSettings.showOnly > 0)
|
||||||
{
|
{
|
||||||
// TODO: Check if there can be questionsPerPage applicable in this case.
|
// TODO: Check if there can be questionsPerPage applicable in this case.
|
||||||
shuffledQuestions = this._FisherYatesShuffle(questions);
|
const shuffledQuestions = util.shuffle(Array.from(questions));
|
||||||
|
// shuffledQuestions = this._FisherYatesShuffle(questions);
|
||||||
newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly);
|
newSurveyModel.pages[0].elements = shuffledQuestions.splice(0, inBlockRandomizationSettings.showOnly);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// TODO: Check if there can be questionsPerPage applicable in this case.
|
// TODO: Check if there can be questionsPerPage applicable in this case.
|
||||||
newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions);
|
newSurveyModel.pages[0].elements = util.shuffle(Array.from(questions));
|
||||||
|
// newSurveyModel.pages[0].elements = this._FisherYatesShuffle(questions);
|
||||||
}
|
}
|
||||||
console.log("model recomposition took", performance.now() - t);
|
console.log("model recomposition took", performance.now() - t);
|
||||||
console.log("recomposed model:", newSurveyModel);
|
console.log("recomposed model:", newSurveyModel);
|
||||||
@ -714,12 +722,14 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
if (inQuestionRandomizationSettings.randomizeAll)
|
if (inQuestionRandomizationSettings.randomizeAll)
|
||||||
{
|
{
|
||||||
questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]);
|
questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName]));
|
||||||
|
// questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]);
|
||||||
// Handle dynamic choices.
|
// Handle dynamic choices.
|
||||||
}
|
}
|
||||||
else if (inQuestionRandomizationSettings.showOnly > 0)
|
else if (inQuestionRandomizationSettings.showOnly > 0)
|
||||||
{
|
{
|
||||||
questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly);
|
questionData[choicesFieldName] = util.shuffle(Array.from(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly));
|
||||||
|
// questionData[choicesFieldName] = this._FisherYatesShuffle(questionData[choicesFieldName]).splice(0, inQuestionRandomizationSettings.showOnly);
|
||||||
}
|
}
|
||||||
else if (inQuestionRandomizationSettings.reverse)
|
else if (inQuestionRandomizationSettings.reverse)
|
||||||
{
|
{
|
||||||
@ -739,8 +749,10 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
// Creating new array of choices to which we're going to write from randomized/reversed sets.
|
// Creating new array of choices to which we're going to write from randomized/reversed sets.
|
||||||
questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length);
|
questionData[choicesFieldName] = new Array(inQuestionRandomizationSettings.layout.length);
|
||||||
const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0);
|
const shuffledSet0 = util.shuffle(Array.from(inQuestionRandomizationSettings.set0));
|
||||||
const shuffledSet1 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set1);
|
const shuffledSet1 = util.shuffle(Array.from(inQuestionRandomizationSettings.set1));
|
||||||
|
// const shuffledSet0 = this._FisherYatesShuffle(inQuestionRandomizationSettings.set0);
|
||||||
|
// 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 (i = 0; i < inQuestionRandomizationSettings.layout.length; i++)
|
||||||
{
|
{
|
||||||
@ -861,12 +873,12 @@ export class Survey extends VisualStim
|
|||||||
if (skipLogic.destination === "ENDOFSURVEY")
|
if (skipLogic.destination === "ENDOFSURVEY")
|
||||||
{
|
{
|
||||||
surveyModel.setCompleted();
|
surveyModel.setCompleted();
|
||||||
this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY);
|
this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY);
|
||||||
}
|
}
|
||||||
else if (skipLogic.destination === "ENDOFBLOCK")
|
else if (skipLogic.destination === "ENDOFBLOCK")
|
||||||
{
|
{
|
||||||
surveyModel.setCompleted();
|
surveyModel.setCompleted();
|
||||||
this._surveyRunningPromiseResolve(SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK);
|
this._surveyRunningPromiseResolve(Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -896,13 +908,12 @@ export class Survey extends VisualStim
|
|||||||
*
|
*
|
||||||
* @param surveyModel
|
* @param surveyModel
|
||||||
* @param options
|
* @param options
|
||||||
* @private
|
* @protected
|
||||||
*/
|
*/
|
||||||
_onSurveyComplete(surveyModel, options)
|
_onSurveyComplete(surveyModel, options)
|
||||||
{
|
{
|
||||||
Object.assign(this._totalSurveyResults, surveyModel.data);
|
Object.assign(this._overallSurveyResults, surveyModel.data);
|
||||||
this._detachResizeObservers();
|
let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL;
|
||||||
let completionCode = SURVEY_COMPLETION_CODES.NORMAL;
|
|
||||||
const questions = surveyModel.getAllQuestions();
|
const questions = surveyModel.getAllQuestions();
|
||||||
|
|
||||||
// It is guaranteed that the question with skip logic is always last on the page.
|
// It is guaranteed that the question with skip logic is always last on the page.
|
||||||
@ -916,12 +927,12 @@ export class Survey extends VisualStim
|
|||||||
{
|
{
|
||||||
if (skipLogic.destination === "ENDOFSURVEY")
|
if (skipLogic.destination === "ENDOFSURVEY")
|
||||||
{
|
{
|
||||||
completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY;
|
completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY;
|
||||||
surveyModel.setCompleted();
|
surveyModel.setCompleted();
|
||||||
}
|
}
|
||||||
else if (skipLogic.destination === "ENDOFBLOCK")
|
else if (skipLogic.destination === "ENDOFBLOCK")
|
||||||
{
|
{
|
||||||
completionCode = SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK;
|
completionCode = Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_BLOCK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -957,6 +968,8 @@ export class Survey extends VisualStim
|
|||||||
this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`);
|
this.psychoJS.logger.warn(`Flag _isCompletedAll is false!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._detachResizeObservers();
|
||||||
|
|
||||||
this._surveyRunningPromiseResolve(completionCode);
|
this._surveyRunningPromiseResolve(completionCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -976,22 +989,18 @@ export class Survey extends VisualStim
|
|||||||
* Run the survey using flow data provided. This method runs recursively.
|
* Run the survey using flow data provided. This method runs recursively.
|
||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
* @param {string} surveyId - the id of the DOM div
|
|
||||||
* @param {Object} surveyData - surveyData / model.
|
* @param {Object} surveyData - surveyData / model.
|
||||||
* @param {Object} prevBlockResults - survey results gathered from running previous block of questions.
|
* @param {Object} surveyFlowBlock - XXX
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
_beginSurvey(surveyData, surveyFlowBlock)
|
_beginSurvey(surveyData, surveyFlowBlock)
|
||||||
{
|
{
|
||||||
let j;
|
|
||||||
let surveyIdx;
|
|
||||||
this._lastPageSwitchHandledIdx = -1;
|
this._lastPageSwitchHandledIdx = -1;
|
||||||
surveyIdx = surveyFlowBlock.surveyIdx;
|
const surveyIdx = surveyFlowBlock.surveyIdx;
|
||||||
console.log("playing survey with idx", surveyIdx);
|
|
||||||
let surveyModelInput = this._processSurveyData(surveyData, surveyIdx);
|
let surveyModelInput = this._processSurveyData(surveyData, surveyIdx);
|
||||||
|
|
||||||
this._surveyModel = new window.Survey.Model(surveyModelInput);
|
this._surveyModel = new window.Survey.Model(surveyModelInput);
|
||||||
for (j in this._variables)
|
for (let j in this._variables)
|
||||||
{
|
{
|
||||||
// Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()).
|
// Adding variables directly to hash to get higher performance (this is instantaneous compared to .setVariable()).
|
||||||
// At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation.
|
// At this stage we don't care to trigger all the callbacks like .setVariable() does, since this is very beginning of survey presentation.
|
||||||
@ -1010,7 +1019,7 @@ export class Survey extends VisualStim
|
|||||||
this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this));
|
this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || CAPTIONS.NEXT) : undefined;
|
const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined;
|
||||||
jQuery(".survey").Survey({
|
jQuery(".survey").Survey({
|
||||||
model: this._surveyModel,
|
model: this._surveyModel,
|
||||||
showItemsInOrder: "column",
|
showItemsInOrder: "column",
|
||||||
@ -1033,15 +1042,11 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {})
|
async _runSurveyFlow(surveyBlock, surveyData, prevBlockResults = {})
|
||||||
{
|
{
|
||||||
// let surveyBlock;
|
let nodeExitCode = Survey.NODE_EXIT_CODES.NORMAL;
|
||||||
let surveyIdx;
|
|
||||||
let surveyCompletionCode;
|
|
||||||
let nodeExitCode = NODE_EXIT_CODES.NORMAL;
|
|
||||||
let i, j;
|
|
||||||
|
|
||||||
if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL)
|
if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL)
|
||||||
{
|
{
|
||||||
const dataset = Object.assign({}, this._totalSurveyResults, this._variables);
|
const dataset = Object.assign({}, this._overallSurveyResults, this._variables);
|
||||||
this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition);
|
this._expressionsRunner.expressionExecutor.setExpression(surveyBlock.condition);
|
||||||
if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined)
|
if (this._expressionsRunner.run(dataset) && surveyBlock.nodes[0] !== undefined)
|
||||||
{
|
{
|
||||||
@ -1054,13 +1059,14 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER)
|
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.RANDOMIZER)
|
||||||
{
|
{
|
||||||
this._InPlaceFisherYatesShuffle(surveyBlock.nodes, 0, surveyBlock.nodes.length - 1);
|
util.shuffle(surveyBlock.nodes, Math.random, 0, surveyBlock.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 (surveyBlock.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[surveyBlock.dataIdx];
|
||||||
for (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.
|
||||||
if (surveyBlockData[j].type === "Custom")
|
if (surveyBlockData[j].type === "Custom")
|
||||||
@ -1089,28 +1095,28 @@ export class Survey extends VisualStim
|
|||||||
this._surveyModel.setCompleted();
|
this._surveyModel.setCompleted();
|
||||||
}
|
}
|
||||||
console.log("EndSurvey block encountered, exiting.");
|
console.log("EndSurvey block encountered, exiting.");
|
||||||
nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW;
|
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
||||||
}
|
}
|
||||||
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
else if (surveyBlock.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
|
||||||
{
|
{
|
||||||
surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock);
|
const surveyCompletionCode = await this._beginSurvey(surveyData, surveyBlock);
|
||||||
Object.assign({}, prevBlockResults, this._surveyModel.data);
|
Object.assign({}, prevBlockResults, this._surveyModel.data);
|
||||||
|
|
||||||
// SkipLogic had destination set to ENDOFSURVEY.
|
// SkipLogic had destination set to ENDOFSURVEY.
|
||||||
if (surveyCompletionCode === SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY)
|
if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY)
|
||||||
{
|
{
|
||||||
nodeExitCode = NODE_EXIT_CODES.BREAK_FLOW;
|
nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeExitCode === NODE_EXIT_CODES.NORMAL &&
|
if (nodeExitCode === Survey.NODE_EXIT_CODES.NORMAL &&
|
||||||
surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL &&
|
surveyBlock.type !== Survey.SURVEY_FLOW_PLAYBACK_TYPES.CONDITIONAL &&
|
||||||
surveyBlock.nodes instanceof Array)
|
surveyBlock.nodes instanceof Array)
|
||||||
{
|
{
|
||||||
for (i = 0; i < surveyBlock.nodes.length; i++)
|
for (let i = 0; i < surveyBlock.nodes.length; i++)
|
||||||
{
|
{
|
||||||
nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults);
|
nodeExitCode = await this._runSurveyFlow(surveyBlock.nodes[i], surveyData, prevBlockResults);
|
||||||
if (nodeExitCode === NODE_EXIT_CODES.BREAK_FLOW)
|
if (nodeExitCode === Survey.NODE_EXIT_CODES.BREAK_FLOW)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1133,14 +1139,11 @@ export class Survey extends VisualStim
|
|||||||
|
|
||||||
_handleSignaturePadResize(entries)
|
_handleSignaturePadResize(entries)
|
||||||
{
|
{
|
||||||
let signatureCanvas;
|
for (let i = 0; i < entries.length; i++)
|
||||||
let q;
|
|
||||||
let i;
|
|
||||||
for (i = 0; i < entries.length; i++)
|
|
||||||
{
|
{
|
||||||
signatureCanvas = entries[i].target.querySelector("canvas");
|
// const signatureCanvas = entries[i].target.querySelector("canvas");
|
||||||
q = this._surveyModel.getQuestionByName(entries[i].target.dataset.name);
|
const question = this._surveyModel.getQuestionByName(entries[i].target.dataset.name);
|
||||||
q.signatureWidth = Math.min(q.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize);
|
question.signatureWidth = Math.min(question.maxSignatureWidth, entries[i].contentBoxSize[0].inlineSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1163,21 +1166,23 @@ export class Survey extends VisualStim
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init the SurveyJS.io library.
|
* Init the SurveyJS.io library and various extensions, setup the theme.
|
||||||
*
|
*
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
_initSurveyJS()
|
_initSurveyJS()
|
||||||
{
|
{
|
||||||
// load the Survey.js libraries, if necessary:
|
// note: the Survey.js libraries must be added to the list of resources in PsychoJS.start:
|
||||||
// TODO
|
// psychoJS.start({ resources: [ {'surveyLibrary': true}, ... ], ...});
|
||||||
|
|
||||||
|
// id of the SurveyJS html div:
|
||||||
|
this._surveyDivId = `survey-${this._name}`;
|
||||||
|
|
||||||
// load the PsychoJS SurveyJS extensions:
|
|
||||||
this._expressionsRunner = new window.Survey.ExpressionRunner();
|
|
||||||
this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray);
|
this._registerCustomExpressionFunctions(window.Survey, customExpressionFunctionsArray);
|
||||||
this._registerWidgets(window.Survey);
|
this._registerWidgets(window.Survey);
|
||||||
this._registerCustomSurveyProperties(window.Survey);
|
this._registerCustomSurveyProperties(window.Survey);
|
||||||
this._addEventListeners();
|
this._addEventListeners();
|
||||||
|
this._expressionsRunner = new window.Survey.ExpressionRunner();
|
||||||
|
|
||||||
// setup the survey theme:
|
// setup the survey theme:
|
||||||
window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px";
|
window.Survey.Serializer.getProperty("expression", "minWidth").defaultValue = "100px";
|
||||||
|
Loading…
Reference in New Issue
Block a user