diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index 67b8dc684..27fe1658a 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -25,18 +25,22 @@ define([ defaults() { return AdaptModel.resultExtend('defaults', { - _isA11yComponentDescriptionEnabled: true + _isA11yComponentDescriptionEnabled: true, + _userAnswer: null, + _attemptStates: null, }); } trackable() { return AdaptModel.resultExtend('trackable', [ - '_userAnswer' + '_userAnswer', + '_attemptStates' ]); } trackableType() { return AdaptModel.resultExtend('trackableType', [ + Array, Array ]); } @@ -45,6 +49,133 @@ define([ return false; } + init() { + if (Adapt.get('_isStarted')) { + this.onAdaptInitialize(); + return; + } + this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize); + } + + onAdaptInitialize() { + this.restoreUserAnswers(); + } + + /** + * Restore the user's answer from the _userAnswer property. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. + */ + restoreUserAnswers() {} + + /** + * Store the user's answer in the _userAnswer property. + * The _userAnswer value must be in the form of arrays containing + * numbers, booleans or arrays only. + */ + storeUserAnswer() {} + + resetUserAnswer() { + this.set('_userAnswer', null); + } + + reset(type, force) { + if (!this.get('_canReset') && !force) return; + this.resetUserAnswer(); + super.reset(type, force); + } + + /** + * Returns the current attempt state raw data or the raw data from the supplied attempt state object. + * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. + * @returns {Array} + */ + getAttemptState(object = this.toJSON()) { + const trackables = this.trackable(); + const types = this.trackableType(); + trackables.find((name, index) => { + // Exclude _attemptStates as it's trackable but isn't needed here + if (name !== '_attemptStates') return; + trackables.splice(index, 1); + types.splice(index, 1); + return true; + }); + const values = trackables.map(n => object[n]); + const booleans = values.filter((v, i) => types[i] === Boolean).map(Boolean); + const numbers = values.filter((v, i) => types[i] === Number).map(v => Number(v) || 0); + const arrays = values.filter((v, i) => types[i] === Array); + return [ + numbers, + booleans, + arrays + ]; + } + + /** + * Returns an attempt object representing the current state or a formatted version of the raw state object supplied. + * @param {Array} [state] JSON object representing the component state, defaults to current state returned from getAttemptState(). + * @returns {Object} + */ + getAttemptObject(state = this.getAttemptState()) { + const trackables = this.trackable(); + const types = this.trackableType(); + trackables.find((name, index) => { + // Exclude _attemptStates as it's trackable but isn't needed here + if (name !== '_attemptStates') return; + trackables.splice(index, 1); + types.splice(index, 1); + return true; + }); + const numbers = (state[0] || []).slice(0); + const booleans = (state[1] || []).slice(0); + const arrays = (state[2] || []).slice(0); + const object = {}; + trackables.forEach((n, i) => { + if (n === '_id') return; + switch (types[i]) { + case Number: + object[n] = numbers.shift(); + break; + case Boolean: + object[n] = booleans.shift(); + break; + case Array: + object[n] = arrays.shift(); + break; + } + }); + return object; + } + + /** + * Sets the current attempt state from the supplied attempt state object. + * @param {Object} object JSON object representing the component state. + * @param {boolean} silent Stops change events from triggering + */ + setAttemptObject(object, silent = true) { + this.set(object, { silent }); + } + + /** + * Adds the current attempt state object or the supplied state object to the attempts store. + * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. + */ + addAttemptObject(object = this.getAttemptObject()) { + const attemptStates = this.get('_attemptStates') || []; + const state = this.getAttemptState(object); + attemptStates.push(state); + this.set('_attemptStates', attemptStates); + } + + /** + * Returns an array of the previous state objects. The most recent state is last in the list. + * @returns {Array} + */ + getAttemptObjects() { + const states = this.get('_attemptStates') || []; + return states.map(state => this.getAttemptObject(state)); + } + } // This abstract model needs to registered to support deprecated view-only components diff --git a/src/core/js/models/itemsComponentModel.js b/src/core/js/models/itemsComponentModel.js index 6ed0c284f..c365bf7be 100644 --- a/src/core/js/models/itemsComponentModel.js +++ b/src/core/js/models/itemsComponentModel.js @@ -18,6 +18,18 @@ define([ 'all': this.onAll, 'change:_isVisited': this.checkCompletionStatus }); + super.init(); + } + + restoreUserAnswers() { + const booleanArray = this.get('_userAnswer'); + if (!booleanArray) return; + this.getChildren().forEach((child, index) => child.set('_isVisited', booleanArray[index])); + } + + storeUserAnswer() { + const booleanArray = this.getChildren().map(child => child.get('_isVisited')); + this.set('_userAnswer', booleanArray); } /** @@ -56,6 +68,7 @@ define([ } checkCompletionStatus() { + this.storeUserAnswer(); if (!this.areAllItemsCompleted()) return; this.setCompletionStatus(); } diff --git a/src/core/js/models/questionModel.js b/src/core/js/models/questionModel.js index 5d6d62b53..c33641b80 100644 --- a/src/core/js/models/questionModel.js +++ b/src/core/js/models/questionModel.js @@ -32,8 +32,7 @@ define([ '_isSubmitted', '_score', '_isCorrect', - '_attemptsLeft', - '_attemptStates' + '_attemptsLeft' ]); } @@ -42,8 +41,7 @@ define([ Boolean, Number, Boolean, - Number, - Array + Number ]); } @@ -58,11 +56,7 @@ define([ init() { this.setupDefaultSettings(); this.setLocking('_canSubmit', true); - if (Adapt.get('_isStarted')) { - this.onAdaptInitialize(); - return; - } - this.listenToOnce(Adapt, 'adapt:initialize', this.onAdaptInitialize); + super.init(); } // Calls default methods to setup on questions @@ -109,18 +103,6 @@ define([ // Not needed as handled by model defaults, keeping to maintain API } - /// /// - // Selection restoration process - /// / - - // Used to add post-load changes to the model - onAdaptInitialize() { - this.restoreUserAnswers(); - } - - // Used to restore the user answers - restoreUserAnswers() {} - /// /// // Submit process /// / @@ -149,10 +131,6 @@ define([ }); } - // This is important for returning or showing the users answer - // This should preserve the state of the users answers - storeUserAnswer() {} - // Sets _isCorrect:true/false based upon isCorrect method below markQuestion() { @@ -308,9 +286,6 @@ define([ }); } - // Used by the question view to reset the stored user answer - resetUserAnswer() {} - refresh() { this.trigger('question:refresh'); } @@ -358,97 +333,6 @@ define([ return this.get('_isInteractionComplete'); } - /** - * Returns the current attempt state raw data or the raw data from the supplied attempt state object. - * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. - * @returns {Array} - */ - getAttemptState(object = this.toJSON()) { - const trackables = this.trackable(); - const types = this.trackableType(); - trackables.find((name, index) => { - // Exclude _attemptStates as it's trackable but isn't needed here - if (name !== '_attemptStates') return; - trackables.splice(index, 1); - types.splice(index, 1); - return true; - }); - const values = trackables.map(n => object[n]); - const booleans = values.filter((v, i) => types[i] === Boolean).map(Boolean); - const numbers = values.filter((v, i) => types[i] === Number).map(v => Number(v) || 0); - const arrays = values.filter((v, i) => types[i] === Array); - return [ - numbers, - booleans, - arrays - ]; - } - - /** - * Returns an attempt object representing the current state or a formatted version of the raw state object supplied. - * @param {Array} [state] JSON object representing the component state, defaults to current state returned from getAttemptState(). - * @returns {Object} - */ - getAttemptObject(state = this.getAttemptState()) { - const trackables = this.trackable(); - const types = this.trackableType(); - trackables.find((name, index) => { - // Exclude _attemptStates as it's trackable but isn't needed here - if (name !== '_attemptStates') return; - trackables.splice(index, 1); - types.splice(index, 1); - return true; - }); - const numbers = (state[0] || []).slice(0); - const booleans = (state[1] || []).slice(0); - const arrays = (state[2] || []).slice(0); - const object = {}; - trackables.forEach((n, i) => { - if (n === '_id') return; - switch (types[i]) { - case Number: - object[n] = numbers.shift(); - break; - case Boolean: - object[n] = booleans.shift(); - break; - case Array: - object[n] = arrays.shift(); - break; - } - }); - return object; - } - - /** - * Sets the current attempt state from the supplied attempt state object. - * @param {Object} object JSON object representing the component state. - * @param {boolean} silent Stops change events from triggering - */ - setAttemptObject(object, silent = true) { - this.set(object, { silent }); - } - - /** - * Adds the current attempt state object or the supplied state object to the attempts store. - * @param {Object} [object] JSON object representing the component state. Defaults to current JSON. - */ - addAttemptObject(object = this.getAttemptObject()) { - const attemptStates = this.get('_attemptStates') || []; - const state = this.getAttemptState(object); - attemptStates.push(state); - this.set('_attemptStates', attemptStates); - } - - /** - * Returns an array of the previous state objects. The most recent state is last in the list. - * @returns {Array} - */ - getAttemptObjects() { - const states = this.get('_attemptStates') || []; - return states.map(state => this.getAttemptObject(state)); - } - } return QuestionModel;