diff --git a/README.md b/README.md index c8f6940d..8e3eb01b 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,9 @@ Determines whether the `cmi.completion_status` is set to "completed" if the asse #### \_showCookieLmsResetButton (boolean): Determines whether a reset button will be available to relaunch the course and optionally clear tracking data (scorm_test_harness.html only). The default is `false`. +#### \_shouldPersistCookieLMSData (boolean): +Determines whether to persist the cookie data over browser sessions (scorm_test_harness.html only). The default is `true`. +
Back to Top
## Notes diff --git a/example.json b/example.json index 45898f3d..cf0331ee 100644 --- a/example.json +++ b/example.json @@ -29,7 +29,8 @@ "_exitStateIfComplete": "auto", "_setCompletedWhenFailed": true }, - "_showCookieLmsResetButton": false + "_showCookieLmsResetButton": false, + "_shouldPersistCookieLMSData": true } // to be added to course/en/course.json (note: you only need to add the ones you want to change/translate) diff --git a/js/adapt-contrib-spoor.js b/js/adapt-contrib-spoor.js index 7bc8615f..a48c0690 100644 --- a/js/adapt-contrib-spoor.js +++ b/js/adapt-contrib-spoor.js @@ -3,11 +3,13 @@ import ScormWrapper from './scorm/wrapper'; import StatefulSession from './adapt-stateful-session'; import OfflineStorage from './adapt-offlineStorage-scorm'; import offlineStorage from 'core/js/offlineStorage'; +import { shouldStart as shouldStartCookieLMS, start as startCookieLMS } from './scorm/cookieLMS'; class Spoor extends Backbone.Controller { initialize() { this.config = null; + if (shouldStartCookieLMS) startCookieLMS(); this.scorm = ScormWrapper.getInstance(); this.listenToOnce(Adapt, 'offlineStorage:prepare', this._prepare); } diff --git a/js/scorm/cookieLMS.js b/js/scorm/cookieLMS.js new file mode 100644 index 00000000..087df92a --- /dev/null +++ b/js/scorm/cookieLMS.js @@ -0,0 +1,189 @@ +import Adapt from 'core/js/adapt'; +import Cookies from 'libraries/js-cookie.js'; + +/** Start the mock API if window.ISCOOKIELMS exists and isn't null */ +export const shouldStart = (Object.prototype.hasOwnProperty.call(window, 'ISCOOKIELMS') && window.ISCOOKIELMS !== null); + +/** Store the data in a cookie if window.ISCOOKIELMS is true, otherwise setup the API without storing data. */ +export const isStoringData = (window.ISCOOKIELMS === true); + +export function createResetButton() { + const resetButtonStyle = ''; + const resetButton = ''; + $('body').append($(resetButtonStyle)); + const $button = $(resetButton); + $('body').append($button); + $button.on('click', e => { + if (!e.shiftKey) { + Cookies.remove('_spoor'); + alert('SCORM tracking cookie has been deleted! Tip: shift-click reset to preserve cookie.'); + } + window.location = window.location.pathname; + }); +} + +export function configure() { + if (!isStoringData) return; + const spoorConfig = Adapt.config.get('_spoor'); + if (spoorConfig?._showCookieLmsResetButton) createResetButton(); + if (!spoorConfig?._shouldPersistCookieLMSData) { + Cookies.defaults = { + // uncomment this if you need the cookie to 'persist'. if left commented-out it will act as a 'session' cookie + // see https://github.com/js-cookie/js-cookie/tree/latest#expires + /* expires: 365, */ + sameSite: 'strict' + }; + } +} + +export function postStorageWarning() { + if (postStorageWarning.__storageWarningTimeoutId !== null) return; + postStorageWarning.__storageWarningTimeoutId = setTimeout(() => { + const notificationMethod = (Adapt.config.get('_spoor')?._advancedSettings?._suppressErrors === true) + ? console.error + : alert; + postStorageWarning.__storageWarningTimeoutId = null; + notificationMethod('Warning: possible cookie storage limit exceeded - tracking may malfunction'); + }, 1000); +} + +export function start () { + + const GenericAPI = { + + __offlineAPIWrapper: true, + + store: function(force) { + if (!isStoringData) return; + + if (!force && Cookies.get('_spoor') === undefined) return; + + Cookies.set('_spoor', this.data); + + // a length mismatch will most likely indicate cookie storage limit exceeded + if (Cookies.get('_spoor').length !== JSON.stringify(this.data).length) postStorageWarning(); + }, + + fetch: function() { + if (!isStoringData) { + this.data = {}; + return; + } + + this.data = Cookies.getJSON('_spoor'); + + if (!this.data) { + this.data = {}; + return false; + } + + return true; + } + + }; + + // SCORM 1.2 API + window.API = { + + ...GenericAPI, + + LMSInitialize: function() { + configure(); + if (!this.fetch()) { + this.data['cmi.core.lesson_status'] = 'not attempted'; + this.data['cmi.suspend_data'] = ''; + this.data['cmi.core.student_name'] = 'Surname, Sam'; + this.data['cmi.core.student_id'] = 'sam.surname@example.org'; + this.store(true); + } + return 'true'; + }, + + LMSFinish: function() { + return 'true'; + }, + + LMSGetValue: function(key) { + return this.data[key]; + }, + + LMSSetValue: function(key, value) { + const str = 'cmi.interactions.'; + if (key.indexOf(str) !== -1) return 'true'; + + this.data[key] = value; + + this.store(); + return 'true'; + }, + + LMSCommit: function() { + return 'true'; + }, + + LMSGetLastError: function() { + return 0; + }, + + LMSGetErrorString: function() { + return 'Fake error string.'; + }, + + LMSGetDiagnostic: function() { + return 'Fake diagnostic information.'; + } + }; + + // SCORM 2004 API + window.API_1484_11 = { + + ...GenericAPI, + + Initialize: function() { + configure(); + if (!this.fetch()) { + this.data['cmi.completion_status'] = 'not attempted'; + this.data['cmi.suspend_data'] = ''; + this.data['cmi.learner_name'] = 'Surname, Sam'; + this.data['cmi.learner_id'] = 'sam.surname@example.org'; + this.store(true); + } + return 'true'; + }, + + Terminate: function() { + return 'true'; + }, + + GetValue: function(key) { + return this.data[key]; + }, + + SetValue: function(key, value) { + const str = 'cmi.interactions.'; + if (key.indexOf(str) !== -1) return 'true'; + + this.data[key] = value; + + this.store(); + return 'true'; + }, + + Commit: function() { + return 'true'; + }, + + GetLastError: function() { + return 0; + }, + + GetErrorString: function() { + return 'Fake error string.'; + }, + + GetDiagnostic: function() { + return 'Fake diagnostic information.'; + } + + }; +} diff --git a/properties.schema b/properties.schema index 162b2904..42e7e380 100644 --- a/properties.schema +++ b/properties.schema @@ -269,6 +269,15 @@ "inputType": "Checkbox", "validators": [], "help": "If enabled, a reset button will be available to relaunch the course and optionally clear tracking data (scorm_test_harness.html only)." + }, + "_shouldPersistCookieLMSData": { + "type": "boolean", + "required": false, + "default": true, + "title": "Persist cookie data (scorm_test_harness.html only)", + "inputType": "Checkbox", + "validators": [], + "help": "If enabled, the course data will persist over browser sessions (scorm_test_harness.html only)." } } } diff --git a/required/index.html b/required/index.html index 3cc83604..b51ae782 100644 --- a/required/index.html +++ b/required/index.html @@ -5,10 +5,9 @@ window.ADAPT_BUILD_TYPE = '@@build.type'; window.ISCOOKIELMS = false; - - Adapt + @@ -18,7 +17,7 @@
- +
diff --git a/required/index_lms.html b/required/index_lms.html index e29dc57a..33c2dd31 100644 --- a/required/index_lms.html +++ b/required/index_lms.html @@ -3,10 +3,11 @@ - Adapt + @@ -14,11 +15,11 @@ -
-
- -
-
+
+
+ +
+
diff --git a/required/offline_API_wrapper.js b/required/offline_API_wrapper.js deleted file mode 100644 index 0572b240..00000000 --- a/required/offline_API_wrapper.js +++ /dev/null @@ -1,185 +0,0 @@ -var GenericAPI = { - - __offlineAPIWrapper: true, - __storageWarningTimeoutId: null, - - store: function(force) { - if (window.ISCOOKIELMS === false) return; - - if (!force && window.Cookies.get('_spoor') === undefined) return; - - window.Cookies.set('_spoor', this.data); - - // a length mismatch will most likely indicate cookie storage limit exceeded - if (window.Cookies.get('_spoor').length !== JSON.stringify(this.data).length) { - // defer call to avoid excessive alerts - if (this.__storageWarningTimeoutId == null) { - this.__storageWarningTimeoutId = setTimeout(function() { - this.storageWarning(); - }.bind(this), 1000); - } - } - }, - - fetch: function() { - if (window.ISCOOKIELMS === false) { - this.data = {}; - return; - } - - this.data = window.Cookies.getJSON('_spoor'); - - if (!this.data) { - this.data = {}; - return false; - } - - return true; - }, - - reset: function() { - window.Cookies.remove('_spoor'); - }, - - createResetButton: function() { - $('body').append($('')); - var $button = $(''); - $('body').append($button); - $button.on('click', function(e) { - if (!e.shiftKey) { - this.reset(); - alert('SCORM tracking cookie has been deleted! Tip: shift-click reset to preserve cookie.'); - } - window.location = window.location.pathname; - }.bind(this)); - }, - - storageWarning: function() { - var Adapt; - var notificationMethod = alert; - this.__storageWarningTimeoutId = null; - if (require) Adapt = require('core/js/adapt'); - if (Adapt && Adapt.config && Adapt.config.has('_spoor')) { - if (Adapt.config.get('_spoor')._advancedSettings && - Adapt.config.get('_spoor')._advancedSettings._suppressErrors === true) { - notificationMethod = console.error; - } - } - notificationMethod('Warning: possible cookie storage limit exceeded - tracking may malfunction'); - } - -}; - -// SCORM 1.2 API -window.API = { - - LMSInitialize: function() { - const Adapt = require('core/js/adapt'); - - if (window.ISCOOKIELMS !== false && Adapt?.config?.get('_spoor')?._showCookieLmsResetButton) { - this.createResetButton(); - } - if (!this.fetch()) { - this.data['cmi.core.lesson_status'] = 'not attempted'; - this.data['cmi.suspend_data'] = ''; - this.data['cmi.core.student_name'] = 'Surname, Sam'; - this.data['cmi.core.student_id'] = 'sam.surname@example.org'; - this.store(true); - } - return 'true'; - }, - - LMSFinish: function() { - return 'true'; - }, - - LMSGetValue: function(key) { - return this.data[key]; - }, - - LMSSetValue: function(key, value) { - var str = 'cmi.interactions.'; - if (key.indexOf(str) !== -1) return 'true'; - - this.data[key] = value; - - this.store(); - return 'true'; - }, - - LMSCommit: function() { - return 'true'; - }, - - LMSGetLastError: function() { - return 0; - }, - - LMSGetErrorString: function() { - return 'Fake error string.'; - }, - - LMSGetDiagnostic: function() { - return 'Fake diagnostic information.'; - } -}; - -// SCORM 2004 API -window.API_1484_11 = { - - Initialize: function() { - const Adapt = require('core/js/adapt'); - - if (window.ISCOOKIELMS !== false && Adapt?.config?.get('_spoor')?._showCookieLmsResetButton) { - this.createResetButton(); - } - if (!this.fetch()) { - this.data['cmi.completion_status'] = 'not attempted'; - this.data['cmi.suspend_data'] = ''; - this.data['cmi.learner_name'] = 'Surname, Sam'; - this.data['cmi.learner_id'] = 'sam.surname@example.org'; - this.store(true); - } - return 'true'; - }, - - Terminate: function() { - return 'true'; - }, - - GetValue: function(key) { - return this.data[key]; - }, - - SetValue: function(key, value) { - var str = 'cmi.interactions.'; - if (key.indexOf(str) !== -1) return 'true'; - - this.data[key] = value; - - this.store(); - return 'true'; - }, - - Commit: function() { - return 'true'; - }, - - GetLastError: function() { - return 0; - }, - - GetErrorString: function() { - return 'Fake error string.'; - }, - - GetDiagnostic: function() { - return 'Fake diagnostic information.'; - } - -}; - -for (var key in GenericAPI) { - window.API[key] = window.API_1484_11[key] = GenericAPI[key]; -} - diff --git a/required/scorm_test_harness.html b/required/scorm_test_harness.html index 660783c8..6c591cc3 100644 --- a/required/scorm_test_harness.html +++ b/required/scorm_test_harness.html @@ -1,16 +1,15 @@ - + - - - + @@ -23,13 +22,4 @@ - - - + diff --git a/schema/config.schema.json b/schema/config.schema.json index 84393056..a9f2f3cc 100644 --- a/schema/config.schema.json +++ b/schema/config.schema.json @@ -192,6 +192,12 @@ "title": "Show reset button (scorm_test_harness.html only)", "description": "If enabled, a reset button will be available to relaunch the course and optionally clear tracking data (scorm_test_harness.html only).", "default": false + }, + "_shouldPersistCookieLMSData": { + "type": "boolean", + "title": "Persist cookie data (scorm_test_harness.html only)", + "description": "If enabled, the course data will persist over browser sessions (scorm_test_harness.html only).", + "default": true } } }