diff --git a/README.md b/README.md index 818d6fd..9b1d9c8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ For a guide on the difference between using two curly braces and three curly bra No known limitations. ---------------------------- -**Version number:** 4.1.1 +**Version number:** 4.2.0 **Framework versions:** 5.8+ **Author / maintainer:** DeltaNet, forked from [adapt-contrib-assessmentResults](https://github.com/adaptlearning/adapt-contrib-assessmentResults) **Accessibility support:** WAI AA diff --git a/bower.json b/bower.json index fe9ff6f..e617bee 100644 --- a/bower.json +++ b/bower.json @@ -1,10 +1,11 @@ { "name": "adapt-contrib-assessmentResults-audio", - "version": "4.1.1", + "version": "4.2.0", "framework": ">=5.8", "homepage": "https://github.com/deltanet/adapt-contrib-assessmentResults-audio", "issues": "https://github.com/deltanet/adapt-contrib-assessmentResults-audio/issues", "component" : "assessmentResultsAudio", + "targetAttribute": "_assessmentResultsAudio", "displayName" : "Assessment Results Audio", "description": "An audio enabled component used to display a single assessment's results", "main": "/js/adapt-contrib-assessmentResults-audio.js", diff --git a/example.json b/example.json index e053cae..67c591c 100644 --- a/example.json +++ b/example.json @@ -3,7 +3,7 @@ "_id": "c-05", "_parentId": "b-05", "_type": "component", - "_component": "assessmentResults", + "_component": "assessmentResultsAudio", "_classes": "", "_layout": "full", "title": "Results", diff --git a/js/adapt-contrib-assessmentResults-audio.js b/js/adapt-contrib-assessmentResults-audio.js index 01be058..c7b08dc 100644 --- a/js/adapt-contrib-assessmentResults-audio.js +++ b/js/adapt-contrib-assessmentResults-audio.js @@ -1,12 +1,8 @@ -define([ - 'core/js/adapt', - './assessmentResultsAudioModel', - './assessmentResultsAudioView' -], function(Adapt, AssessmentResultsAudioModel, AssessmentResultsAudioView) { - - return Adapt.register("assessmentResultsAudio", { - model: AssessmentResultsAudioModel, - view: AssessmentResultsAudioView - }); +import Adapt from 'core/js/adapt'; +import AssessmentResultsAudioModel from './assessmentResultsAudioModel'; +import AssessmentResultsAudioView from './assessmentResultsAudioView'; +export default Adapt.register('assessmentResultsAudio', { + model: AssessmentResultsAudioModel, + view: AssessmentResultsAudioView }); diff --git a/js/assessmentResultsAudioModel.js b/js/assessmentResultsAudioModel.js index 77d8012..7b82f98 100644 --- a/js/assessmentResultsAudioModel.js +++ b/js/assessmentResultsAudioModel.js @@ -1,181 +1,174 @@ -define([ - 'core/js/adapt', - 'core/js/models/componentModel' -], function(Adapt, ComponentModel) { - - var AssessmentResultsAudioModel = ComponentModel.extend({ - - init: function() { - this.set('originalBody', this.get('body'));// save the original body text so we can restore it when the assessment is reset - this.set('originalInstruction', this.get('instruction'));// save the original body text so we can restore it when the assessment is reset - - this.listenTo(Adapt, { - 'assessments:complete': this.onAssessmentComplete, - 'assessments:reset': this.onAssessmentReset - }); - }, - - /** - * Checks to see if the assessment was completed in a previous session or not - */ - checkIfAssessmentComplete: function() { - if (!Adapt.assessment || this.get('_assessmentId') === undefined) { - return; - } - - var assessmentModel = Adapt.assessment.get(this.get('_assessmentId')); - if (!assessmentModel || assessmentModel.length === 0) return; - - var state = assessmentModel.getState(); - if (state.isComplete) { - this.onAssessmentComplete(state); - return; - } - - this.setVisibility(); - }, - - onAssessmentComplete: function(state) { - if (this.get('_assessmentId') === undefined || - this.get('_assessmentId') != state.id) return; - - /* - make shortcuts to some of the key properties in the state object so that - content developers can just use {{attemptsLeft}} in json instead of {{state.attemptsLeft}} - */ - this.set( { - _state: state, - attempts: state.attempts, - attemptsSpent: state.attemptsSpent, - attemptsLeft: state.attemptsLeft, - score: state.score, - scoreAsPercent: state.scoreAsPercent, - maxScore: state.maxScore, - isPass: state.isPass - }); - - this.setFeedbackBand(state); - - this.checkRetryEnabled(state); - - this.setFeedbackText(); - - this.toggleVisibility(true); - }, - - setFeedbackBand: function(state) { - var scoreProp = state.isPercentageBased ? 'scoreAsPercent' : 'score'; - var bands = _.sortBy(this.get('_bands'), '_score'); - - for (var i = (bands.length - 1); i >= 0; i--) { - var isScoreInBandRange = (state[scoreProp] >= bands[i]._score); - if (!isScoreInBandRange) continue; - - this.set('_feedbackBand', bands[i]); - break; - } - }, - - checkRetryEnabled: function(state) { - var assessmentModel = Adapt.assessment.get(state.id); - if (!assessmentModel.canResetInPage()) return false; - - var feedbackBand = this.get('_feedbackBand'); - var isRetryEnabled = (feedbackBand && feedbackBand._allowRetry) !== false; - var isAttemptsLeft = (state.attemptsLeft > 0 || state.attemptsLeft === 'infinite'); - var showRetry = isRetryEnabled && isAttemptsLeft && (!state.isPass || state.allowResetIfPassed); - - this.set({ - _isRetryEnabled: showRetry, - retryFeedback: showRetry ? this.get('_retry').feedback : '' - }); - }, - - setFeedbackText: function() { - var feedbackBand = this.get('_feedbackBand'); - - // ensure any handlebars expressions in the .feedback are handled... - var feedback = feedbackBand ? Handlebars.compile(feedbackBand.feedback)(this.toJSON()) : ''; - - this.set({ - feedback: feedback, - body: this.get('_completionBody'), - instruction: feedbackBand.instruction - }); - - if (this.get('_audioAssessment')._isEnabled) { - this.set('audioFile', feedbackBand._audio.src); - } - }, - - setVisibility: function() { - if (!Adapt.assessment) return; - - var isVisibleBeforeCompletion = this.get('_isVisibleBeforeCompletion') || false; - var wasVisible = this.get('_isVisible'); - - var assessmentModel = Adapt.assessment.get(this.get('_assessmentId')); - if (!assessmentModel || assessmentModel.length === 0) return; - - var state = assessmentModel.getState(); - var isComplete = state.isComplete; - var isAttemptInProgress = state.attemptInProgress; - var attemptsSpent = state.attemptsSpent; - var hasHadAttempt = (!isAttemptInProgress && attemptsSpent > 0); - - var isVisible = (isVisibleBeforeCompletion && !isComplete) || hasHadAttempt; - - if (!wasVisible && isVisible) isVisible = false; - - this.toggleVisibility(isVisible); - }, - - toggleVisibility: function (isVisible) { - if (isVisible === undefined) { - isVisible = !this.get('_isVisible'); - } - - this.set('_isVisible', isVisible, {pluginName: 'assessmentResults'}); - }, - - checkCompletion: function() { - if (this.get('_setCompletionOn') === 'pass' && !this.get('isPass')) { - return; - } - - this.setCompletionStatus(); - }, - - /** - * Handles resetting the component whenever its corresponding assessment is reset - * The component can either inherit the assessment's reset type or define its own - */ - onAssessmentReset: function(state) { - if (this.get('_assessmentId') === undefined || - this.get('_assessmentId') != state.id) return; - - var resetType = this.get('_resetType'); - if (!resetType || resetType === 'inherit') { - resetType = state.resetType || 'hard';// backwards compatibility - state.resetType was only added in assessment v2.3.0 - } - this.reset(resetType, true); - }, - - reset: function() { - this.set({ - body: this.get('originalBody'), - instruction: this.get('originalInstruction'), - state: null, - feedback: '', - _feedbackBand: null, - retryFeedback: '', - _isRetryEnabled: false - }); - - ComponentModel.prototype.reset.apply(this, arguments); - } +import Adapt from 'core/js/adapt'; +import ComponentModel from 'core/js/models/componentModel'; + +export default class AssessmentResultsAudioModel extends ComponentModel { + + init(...args) { + // save the original body text so we can restore it when the assessment is reset + this.set('originalBody', this.get('body')); + // save the original body text so we can restore it when the assessment is reset + this.set('originalInstruction', this.get('instruction')); + + this.listenTo(Adapt, { + 'assessments:complete': this.onAssessmentComplete, + 'assessments:reset': this.onAssessmentReset }); - return AssessmentResultsAudioModel; + super.init(...args); + } + + /** + * Checks to see if the assessment was completed in a previous session or not + */ + checkIfAssessmentComplete() { + if (!Adapt.assessment || this.get('_assessmentId') === undefined) { + return; + } + + const assessmentModel = Adapt.assessment.get(this.get('_assessmentId')); + if (!assessmentModel || assessmentModel.length === 0) return; + + const state = assessmentModel.getState(); + const isResetOnRevisit = assessmentModel.get('_assessment')._isResetOnRevisit; + if (state.isComplete && (!state.allowResetIfPassed || !isResetOnRevisit)) { + this.onAssessmentComplete(state); + return; + } + + this.setVisibility(); + } + + onAssessmentComplete(state) { + if (this.get('_assessmentId') === undefined || + this.get('_assessmentId') !== state.id) return; + + /* + make shortcuts to some of the key properties in the state object so that + content developers can just use {{attemptsLeft}} in json instead of {{state.attemptsLeft}} + */ + this._state = state; + this.set({ + attempts: state.attempts, + attemptsSpent: state.attemptsSpent, + attemptsLeft: state.attemptsLeft, + score: state.score, + scoreAsPercent: state.scoreAsPercent, + maxScore: state.maxScore, + isPass: state.isPass + }); + + this.setFeedbackBand(state); + + this.checkRetryEnabled(state); + + this.setFeedbackText(); + + this.toggleVisibility(true); + } + + setFeedbackBand(state) { + const scoreProp = state.isPercentageBased ? 'scoreAsPercent' : 'score'; + const bands = _.sortBy(this.get('_bands'), '_score'); + + for (let i = (bands.length - 1); i >= 0; i--) { + const isScoreInBandRange = (state[scoreProp] >= bands[i]._score); + if (!isScoreInBandRange) continue; + + this.set('_feedbackBand', bands[i]); + break; + } + } + + checkRetryEnabled(state) { + const assessmentModel = Adapt.assessment.get(state.id); + if (!assessmentModel.canResetInPage()) return false; + + const feedbackBand = this.get('_feedbackBand'); + const isRetryEnabled = (feedbackBand && feedbackBand._allowRetry) !== false; + const isAttemptsLeft = (state.attemptsLeft > 0 || state.attemptsLeft === 'infinite'); + const showRetry = isRetryEnabled && isAttemptsLeft && (!state.isPass || state.allowResetIfPassed); + + this.set({ + _isRetryEnabled: showRetry, + retryFeedback: showRetry ? this.get('_retry').feedback : '' + }); + } + + setFeedbackText() { + const feedbackBand = this.get('_feedbackBand'); + + // ensure any handlebars expressions in the .feedback are handled... + const feedback = feedbackBand ? Handlebars.compile(feedbackBand.feedback)(this.toJSON()) : ''; + + this.set({ + feedback, + body: this.get('_completionBody'), + instruction: feedbackBand.instruction + }); + + if (this.get('_audioAssessment')._isEnabled) { + this.set('audioFile', feedbackBand._audio.src); + } + } + + setVisibility() { + if (!Adapt.assessment) return; + + const assessmentModel = Adapt.assessment.get(this.get('_assessmentId')); + if (!assessmentModel || assessmentModel.length === 0) return; + + const state = assessmentModel.getState(); + const isAttemptInProgress = state.attemptInProgress; + const isComplete = !isAttemptInProgress && state.isComplete; + const isVisibleBeforeCompletion = this.get('_isVisibleBeforeCompletion') || false; + const isVisible = isVisibleBeforeCompletion || isComplete; + + this.toggleVisibility(isVisible); + } + + toggleVisibility(isVisible) { + if (isVisible === undefined) { + isVisible = !this.get('_isVisible'); + } + + this.set('_isVisible', isVisible, { pluginName: 'assessmentResultsAudio' }); + } + + checkCompletion() { + if (this.get('_setCompletionOn') === 'pass' && !this.get('isPass')) { + return; + } + + this.setCompletionStatus(); + } + + /** + * Handles resetting the component whenever its corresponding assessment is reset + * The component can either inherit the assessment's reset type or define its own + */ + onAssessmentReset(state) { + if (this.get('_assessmentId') === undefined || + this.get('_assessmentId') !== state.id) return; + + let resetType = this.get('_resetType'); + if (!resetType || resetType === 'inherit') { + // backwards compatibility - state.resetType was only added in assessment v2.3.0 + resetType = state.resetType || 'hard'; + } + this.reset(resetType, true); + } + + reset(...args) { + this.set({ + body: this.get('originalBody'), + instruction: this.get('originalInstruction'), + state: null, + feedback: '', + _feedbackBand: null, + retryFeedback: '', + _isRetryEnabled: false + }); -}); + super.reset(...args); + } +} diff --git a/js/assessmentResultsAudioView.js b/js/assessmentResultsAudioView.js index c1c2552..d981ab0 100644 --- a/js/assessmentResultsAudioView.js +++ b/js/assessmentResultsAudioView.js @@ -1,155 +1,148 @@ -define([ - 'core/js/adapt', - 'core/js/views/componentView' -], function(Adapt, ComponentView) { +import Adapt from 'core/js/adapt'; +import ComponentView from 'core/js/views/componentView'; - class AssessmentResultsAudioView extends ComponentView { +class AssessmentResultsAudioView extends ComponentView { - events() { - return { - 'click .js-assessment-retry-btn': 'onRetryClicked', - 'click .js-audio-toggle': 'toggleAudio' - } - } + events() { + return { + 'click .js-assessment-retry-btn': 'onRetryClicked', + 'click .js-audio-toggle': 'toggleAudio' + }; + } - preRender() { - this.model.setLocking('_isVisible', false); + preRender() { + this.model.setLocking('_isVisible', false); - this.listenTo(Adapt.parentView, 'preRemove', function () { - this.model.unsetLocking('_isVisible'); - }); + this.listenTo(Adapt.parentView, 'preRemove', () => { + this.model.unsetLocking('_isVisible'); + }); - this.listenTo(this.model, { - 'change:_feedbackBand': this.addClassesToArticle, - 'change:body': this.render - }); + this.listenTo(this.model, { + 'change:_feedbackBand': this.addClassesToArticle, + 'change:body': this.render + }); - if (Adapt.audio && this.model.get('_audioAssessment') && this.model.get('_audioAssessment')._isEnabled) { - this.setupAudio(); - } + if (Adapt.audio && this.model.get('_audioAssessment') && this.model.get('_audioAssessment')._isEnabled) { + this.setupAudio(); } + } - postRender() { - this.model.checkIfAssessmentComplete(); - this.setReadyStatus(); - this.setupInviewCompletion('.component__inner', this.model.checkCompletion.bind(this.model)); + postRender() { + this.model.checkIfAssessmentComplete(); + this.setReadyStatus(); + this.setupInviewCompletion('.component__inner', this.model.checkCompletion.bind(this.model)); - // Audio - if (!Adapt.audio || !this.model.get('_audioAssessment')._isEnabled) return; - // Hide controls if set in JSON or if audio is turned off - if (this.model.get('_audioAssessment')._showControls==false || Adapt.audio.audioClip[this.audioChannel].status==0){ - this.$('.audio__controls').addClass('is-hidden'); - } - } + // Audio + if (!Adapt.audio || !this.model.get('_audioAssessment')._isEnabled) return; - setupAudio() { - // Set vars - this.audioChannel = this.model.get("_audioAssessment")._channel; - this.elementId = this.model.get("_id"); - this.model.set('audioFile', this.model.get("_audioAssessment")._media.src); - - this.onscreenTriggered = false; + // Hide controls if set in JSON or if audio is turned off + if (this.model.get('_audioAssessment')._showControls==false || Adapt.audio.audioClip[this.audioChannel].status==0){ + this.$('.audio__controls').addClass('is-hidden'); + } + } - // Autoplay - if (Adapt.audio.autoPlayGlobal || this.model.get("_audioAssessment")._autoplay){ - this.canAutoplay = true; - } else { - this.canAutoplay = false; - } + setupAudio() { + this.audioChannel = this.model.get("_audioAssessment")._channel; + this.elementId = this.model.get("_id"); + this.model.set('audioFile', this.model.get("_audioAssessment")._media.src); - // Autoplay once - if (Adapt.audio.autoPlayOnceGlobal == false){ - this.autoplayOnce = false; - } else if(Adapt.audio.autoPlayOnceGlobal || this.model.get("_audioAssessment")._autoPlayOnce){ - this.autoplayOnce = true; - } else { - this.autoplayOnce = false; - } + this.onscreenTriggered = false; - // Hide controls if set in JSON or if audio is turned off - if (this.model.get('_audioAssessment')._showControls==false || Adapt.audio.audioClip[this.audioChannel].status==0){ - this.$('.audio__controls').addClass('is-hidden'); - } + // Autoplay + if (Adapt.audio.autoPlayGlobal || this.model.get("_audioAssessment")._autoplay){ + this.canAutoplay = true; + } else { + this.canAutoplay = false; } - onInview(event, visible, visiblePartX, visiblePartY) { - if (!visible) return; - - switch (visiblePartY) { - case 'top': - this.hasSeenTop = true; - break; - case 'bottom': - this.hasSeenBottom = true; - break; - case 'both': - this.hasSeenTop = this.hasSeenBottom = true; - } - - if (!this.hasSeenTop || !this.hasSeenBottom) { - this.onscreenTriggered = false; - Adapt.trigger('audio:onscreenOff', this.elementId, this.audioChannel); - }; - - if (visible && this.canAutoplay && this.onscreenTriggered == false) { - // Check if audio is set to on - if (Adapt.audio.audioClip[this.audioChannel].status == 1) { - Adapt.trigger('audio:playAudio', this.model.get('audioFile'), this.elementId, this.audioChannel); - } - // Set to false to stop autoplay when onscreen again - if (this.autoplayOnce) { - this.canAutoplay = false; - } - // Set to true to stop onscreen looping - this.onscreenTriggered = true; - } + // Autoplay once + if (Adapt.audio.autoPlayOnceGlobal == false){ + this.autoplayOnce = false; + } else if (Adapt.audio.autoPlayOnceGlobal || this.model.get("_audioAssessment")._autoPlayOnce){ + this.autoplayOnce = true; + } else { + this.autoplayOnce = false; + } - super.onInview(event, visible, visiblePartX, visiblePartY); + // Hide controls if set in JSON or if audio is turned off + if (this.model.get('_audioAssessment')._showControls==false || Adapt.audio.audioClip[this.audioChannel].status==0){ + this.$('.audio__controls').addClass('is-hidden'); } + } - toggleAudio(event) { - if (event) event.preventDefault(); + onInview(event, visible, visiblePartX, visiblePartY) { + if (!visible) return; + + switch (visiblePartY) { + case 'top': + this.hasSeenTop = true; + break; + case 'bottom': + this.hasSeenBottom = true; + break; + case 'both': + this.hasSeenTop = this.hasSeenBottom = true; + } - Adapt.audio.audioClip[this.audioChannel].onscreenID = ""; + if (!this.hasSeenTop || !this.hasSeenBottom) { + this.onscreenTriggered = false; + Adapt.trigger('audio:onscreenOff', this.elementId, this.audioChannel); + }; - if ($(event.currentTarget).hasClass('playing')) { - Adapt.trigger('audio:pauseAudio', this.audioChannel); - } else { + if (visible && this.canAutoplay && this.onscreenTriggered == false) { + // Check if audio is set to on + if (Adapt.audio.audioClip[this.audioChannel].status == 1) { Adapt.trigger('audio:playAudio', this.model.get('audioFile'), this.elementId, this.audioChannel); } + // Set to false to stop autoplay when onscreen again + if (this.autoplayOnce) { + this.canAutoplay = false; + } + // Set to true to stop onscreen looping + this.onscreenTriggered = true; } - /** - * Resets the state of the assessment and optionally redirects the user - * back to the assessment for another attempt. - */ - onRetryClicked() { - const state = this.model.get('_state'); - - Adapt.assessment.get(state.id).reset(null, wasReset => { - if (!wasReset) { - return; - } - if (this.model.get('_retry')._routeToAssessment === true) { - Adapt.navigateToElement('.' + state.articleId); - } - }); - } + super.onInview(event, visible, visiblePartX, visiblePartY); + } + + toggleAudio(event) { + if (event) event.preventDefault(); - /** - * If there are classes specified for the feedback band, apply them to the containing article - * This allows for custom styling based on the band the user's score falls into - */ - addClassesToArticle(model, value) { - if (!value || !value._classes) return; + Adapt.audio.audioClip[this.audioChannel].onscreenID = ""; - this.$el.parents('.article').addClass(value._classes); + if ($(event.currentTarget).hasClass('playing')) { + Adapt.trigger('audio:pauseAudio', this.audioChannel); + } else { + Adapt.trigger('audio:playAudio', this.model.get('audioFile'), this.elementId, this.audioChannel); } + } + /** + * Resets the state of the assessment and optionally redirects the user + * back to the assessment for another attempt. + */ + onRetryClicked() { + const state = this.model._state; + Adapt.assessment.get(state.id).reset(null, wasReset => { + if (!wasReset) return; + + if (this.model.get('_retry')._routeToAssessment !== true) return; + Adapt.navigateToElement(`.${state.articleId}`); + }); + } + + /** + * If there are classes specified for the feedback band, apply them to the containing article + * This allows for custom styling based on the band the user's score falls into + */ + addClassesToArticle(model, value) { + if (!value?._classes) return; + + this.$el.parents('.article').addClass(value._classes); } - AssessmentResultsAudioView.template = 'assessmentResultsAudio'; +} - return AssessmentResultsAudioView; +AssessmentResultsAudioView.template = 'assessmentResultsAudio'; -}); +export default AssessmentResultsAudioView; diff --git a/schema/component.schema.json b/schema/component.schema.json index fb070ca..02f4f28 100644 --- a/schema/component.schema.json +++ b/schema/component.schema.json @@ -1,5 +1,5 @@ { - "$anchor": "assessmentResults-component", + "$anchor": "assessmentResultsAudio-component", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "$merge": { diff --git a/schema/course.schema.json b/schema/course.schema.json index 98d497b..b08415f 100644 --- a/schema/course.schema.json +++ b/schema/course.schema.json @@ -1,5 +1,5 @@ { - "$anchor": "assessmentResults-course", + "$anchor": "assessmentResultsAudio-course", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "$patch": { @@ -12,7 +12,7 @@ "type": "object", "default": {}, "properties": { - "_assessmentResults": { + "_assessmentResultsAudio": { "type": "object", "default": {}, "properties": { diff --git a/templates/assessmentResultsAudio.hbs b/templates/assessmentResultsAudio.hbs index 944a2e5..0573840 100644 --- a/templates/assessmentResultsAudio.hbs +++ b/templates/assessmentResultsAudio.hbs @@ -1,17 +1,38 @@ {{! make the _globals object in course.json available to this template}} {{import_globals}} -
- {{> component this}} -
- {{#if _isRetryEnabled}} -
-
- {{{compile retryFeedback}}} -
-
- -
+ +
+ + {{> component this}} + +
+ + {{#if _isRetryEnabled}} + + {{/if}} + +
+