1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00

NF: Simple Stair Handler

This commit is contained in:
Alain Pitiot 2024-07-29 11:34:11 +02:00
parent 8a6209b302
commit b366e6a872
7 changed files with 588 additions and 65 deletions

View File

@ -640,8 +640,8 @@ export class PsychoJS
if (showOK) if (showOK)
{ {
let text = "Thank you for your patience. "; const defaultMsg = "Thank you for your patience. Goodbye!";
text += (typeof message !== "undefined") ? message : "Goodbye!"; const text = (typeof message !== "undefined") ? message : defaultMsg;
this._gui.dialog({ this._gui.dialog({
message: text, message: text,
onOK: onTerminate onOK: onTerminate

View File

@ -1533,8 +1533,8 @@ export class ServerManager extends PsychObject
_setupPreloadQueue() _setupPreloadQueue()
{ {
const response = { const response = {
origin: "ServerManager._setupPreloadQueue", origin: "ServerManager.[preload]",
context: "when setting up a preload queue" context: "when downloading resources"
}; };
this._preloadQueue = new createjs.LoadQueue(true, "", true); this._preloadQueue = new createjs.LoadQueue(true, "", true);

View File

@ -11,6 +11,7 @@
import {TrialHandler} from "./TrialHandler.js"; import {TrialHandler} from "./TrialHandler.js";
import {QuestHandler} from "./QuestHandler.js"; import {QuestHandler} from "./QuestHandler.js";
import {StairHandler} from "./StairHandler.js";
import * as util from "../util/Util.js"; import * as util from "../util/Util.js";
import seedrandom from "seedrandom"; import seedrandom from "seedrandom";
@ -81,6 +82,8 @@ export class MultiStairHandler extends TrialHandler
this._randomNumberGenerator = seedrandom(); this._randomNumberGenerator = seedrandom();
} }
this._finished = false;
this._prepareStaircases(); this._prepareStaircases();
this._nextTrial(); this._nextTrial();
} }
@ -107,11 +110,10 @@ export class MultiStairHandler extends TrialHandler
return this._currentStaircase.getQuestValue(); return this._currentStaircase.getQuestValue();
} }
// TODO similar for simple staircase: if (this._currentStaircase instanceof StairHandler)
// if (this._currentStaircase instanceof StaircaseHandler) {
// { return this._currentStaircase.getStairValue();
// return this._currentStaircase.getStairValue(); }
// }
return undefined; return undefined;
} }
@ -125,6 +127,8 @@ export class MultiStairHandler extends TrialHandler
*/ */
addResponse(response, value) addResponse(response, value)
{ {
this._psychoJS.logger.debug(`response= ${response}`);
// check that response is either 0 or 1: // check that response is either 0 or 1:
if (response !== 0 && response !== 1) if (response !== 0 && response !== 1)
{ {
@ -162,12 +166,6 @@ export class MultiStairHandler extends TrialHandler
throw "conditions should be a non empty array of objects"; throw "conditions should be a non empty array of objects";
} }
// TODO this is temporary until we have implemented StairHandler:
if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
{
throw "'simple' staircases are currently not supported";
}
for (const condition of this._conditions) for (const condition of this._conditions)
{ {
// each condition must be an object: // each condition must be an object:
@ -191,6 +189,7 @@ export class MultiStairHandler extends TrialHandler
{ {
throw "QUEST conditions must include a startValSd field"; throw "QUEST conditions must include a startValSd field";
} }
} }
} }
catch (error) catch (error)
@ -234,14 +233,47 @@ export class MultiStairHandler extends TrialHandler
args.nTrials = this._nTrials; args.nTrials = this._nTrials;
} }
// inform the StairHandler that it is instantiated from a MultiStairHandler
// (and so there is no need to update the trial list there since it is updated here)
args.fromMultiStair = true;
// TODO extraArgs
handler = new QuestHandler(args); handler = new QuestHandler(args);
} }
// simple StairCase handler: // simple StairCase handler:
if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE) else if (this._stairType === MultiStairHandler.StaircaseType.SIMPLE)
{ {
// TODO not supported just yet, an exception is raised in _validateConditions const args = Object.assign({}, condition);
continue; args.psychoJS = this._psychoJS;
args.varName = this._varName;
// label becomes name:
args.name = condition.label;
args.autoLog = this._autoLog;
if (typeof condition.nTrials === "undefined")
{
args.nTrials = this._nTrials;
}
// inform the StairHandler that it is instantiated from a MultiStairHandler
// (and so there is no need to update the trial list there since it is updated here)
args.fromMultiStair = true;
// gather all args above and beyond those expected by the StairHandler constructor
// in a separate "extraArgs" argument:
const extraArgs = {};
const stairHandlerConstructorArgs = ["label", "psychoJS", "varName", "startVal", "minVal", "maxVal", "nTrials", "nReversals", "nUp", "nDown", "applyInitialRule", "stepSizes", "stepType", "name", "autolog", "fromMultiStair", "extraArgs"];
for (const key in condition)
{
if (stairHandlerConstructorArgs.indexOf(key) === -1)
{
extraArgs[key] = condition[key];
}
}
args["extraArgs"] = extraArgs;
handler = new StairHandler(args);
} }
this._staircases.push(handler); this._staircases.push(handler);
@ -267,6 +299,8 @@ export class MultiStairHandler extends TrialHandler
*/ */
_nextTrial() _nextTrial()
{ {
this._psychoJS.logger.debug(`current staircase (before update)= ${this._currentStaircase}`);
try try
{ {
// if the current pass is empty, get a new one: // if the current pass is empty, get a new one:
@ -298,7 +332,6 @@ export class MultiStairHandler extends TrialHandler
// pick the next staircase in the pass: // pick the next staircase in the pass:
this._currentStaircase = this._currentPass.shift(); this._currentStaircase = this._currentPass.shift();
// test for termination: // test for termination:
if (typeof this._currentStaircase === "undefined") if (typeof this._currentStaircase === "undefined")
{ {
@ -325,11 +358,10 @@ export class MultiStairHandler extends TrialHandler
{ {
value = this._currentStaircase.getQuestValue(); value = this._currentStaircase.getQuestValue();
} }
// TODO add a test for simple staircase: if (this._currentStaircase instanceof StairHandler)
// if (this._currentStaircase instanceof StaircaseHandler) {
// { value = this._currentStaircase.getStairValue();
// value = this._currentStaircase.getStairValue(); }
// }
this._psychoJS.logger.debug(`selected staircase: ${this._currentStaircase.name}, estimated value for variable ${this._varName}: ${value}`); this._psychoJS.logger.debug(`selected staircase: ${this._currentStaircase.name}, estimated value for variable ${this._varName}: ${value}`);
@ -342,8 +374,10 @@ export class MultiStairHandler extends TrialHandler
{ {
this._trialList[t] = { this._trialList[t] = {
[this._name+"."+this._varName]: value, [this._name+"."+this._varName]: value,
[this._name+".intensity"]: value [this._name+".intensity"]: value,
[this._varName]: value
}; };
for (const attribute of this._currentStaircase._userAttributes) for (const attribute of this._currentStaircase._userAttributes)
{ {
// "name" becomes "label" again: // "name" becomes "label" again:
@ -357,6 +391,15 @@ export class MultiStairHandler extends TrialHandler
} }
} }
console.log("@@@@", this._currentStaircase._extraArgs);
for (const arg in this._currentStaircase._extraArgs)
{
this._trialList[t][this._name+"."+arg] = this._currentStaircase._extraArgs[arg];
this._trialList[t][arg] = this._currentStaircase._extraArgs[arg];
}
this._psychoJS.logger.debug(`updated the trialList at index: ${t} to: ${JSON.stringify(this._trialList[t])}`);
if (typeof this._snapshots[t] !== "undefined") if (typeof this._snapshots[t] !== "undefined")
{ {
let fieldName = /*this._name + "." +*/ this._varName; let fieldName = /*this._name + "." +*/ this._varName;
@ -383,6 +426,7 @@ export class MultiStairHandler extends TrialHandler
} }
} }
} }
break; break;
} }
} }

