diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index ba4eca2..95f96d0 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -640,8 +640,8 @@ export class PsychoJS if (showOK) { - let text = "Thank you for your patience. "; - text += (typeof message !== "undefined") ? message : "Goodbye!"; + const defaultMsg = "Thank you for your patience. Goodbye!"; + const text = (typeof message !== "undefined") ? message : defaultMsg; this._gui.dialog({ message: text, onOK: onTerminate diff --git a/src/core/ServerManager.js b/src/core/ServerManager.js index a08e365..27e668b 100644 --- a/src/core/ServerManager.js +++ b/src/core/ServerManager.js @@ -1533,8 +1533,8 @@ export class ServerManager extends PsychObject _setupPreloadQueue() { const response = { - origin: "ServerManager._setupPreloadQueue", - context: "when setting up a preload queue" + origin: "ServerManager.[preload]", + context: "when downloading resources" }; this._preloadQueue = new createjs.LoadQueue(true, "", true); diff --git a/src/data/MultiStairHandler.js b/src/data/MultiStairHandler.js index a2b9b51..8899a8e 100644 --- a/src/data/MultiStairHandler.js +++ b/src/data/MultiStairHandler.js @@ -11,6 +11,7 @@ import {TrialHandler} from "./TrialHandler.js"; import {QuestHandler} from "./QuestHandler.js"; +import {StairHandler} from "./StairHandler.js"; import * as util from "../util/Util.js"; import seedrandom from "seedrandom"; @@ -81,6 +82,8 @@ export class MultiStairHandler extends TrialHandler this._randomNumberGenerator = seedrandom(); } + this._finished = false; + this._prepareStaircases(); this._nextTrial(); } @@ -107,11 +110,10 @@ export class MultiStairHandler extends TrialHandler return this._currentStaircase.getQuestValue(); } - // TODO similar for simple staircase: - // if (this._currentStaircase instanceof StaircaseHandler) - // { - // return this._currentStaircase.getStairValue(); - // } + if (this._currentStaircase instanceof StairHandler) + { + return this._currentStaircase.getStairValue(); + } return undefined; } @@ -125,6 +127,8 @@ export class MultiStairHandler extends TrialHandler */ addResponse(response, value) { + this._psychoJS.logger.debug(`response= ${response}`); + // check that response is either 0 or 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"; } - // 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) { // each condition must be an object: @@ -191,6 +189,7 @@ export class MultiStairHandler extends TrialHandler { throw "QUEST conditions must include a startValSd field"; } + } } catch (error) @@ -234,14 +233,47 @@ export class MultiStairHandler extends TrialHandler 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); } // 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 - continue; + const args = Object.assign({}, condition); + 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); @@ -267,6 +299,8 @@ export class MultiStairHandler extends TrialHandler */ _nextTrial() { + this._psychoJS.logger.debug(`current staircase (before update)= ${this._currentStaircase}`); + try { // 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: this._currentStaircase = this._currentPass.shift(); - // test for termination: if (typeof this._currentStaircase === "undefined") { @@ -325,11 +358,10 @@ export class MultiStairHandler extends TrialHandler { value = this._currentStaircase.getQuestValue(); } - // TODO add a test for simple staircase: - // if (this._currentStaircase instanceof StaircaseHandler) - // { - // value = this._currentStaircase.getStairValue(); - // } + if (this._currentStaircase instanceof StairHandler) + { + value = this._currentStaircase.getStairValue(); + } 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._name+"."+this._varName]: value, - [this._name+".intensity"]: value + [this._name+".intensity"]: value, + [this._varName]: value }; + for (const attribute of this._currentStaircase._userAttributes) { // "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") { let fieldName = /*this._name + "." +*/ this._varName; @@ -383,6 +426,7 @@ export class MultiStairHandler extends TrialHandler } } } + break; } } diff --git a/src/data/StairHandler.js b/src/data/StairHandler.js new file mode 100644 index 0000000..fd14db3 --- /dev/null +++ b/src/data/StairHandler.js @@ -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"; + +/** + *

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}.

+ * + * @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. + * + *

This is the getter associated to getStairValue.

