diff --git a/src/core/js/adapt.js b/src/core/js/adapt.js index 6db358445..c9c0a0cf9 100644 --- a/src/core/js/adapt.js +++ b/src/core/js/adapt.js @@ -7,7 +7,8 @@ define([ defaults: { _canScroll: true, //to stop scrollTo behaviour, _outstandingCompletionChecks: 0, - _pluginWaitCount:0 + _pluginWaitCount:0, + _isStarted: false }, lockedAttributes: { @@ -82,6 +83,8 @@ define([ //start adapt in a full restored state Adapt.trigger('adapt:start'); Backbone.history.start(); + Adapt.set("_isStarted", true); + Adapt.trigger('adapt:initialize'); }); diff --git a/src/core/js/libraries/backbone.controller.js b/src/core/js/libraries/backbone.controller.js index 9ac6111b4..1e2365500 100644 --- a/src/core/js/libraries/backbone.controller.js +++ b/src/core/js/libraries/backbone.controller.js @@ -1,4 +1,4 @@ -// 2017-02-28 https://github.com/cgkineo/backbone.controller +// 2017-04-11 https://github.com/cgkineo/backbone.controller /* Adds an extensible class to backbone, which doesn't have a Model or DOM element (.$el) and isn't a Collection,. It still works exactly like Model, View and Collection, in that it has the Events API, .extend and an initialize function @@ -24,4 +24,6 @@ define("backbone.controller", [ Controller.extend = Backbone.View.extend; + return Backbone; + }); \ No newline at end of file diff --git a/src/core/js/libraries/backbone.controller.results.js b/src/core/js/libraries/backbone.controller.results.js new file mode 100644 index 000000000..d9fcbafb4 --- /dev/null +++ b/src/core/js/libraries/backbone.controller.results.js @@ -0,0 +1,39 @@ +// 2017-09-06 https://github.com/cgkineo/backbone.controller.results +/* + These functions are useful to resolve instance properties which are an array or object + or instance functions which return an array/object, to copy and extend the returned value. +*/ +define('backbone.controller.results', [ + 'underscore.results', + 'backbone.controller' +], function(_, Backbone) { + + var extend = [ Backbone.View, Backbone.Model, Backbone.Collection, Backbone.Controller ]; + + function resultExtendClass() { + + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(this.prototype); + + return _.resultExtend.apply(this, args); + + }; + + function resultExtendInstance() { + + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(this); + + return _.resultExtend.apply(this, args); + + }; + + _.each(extend, function(item) { + + item.resultExtend = resultExtendClass; + item.prototype.resultExtend = resultExtendInstance; + + }); + + +}); diff --git a/src/core/js/libraries/underscore.results.js b/src/core/js/libraries/underscore.results.js new file mode 100644 index 000000000..d437a58d7 --- /dev/null +++ b/src/core/js/libraries/underscore.results.js @@ -0,0 +1,79 @@ +// 2017-09-06 https://github.com/cgkineo/underscore.results +define('underscore.results', [ + 'underscore' +], function(_) { + + _.mixin({ + + /* + This function is useful to resolve instance properties which are an array or object + or instance functions which return an array/object, to copy and extend the returned value. + */ + resultExtend: function(instance, propertyName, withData) { + + /* + Resolve the propertyName on the instance, it should be an object or array or + a function returning an object or an array + */ + var result = _.result(instance, propertyName); + var resultType = (result instanceof Array ? "array" : typeof result); + + if (!withData) { + + // If no withData assume we're just copying the result + switch (resultType) { + case "array": + // Create a copy of result and return + return result.slice(0); + case "object": + // Create a copy of result and return + return _.extend({}, result); + default: + throw "Incorrect types in resultExtend"; + } + + } + + var withType = (withData instanceof Array ? "array" : typeof withData); + + // If no result, make a dummy one from the withData type + if (!result) { + + switch (withType) { + case "array": + result = []; + resultType = "array"; + break; + case "object": + result = {}; + resultType = "object"; + break; + default: + throw "Incorrect types in resultExtend"; + } + + } + + if (resultType !== withType) { + throw "Incorrect types in resultExtend"; + } + + switch (resultType) { + case "array": + // Create a copy of result, concat new data and return + return result.slice(0).concat(withData); + case "object": + // Create a copy of result, overwrite with new data and return + return _.extend({}, result, withData); + } + + // If the resolved result isn't an array or object throw an error + throw "Incorrect types in resultExtend"; + + } + + }); + + return _; + +}); diff --git a/src/core/js/models/adaptModel.js b/src/core/js/models/adaptModel.js index 936db6605..261aac48f 100644 --- a/src/core/js/models/adaptModel.js +++ b/src/core/js/models/adaptModel.js @@ -22,6 +22,12 @@ define([ _isHidden: false }, + trackable: [ + '_id', + '_isComplete', + '_isInteractionComplete' + ], + initialize: function () { // Wait until data is loaded before setting up model this.listenToOnce(Adapt, 'app:dataLoaded', this.setupModel); @@ -52,7 +58,55 @@ define([ this.checkLocking(); } + + this.setupTrackables(); + }, this)); + + }, + + setupTrackables: function() { + + // Limit state trigger calls and make state change callbacks batched-asynchronous + var originalTrackableStateFunction = this.triggerTrackableState; + this.triggerTrackableState = _.compose( + _.bind(function() { + + // Flag that the function is awaiting trigger + this.triggerTrackableState.isQueued = true; + + }, this), + _.debounce(_.bind(function() { + + // Trigger original function + originalTrackableStateFunction.apply(this); + + // Unset waiting flag + this.triggerTrackableState.isQueued = false; + + }, this), 17) + ); + + // Listen to model changes, trigger trackable state change when appropriate + this.listenTo(this, "change", function(model, value) { + + // Skip if trigger queued or adapt hasn't started yet + if (this.triggerTrackableState.isQueued || !Adapt.attributes._isStarted) { + return; + } + + // Check that property is trackable + var isTrackable = _.keys(model.changed).find(function(item, index) { + return _.contains(_.result(this, 'trackable', []), item); + }.bind(this)); + + if (isTrackable) { + // Trigger trackable state change + this.triggerTrackableState(); + } + + }); + }, setupChildListeners: function() { @@ -70,6 +124,39 @@ define([ init: function() {}, + getTrackableState: function() { + + var trackable = this.resultExtend("trackable", []); + var json = this.toJSON(); + + var args = trackable; + args.unshift(json); + + return _.pick.apply(_, args); + + }, + + setTrackableState: function(state) { + + var trackable = this.resultExtend("trackable", []); + + var args = trackable; + args.unshift(state); + + state = _.pick.apply(_, args); + + this.set(state); + + return this; + + }, + + triggerTrackableState: function() { + + Adapt.trigger("state:change", this, this.getTrackableState()); + + }, + reset: function(type, force) { if (!this.get("_canReset") && !force) return; diff --git a/src/core/js/models/componentModel.js b/src/core/js/models/componentModel.js index 1cdd1db31..647ccb1a8 100644 --- a/src/core/js/models/componentModel.js +++ b/src/core/js/models/componentModel.js @@ -5,7 +5,12 @@ define([ var ComponentModel = AdaptModel.extend({ _parent:'blocks', - _siblings:'components' + _siblings:'components', + + trackable: AdaptModel.resultExtend("trackable", [ + '_userAnswer' + ]) + }); return ComponentModel; diff --git a/src/core/js/models/questionModel.js b/src/core/js/models/questionModel.js index 055911d61..452cd608e 100644 --- a/src/core/js/models/questionModel.js +++ b/src/core/js/models/questionModel.js @@ -13,17 +13,25 @@ define([ // Used to set model defaults defaults: function() { // Extend from the ComponentModel defaults - return _.extend({ - '_isQuestionType': true, - '_shouldDisplayAttempts': false, - '_canShowModelAnswer': true, - '_canShowFeedback': true, - '_canShowMarking': true, - '_isSubmitted': false, - '_questionWeight': Adapt.config.get("_questionWeight"), - }, ComponentModel.prototype.defaults); + return ComponentModel.resultExtend("defaults", { + _isQuestionType: true, + _shouldDisplayAttempts: false, + _canShowModelAnswer: true, + _canShowFeedback: true, + _canShowMarking: true, + _isSubmitted: false, + _questionWeight: Adapt.config.get("_questionWeight"), + }); }, + // Extend from the ComponentModel trackable + trackable: ComponentModel.resultExtend("trackable", [ + '_isSubmitted', + '_score', + '_isCorrect', + '_attemptsLeft' + ]), + init: function() { this.setupDefaultSettings(); this.listenToOnce(Adapt, "adapt:initialize", this.onAdaptInitialize); diff --git a/src/core/js/scriptLoader.js b/src/core/js/scriptLoader.js index 8e865d50f..9f2588c4b 100644 --- a/src/core/js/scriptLoader.js +++ b/src/core/js/scriptLoader.js @@ -21,8 +21,10 @@ }, paths: { underscore: 'libraries/underscore.min', + 'underscore.results': 'libraries/underscore.results', backbone: 'libraries/backbone.min', 'backbone.controller': 'libraries/backbone.controller', + 'backbone.controller.results': 'libraries/backbone.controller.results', handlebars: 'libraries/handlebars.min', velocity: 'libraries/velocity.min', imageReady: 'libraries/imageReady', @@ -88,8 +90,10 @@ function loadFoundationLibraries() { require([ 'underscore', + 'underscore.results', 'backbone', 'backbone.controller', + 'backbone.controller.results', 'handlebars', 'velocity', 'imageReady',