435
src/data/StairHandler.js Normal file
View File

@ -0,0 +1,435 @@
/**
* Stair Handler
*
* @author Alain Pitiot
* @version 2022.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
import {TrialHandler} from "./TrialHandler.js";
/**
* <p>A Trial Handler that implements the Quest algorithm for quick measurement of
psychophysical thresholds. QuestHandler relies on the [jsQuest]{@link https://github.com/kurokida/jsQUEST} library, a port of Prof Dennis Pelli's QUEST algorithm by [Daiichiro Kuroki]{@link https://github.com/kurokida}.</p>
*
* @extends TrialHandler
*/
export class StairHandler extends TrialHandler
{
/**
* @memberof module:data
* @param {Object} options - the handler options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance
* @param {string} options.varName - the name of the variable / intensity / contrast / threshold manipulated by QUEST
* @param {number} options.startVal - initial guess for the threshold
* @param {number} options.minVal - minimum value for the threshold
* @param {number} options.maxVal - maximum value for the threshold
* @param {number} options.nTrials - maximum number of trials
* @param {string} options.name - name of the handler
* @param {boolean} [options.autoLog= false] - whether or not to log
*/
constructor({
psychoJS,
varName,
startVal,
minVal,
maxVal,
nTrials,
nReversals,
nUp,
nDown,
applyInitialRule,
stepSizes,
stepType,
name,
autoLog,
fromMultiStair,
extraArgs
} = {})
{
super({
psychoJS,
name,
autoLog,
method: TrialHandler.Method.SEQUENTIAL,
trialList: Array(nTrials),
nReps: 1
});
this._addAttribute("varName", varName);
this._addAttribute("startVal", startVal);
this._addAttribute("minVal", minVal, Number.MIN_VALUE);
this._addAttribute("maxVal", maxVal, Number.MAX_VALUE);
this._addAttribute("nTrials", nTrials);
this._addAttribute("nReversals", nReversals, null);
this._addAttribute("nUp", nUp, 1);
this._addAttribute("nDown", nDown, 3);
this._addAttribute("applyInitialRule", applyInitialRule, true);
this._addAttribute("stepType", Symbol.for(stepType), 0.5);
this._addAttribute("stepSizes", stepSizes, [4]);
this._addAttribute("fromMultiStair", fromMultiStair, false);
this._addAttribute("extraArgs", extraArgs);
// turn stepSizes into an array if it is not one already:
if (!Array.isArray(this._stepSizes))
{
this._stepSizes = [this._stepSizes];
}
this._variableStep = (this._stepSizes.length > 1);
this._currentStepSize = this._stepSizes[0];
// TODO update the variables, a la staircase.py :nReversals, stepSizes, etc.
// setup the stair's starting point:
this._stairValue = this._startVal;
this._data = [];
this._values = [];
this._correctCounter = 0;
this._reversalPoints = [];
this.reversalIntensities = [];
this._initialRule = false;
this._currentDirection = StairHandler.Direction.START;
// update the next undefined trial in the trial list, and the associated snapshot:
this._updateTrialList();
}
/**
* Add a response and advance the staircase.
*
* @param{number} response - the response to the trial, must be either 0 (incorrect or
* non-detected) or 1 (correct or detected)
* @param{number | undefined} value - optional intensity / contrast / threshold
* @param{boolean} [doAddData = true] - whether to add the response as data to the
* experiment
*/
addResponse(response, value, doAddData = true)
{
this._psychoJS.logger.debug(`response= ${response}`);
// check that response is either 0 or 1:
if (response !== 0 && response !== 1)
{
throw {
origin: "StairHandler.addResponse",
context: "when adding a trial response",
error: `the response must be either 0 or 1, got: ${JSON.stringify(response)}`
};
}
if (doAddData)
{
this._psychoJS.experiment.addData(this._name + '.response', response);
}
this._data.push(response);
// replace the last value with this one, if need be:
if (typeof value !== "undefined")
{
this._values.pop();
this._values.push(value);
}
// update correctCounter:
if (response === 1)
{
if ( (this._data.length > 1) && (this._data.at(-2) === response))
{
++ this._correctCounter;
}
else
{
// reset the counter:
this._correctCounter = 1;
}
}
// incorrect response:
else
{
if ( (this._data.length > 1) && (this._data.at(-2) === response))
{
-- this._correctCounter;
}
else
{
// reset the counter:
this._correctCounter = -1;
}
}
if (!this._finished)
{
this.next();
// estimate the next value
// (and update the trial list and snapshots):
this._estimateStairValue();
}
}
/**
* Get the current value of the variable / contrast / threshold.
*
* @returns {number} the current value
*/
getStairValue()
{
return this._stairValue;
}
/**
* Get the current value of the variable / contrast / threshold.
*
* <p>This is the getter associated to getStairValue.</p>
*
* @returns {number} the intensity of the current staircase, or undefined if the trial has ended
*/
get intensity()
{
return this.getStairValue();
}
/**
* Estimate the next value, based on the current value, the counter of correct responses,
* and the current staircase direction.
*
* @protected
*/
_estimateStairValue()
{
this._psychoJS.logger.debug(`stairValue before update= ${this._stairValue}, currentDirection= ${this._currentDirection.toString()}, correctCounter= ${this._correctCounter}`);
// default: no reversal, same direction as previous trial
let reverseDirection = false;
// if we are at the very start and the initial rule applies, apply the 1-down, 1-up rule:
if (this.reversalIntensities.length === 0 && this._applyInitialRule)
{
// if the last response was correct:
if (this._data.at(-1) === 1)
{
reverseDirection = (this._currentDirection === StairHandler.Direction.UP);
this._currentDirection = StairHandler.Direction.DOWN;
}
else
{
reverseDirection = (this._currentDirection === StairHandler.Direction.DOWN);
this._currentDirection = StairHandler.Direction.UP;
}
}
// n correct response: time to go down:
else if (this._correctCounter >= this._nDown)
{
reverseDirection = (this._currentDirection === StairHandler.Direction.UP);
this._currentDirection = StairHandler.Direction.DOWN;
}
// n wrong responses, time to go up:
else if (this._correctCounter <= -this._nUp)
{
reverseDirection = (this._currentDirection === StairHandler.Direction.DOWN);
this._currentDirection = StairHandler.Direction.UP;
}
if (reverseDirection)
{
this._reversalPoints.push(this.thisTrialN);
this._initialRule = (this.reversalIntensities.length === 0 && this._applyInitialRule);
this.reversalIntensities.push(this._values.at(-1));
}
// check whether we should finish the trial:
if (this.reversalIntensities.length >= this._nReversals && this._values.length >= this._nTrials)
{
this._finished = true;
// update the snapshots associated with the current trial in the trial list:
for (let t = 0; t < this._snapshots.length - 1; ++t)
{
// the current trial is the last defined one:
if (typeof this._trialList[t + 1] === "undefined")
{
this._snapshots[t].finished = true;
break;
}
}
return;
}
// update the step size, if need be:
if (reverseDirection && this._variableStep)
{
// if we have gone past the end of the step size array, we use the last one:
if (this.reversalIntensities.length >= this._stepSizes.length)
{
this._currentStepSize = this._stepSizes.at(-1);
}
else
{
this._currentStepSize = this._stepSizes.at(this.reversalIntensities.length);
}
}
// apply the new step size:
if ( (this.reversalIntensities.length === 0 || this._initialRule) && this._applyInitialRule )
{
this._initialRule = false;
if (this._data.at(-1) === 1)
{
this._decreaseValue();
}
else
{
this._increaseValue();
}
}
// n correct: decrease the value
else if (this._correctCounter >= this._nDown)
{
this._decreaseValue();
}
// n wrong: increase the value
else if (this._correctCounter <= -this._nUp)
{
this._increaseValue();
}
this._psychoJS.logger.debug(`estimated value for variable ${this._varName}: ${this._stairValue}`);
// update the next undefined trial in the trial list, and the associated snapshot:
this._updateTrialList();
}
/**
* Update the next undefined trial in the trial list, and the associated snapshot.
*
* @protected
*/
_updateTrialList()
{
// if this StairHandler was instantiated from a MultiStairHandler, we do not update the trial list here,
// since it is updated by the MultiStairHandler instead
if (this._fromMultiStair)
{
return;
}
for (let t = 0; t < this._trialList.length; ++t)
{
if (typeof this._trialList[t] === "undefined")
{
this._trialList[t] = { [this._varName]: this._stairValue };
this._psychoJS.logger.debug(`updated the trialList at: ${t}: ${JSON.stringify(this._trialList[t])}`);
if (typeof this._snapshots[t] !== "undefined")
{
this._snapshots[t][this._varName] = this._stairValue;
this._snapshots[t].trialAttributes.push(this._varName);
}
break;
}
}
}
/**
* Increase the current value of the variable / contrast / threshold.
*
* @protected
*/
_increaseValue()
{
this._psychoJS.logger.debug(`stepType= ${this._stepType.toString()}, currentStepSize= ${this._currentStepSize}, stairValue (before update)= ${this._stairValue}`);
this._correctCounter = 0;
switch (this._stepType)
{
case StairHandler.StepType.DB:
this._stairValue *= Math.pow(10.0, this._currentStepSize / 20.0);
break;
case StairHandler.StepType.LOG:
this._stairValue *= Math.pow(10.0, this._currentStepSize);
break;
case StairHandler.StepType.LINEAR:
default:
this._stairValue += this._currentStepSize;
break;
}
// make sure we do not go beyond the maximum value:
if (this._stairValue > this._maxVal)
{
this._stairValue = this._maxVal;
}
}
/**
* Decrease the current value of the variable / contrast / threshold.
*
* @protected
*/
_decreaseValue()
{
this._psychoJS.logger.debug(`stepType= ${this._stepType.toString()}, currentStepSize= ${this._currentStepSize}, stairValue (before update)= ${this._stairValue}`);
this._correctCounter = 0;
switch (this._stepType)
{
case StairHandler.StepType.DB:
this._stairValue /= Math.pow(10.0, this._currentStepSize / 20.0);
break;
case StairHandler.StepType.LOG:
this._stairValue /= Math.pow(10.0, this._currentStepSize);
break;
case StairHandler.StepType.LINEAR:
default:
this._stairValue -= this._currentStepSize;
break;
}
// make sure we do not go beyond the minimum value:
if (this._stairValue < this._minVal)
{
this._stairValue = this._minVal;
}
}
}
/**
* StairHandler step type
*
* @enum {Symbol}
* @readonly
*/
StairHandler.StepType = {
DB: Symbol.for("db"),
LINEAR: Symbol.for("lin"),
LOG: Symbol.for("log")
};
/**
* StairHandler step direction.
*
* @enum {Symbol}
* @readonly
*/
StairHandler.Direction = {
START: Symbol.for("START"),
UP: Symbol.for("UP"),
DOWN: Symbol.for("DOWN")
};