+ * + * @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") +}; diff --git a/src/data/index.js b/src/data/index.js index 1bffe5e..af59512 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -1,5 +1,6 @@ export * from "./ExperimentHandler.js"; export * from "./TrialHandler.js"; export * from "./QuestHandler.js"; +export * from "./StairHandler.js"; export * from "./MultiStairHandler.js"; export * from "./Shelf.js"; diff --git a/src/visual/ButtonStim.js b/src/visual/ButtonStim.js index c5ed8dd..6ffbd44 100644 --- a/src/visual/ButtonStim.js +++ b/src/visual/ButtonStim.js @@ -114,11 +114,6 @@ export class ButtonStim extends TextBox [], ); - this._addAttribute( - "numClicks", - 0, - ); - if (this._autoLog) { 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]); } + + /** + * Clear the previously stored times on and times off. + * + * @returns {void} + */ + reset() + { + this.wasClicked = this.isClicked; + + this.timesOn = []; + this.timesOff = []; + + super.reset(); + } } diff --git a/src/visual/Survey.js b/src/visual/Survey.js index 21eb96a..cf36313 100644 --- a/src/visual/Survey.js +++ b/src/visual/Survey.js @@ -101,7 +101,7 @@ export class Survey extends VisualStim this._overallSurveyResults = {}; this._surveyData = undefined; - this._surveyModel = undefined; + this._surveyJSModel = undefined; this._expressionsRunner = undefined; this._lastPageSwitchHandledIdx = -1; this._variables = {}; @@ -228,6 +228,10 @@ export class Survey extends VisualStim model.surveyFlow.isRootNode = true; this._surveyData = model; + + // augment the question names with block names: + this._augmentQuestionNames(); + this._setAttribute("model", model, log); this._onChange(true, true)(); } @@ -297,7 +301,7 @@ export class Survey extends VisualStim */ evaluateExpression(expression) { - if (typeof expression === "undefined" || typeof this._surveyModel === "undefined") + if (typeof expression === "undefined" || typeof this._surveyJSModel === "undefined") { return undefined; } @@ -309,7 +313,7 @@ export class Survey extends VisualStim 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 - * [Complete] button as been pressed. + * Callback triggered when the participant has completed a SurveyJS Question Block. * - * @param surveyModel - * @param options + * @param {Object} node - super-flow QUESTION_BLOCK node + * @param surveyModel - the associated SurveyJS model + * @param options - the SurveyJS model options * @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); + + let completionCode = Survey.SURVEY_COMPLETION_CODES.NORMAL; const questions = surveyModel.getAllQuestions(); @@ -947,7 +954,7 @@ export class Survey extends VisualStim surveyModel.stopTimer(); // check whether the survey was completed: - const surveyVisibleQuestions = this._surveyModel.getAllQuestions(true); + const surveyVisibleQuestions = this._surveyJSModel.getAllQuestions(true); const nbAnsweredQuestions = surveyVisibleQuestions.reduce( (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 - * @param {Object} surveyData - surveyData / model. - * @param {Object} surveyFlowBlock - XXX - * @return {void} */ - _beginSurvey(surveyData, surveyFlowBlock) + _runQuestionBlock(node, surveyData) { this._lastPageSwitchHandledIdx = -1; - const surveyIdx = surveyFlowBlock.surveyIdx; - let surveyModelInput = this._processSurveyData(surveyData, surveyIdx); + let surveyModelInput = this._processSurveyData(surveyData, node.surveyIdx); - this._surveyModel = new window.Survey.Model(surveyModelInput); + this._surveyJSModel = new window.Survey.Model(surveyModelInput); for (let j in this._variables) { // 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. - this._surveyModel.variablesHash[j] = this._variables[j]; + this._surveyJSModel.variablesHash[j] = this._variables[j]; // this._surveyModel.setVariable(j, this._variables[j]); } - if (!this._surveyModel.isInitialized) + if (!this._surveyJSModel.isInitialized) { - this._registerCustomComponentCallbacks(this._surveyModel); - this._surveyModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); - this._surveyModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); - this._surveyModel.onComplete.add(this._onSurveyComplete.bind(this)); - this._surveyModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); - this._surveyModel.isInitialized = true; - this._surveyModel.onAfterRenderQuestion.add(this._handleAfterQuestionRender.bind(this)); + this._registerCustomComponentCallbacks(this._surveyJSModel); + this._surveyJSModel.onValueChanged.add(this._onQuestionValueChanged.bind(this)); + this._surveyJSModel.onCurrentPageChanging.add(this._onCurrentPageChanging.bind(this)); + this._surveyJSModel.onComplete.add( (surveyJSModel, options) => this._onSurveyJSComplete(node, surveyJSModel, options) ); + this._surveyJSModel.onTextMarkdown.add(this._onTextMarkdown.bind(this)); + this._surveyJSModel.isInitialized = true; + 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({ - model: this._surveyModel, + model: this._surveyJSModel, showItemsInOrder: "column", completeText, - ...surveyData.surveySettings, + ...surveyData.surveySettings }); this._questionAnswerTimestampClock.reset(); // TODO: should this be conditional? - this._surveyModel.startTimer(); + this._surveyJSModel.startTimer(); this._surveyRunningPromise = new Promise((res, rej) => { this._surveyRunningPromiseResolve = res; @@ -1110,18 +1115,19 @@ export class Survey extends VisualStim 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."); nodeExitCode = Survey.NODE_EXIT_CODES.BREAK_FLOW; } + // QUESTION_BLOCK: else if (node.type === Survey.SURVEY_FLOW_PLAYBACK_TYPES.DIRECT) { - const surveyCompletionCode = await this._beginSurvey(surveyData, node); - Object.assign({}, prevBlockResults, this._surveyModel.data); + const surveyCompletionCode = await this._runQuestionBlock(node, surveyData); + Object.assign({}, prevBlockResults, this._surveyJSModel.data); // SkipLogic had destination set to ENDOFSURVEY. if (surveyCompletionCode === Survey.SURVEY_COMPLETION_CODES.SKIP_TO_END_OF_SURVEY) @@ -1161,7 +1167,7 @@ export class Survey extends VisualStim _handleWindowResize(e) { - if (this._surveyModel) + if (this._surveyJSModel) { for (let i = this._signaturePads.length - 1; i >= 0; i--) { @@ -1227,4 +1233,31 @@ export class Survey extends VisualStim // TODO // 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"]) + { + + } + } + } }