View File

@ -1,5 +1,6 @@
export * from "./ExperimentHandler.js"; export * from "./ExperimentHandler.js";
export * from "./TrialHandler.js"; export * from "./TrialHandler.js";
export * from "./QuestHandler.js"; export * from "./QuestHandler.js";
export * from "./StairHandler.js";
export * from "./MultiStairHandler.js"; export * from "./MultiStairHandler.js";
export * from "./Shelf.js"; export * from "./Shelf.js";

View File

@ -114,11 +114,6 @@ export class ButtonStim extends TextBox
[], [],
); );
this._addAttribute(
"numClicks",
0,
);
if (this._autoLog) if (this._autoLog)
{ {
this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`); this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${util.toString(this)}`);
@ -144,4 +139,19 @@ export class ButtonStim extends TextBox
{ {
return this.listener.isPressedIn(this, [1, 0, 0]); return this.listener.isPressedIn(this, [1, 0, 0]);
} }
/**
* Clear the previously stored times on and times off.
*
* @returns {void}
*/
reset()
{
this.wasClicked = this.isClicked;
this.timesOn = [];
this.timesOff = [];
super.reset();
}
} }

View File

@ -101,7 +101,7 @@ export class Survey extends VisualStim
this._overallSurveyResults = {}; this._overallSurveyResults = {};
this._surveyData = undefined; this._surveyData = undefined;
this._surveyModel = undefined; this._surveyJSModel = undefined;
this._expressionsRunner = undefined; this._expressionsRunner = undefined;
this._lastPageSwitchHandledIdx = -1; this._lastPageSwitchHandledIdx = -1;
this._variables = {}; this._variables = {};
@ -228,6 +228,10 @@ export class Survey extends VisualStim
model.surveyFlow.isRootNode = true; model.surveyFlow.isRootNode = true;
this._surveyData = model; this._surveyData = model;
// augment the question names with block names:
this._augmentQuestionNames();
this._setAttribute("model", model, log); this._setAttribute("model", model, log);
this._onChange(true, true)(); this._onChange(true, true)();
} }
@ -297,7 +301,7 @@ export class Survey extends VisualStim
*/ */
evaluateExpression(expression) evaluateExpression(expression)
{ {
if (typeof expression === "undefined" || typeof this._surveyModel === "undefined") if (typeof expression === "undefined" || typeof this._surveyJSModel === "undefined")
{ {
return undefined; return undefined;
} }
@ -309,7 +313,7 @@ export class Survey extends VisualStim
expression = `'${expression}'`; expression = `'${expression}'`;
} }
return this._surveyModel.runExpression(expression); return this._surveyJSModel.runExpression(expression);
} }
/** /**
@ -910,16 +914,19 @@ export class Survey extends VisualStim
} }
/** /**
* Callback triggered when the participant is done with the survey, i.e. when the * Callback triggered when the participant has completed a SurveyJS Question Block.
* [Complete] button as been pressed.
* *
* @param surveyModel * @param {Object} node - super-flow QUESTION_BLOCK node
* @param options * @param surveyModel - the associated SurveyJS model
* @param options - the SurveyJS model options
* @protected * @protected
*/ */
_onSurveyComplete(surveyModel, options) _onSurveyJSComplete(node, surveyModel, options)
{ {
// note: we need to add the node title to the responses
Object.assign(this._overallSurveyResults, surveyModel.data); Object.assign(this._overallSurveyResults, surveyModel.data);
let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL; let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL;
const questions = surveyModel.getAllQuestions(); const questions = surveyModel.getAllQuestions();
@ -947,7 +954,7 @@ export class Survey extends VisualStim
surveyModel.stopTimer(); surveyModel.stopTimer();
// check whether the survey was completed: // check whether the survey was completed:
const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true); const surveyVisibleQuestions = this._surveyJSModel.getAllQuestions(true);
const nbAnsweredQuestions = surveyVisibleQuestions.reduce( const nbAnsweredQuestions = surveyVisibleQuestions.reduce(
(count, question) => (count, question) =>
{ {
@ -996,51 +1003,49 @@ export class Survey extends VisualStim
} }
/** /**
* Run the survey using flow data provided. This method runs recursively. * Run a QUESTION_BLOCK as a SurveyJS survey.
* *
* @param {Object} node - super-flow QUESTION_BLOCK node
* @param {Object} surveyData - the complete surveyData (model)
* @protected * @protected
* @param {Object} surveyData - surveyData / model.
* @param {Object} surveyFlowBlock - XXX
* @return {void}
*/ */
_beginSurvey(surveyData, surveyFlowBlock) _runQuestionBlock(node, surveyData)
{ {
this._lastPageSwitchHandledIdx = -1; this._lastPageSwitchHandledIdx = -1;
const surveyIdx = surveyFlowBlock.surveyIdx; let surveyModelInput = this._processSurveyData(surveyData, node.surveyIdx);
let surveyModelInput = this._processSurveyData(surveyData, surveyIdx);
this._surveyModel = new window.Survey.Model(surveyModelInput); this._surveyJSModel = new window.Survey.Model(surveyModelInput);
for (let 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.
this._surveyModel.variablesHash[j] = this._variables[j]; this._surveyJSModel.variablesHash[j] = this._variables[j];
// this._surveyModel.setVariable(j, this._variables[j]); // this._surveyModel.setVariable(j, this._variables[j]);
} }
if (!this._surveyModel.isInitialized) if (!this._surveyJSModel.isInitialized)
{ {
this._registerCustomComponentCallbacks(this._surveyModel); this._registerCustomComponentCallbacks(this._surveyJSModel);
this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); this._surveyJSModel.onValueChanged.add(this._onQuestionValueChanged.bind(this));
this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); this._surveyJSModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this));
this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); this._surveyJSModel.onComplete.add( (surveyJSModel, options) => this._onSurveyJSComplete(node, surveyJSModel, options) );
this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); this._surveyJSModel.onTextMarkdown.add(this._onTextMarkdown.bind(this));
this._surveyModel.isInitialized = true; this._surveyJSModel.isInitialized = true;
this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); this._surveyJSModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this));
} }
const completeText = surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined; const completeText = node.surveyIdx < this._surveyData.surveys.length - 1 ? (this._surveyJSModel.pageNextText || Survey.CAPTIONS.NEXT) : undefined;
jQuery(".survey").Survey({ jQuery(".survey").Survey({
model: this._surveyModel, model: this._surveyJSModel,
showItemsInOrder: "column", showItemsInOrder: "column",
completeText, completeText,
...surveyData.surveySettings, ...surveyData.surveySettings
}); });
this._questionAnswerTimestampClock.reset(); this._questionAnswerTimestampClock.reset();
// TODO: should this be conditional? // TODO: should this be conditional?
this._surveyModel.startTimer(); this._surveyJSModel.startTimer();
this._surveyRunningPromise = new Promise((res, rej) => { this._surveyRunningPromise = new Promise((res, rej) => {
this._surveyRunningPromiseResolve = res; this._surveyRunningPromiseResolve = res;
@ -1110,18 +1115,19 @@ export class Survey extends VisualStim
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY) else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.ENDSURVEY)
{ {
if (this._surveyModel) if (this._surveyJSModel)
{ {
this._surveyModel.setCompleted(); this._surveyJSModel.setCompleted();
} }
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;
} }
// QUESTION_BLOCK:
else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT)
{ {
const surveyCompletionCode = await this._beginSurvey(surveyData, node); const surveyCompletionCode = await this._runQuestionBlock(node, surveyData);
Object.assign({}, prevBlockResults, this._surveyModel.data); Object.assign({}, prevBlockResults, this._surveyJSModel.data);
// SkipLogic had destination set to ENDOFSURVEY. // SkipLogic had destination set to ENDOFSURVEY.
if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY)
@ -1161,7 +1167,7 @@ export class Survey extends VisualStim
_handleWindowResize(e) _handleWindowResize(e)
{ {
if (this._surveyModel) if (this._surveyJSModel)
{ {
for (let i = this._signaturePads.length - 1; i >= 0; i--) for (let i = this._signaturePads.length - 1; i >= 0; i--)
{ {
@ -1227,4 +1233,31 @@ export class Survey extends VisualStim
// TODO // TODO
// util.loadCss("./survey/css/grey_style.css"); // util.loadCss("./survey/css/grey_style.css");
} }
/**
* Augment the model question names with model names.
*
* @protected
*/
_augmentQuestionNames()
{
if (!("surveys" in this._surveyData))
{
return;
}
const surveys = this._surveyData["surveys"];
for (const survey of surveys)
{
if (!("title" in survey) || !("pages" in survey))
{
continue;
}
for (const page of survey["pages"])
{
}
}
}
} }