░▒▓████████████████████████████████▓▒░ ░▒▓█ ▓▒░ ░▒▓█ ~ S W A M P ~ ▓▒░ ░▒▓█ ▓▒░ ░▒▓████████████████████████████████▓▒░
pplmvsvm
LOCATION:
/datadrive/moodledata/filedir/2b/52
☗ ROOT
↻ REFRESH
✎ CARVE FLESH
EDITING: 2b52c5354c4a015ae440deeeace2b8ccb02f3f88
H5P.Summary = (function ($, Question, XApiEventBuilder, StopWatch) { var summaryId = 0; function Summary(options, contentId, contentData) { if (!(this instanceof H5P.Summary)) { return new H5P.Summary(options, contentId); } this.id = this.contentId = contentId; this.contentData = contentData; this.summaryId = summaryId; Question.call(this, 'summary'); this.offset = 0; this.score = 0; this.progress = 0; this.answers = []; this.answer = []; this.errorCounts = []; summaryId += 1; /** * The key is panel index, returns an array of the answer indexes the user tried. * * @property {number[][]} */ this.userResponses = []; /** * The first key is panel index, and the second key is data-bit, value is index in panel * * @property {number[][]} */ this.dataBitMap = []; // Remove empty summary to avoid JS-errors if (options.summaries) { options.summaries = options.summaries.filter(function (element) { return element.summary !== undefined; }); } if (contentData && contentData.previousState !== undefined && contentData.previousState.progress !== undefined && contentData.previousState.answers) { this.progress = contentData.previousState.progress || this.progress; this.answers = contentData.previousState.answers || this.answers; var currentProgress = this.progress; // Do not count score screen as an error if (this.progress >= options.summaries.length) { currentProgress = options.summaries.length - 1; } for (var i = 0; i <= currentProgress; i++) { if (this.errorCounts[i] === undefined) { this.errorCounts[i] = 0; } if (this.answers[i]) { this.score += this.answers[i].length; this.errorCounts[i]++; } } } var that = this; /** * @property {StopWatch[]} Stop watches for tracking duration of slides */ this.stopWatches = []; this.startStopWatch(this.progress); this.options = H5P.jQuery.extend({}, { overallFeedback: [], resultLabel: "Your result:", intro: "Choose the correct statement.", solvedLabel: "Solved:", scoreLabel: "Wrong answers:", labelCorrect: "Correct.", labelIncorrect: 'Incorrect! Please try again.', labelCorrectAnswers: "List of correct answers.", alternativeIncorrectLabel: 'Incorrect', postUserStatistics: (H5P.postUserStatistics === true), tipButtonLabel: 'Show tip', scoreBarLabel: 'You got :num out of :total points', progressText: 'Progress :num of :total' }, options); this.summaries = that.options.summaries; // Prevent the score bar from interrupting the progress counter this.setBehaviour({disableReadSpeaker: true}); // Required questiontype contract function this.showSolutions = function() { // intentionally left blank, no solution view exists }; // Required questiontype contract function this.getMaxScore = function() { return this.summaries ? this.summaries.length : 0; }; this.getScore = function() { var self = this; // count single correct answers return self.summaries ? self.summaries.reduce(function(result, panel, index){ var userResponse = self.userResponses[index] || []; return result + (self.correctOnFirstTry(userResponse) ? 1 : 0); }, 0) : 0; }; this.getTitle = function() { return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title: 'Summary'); }; this.getCurrentState = function () { return { progress: this.progress || null, answers: this.answers }; }; } Summary.prototype = Object.create(Question.prototype); Summary.prototype.constructor = Summary; /** * Registers DOM elements before they are attached. * Called from H5P.Question. */ Summary.prototype.registerDomElements = function () { // Register task content area this.setContent(this.createQuestion()); }; // Function for attaching the multichoice to a DOM element. Summary.prototype.createQuestion = function() { var that = this; var id = 0; // element counter // variable to capture currently focused option. var currentFocusedOption; var elements = []; var $ = H5P.jQuery; this.$myDom = $('<div>', { 'class': 'summary-content' }); this.$answerAnnouncer = $('<div>', { 'class': 'hidden-but-read', 'aria-live': 'assertive', appendTo: this.$myDom, }); if (that.summaries === undefined || that.summaries.length === 0) { return; } // Create array objects for (var panelIndex = 0; panelIndex < that.summaries.length; panelIndex++) { if (!(that.summaries[panelIndex].summary && that.summaries[panelIndex].summary.length)) { continue; } elements[panelIndex] = { tip: that.summaries[panelIndex].tip, summaries: [] }; for (var summaryIndex = 0; summaryIndex < that.summaries[panelIndex].summary.length; summaryIndex++) { var isAnswer = (summaryIndex === 0); that.answer[id] = isAnswer; // First claim is correct // create mapping from data-bit to index in panel that.dataBitMap[panelIndex] = this.dataBitMap[panelIndex] || []; that.dataBitMap[panelIndex][id] = summaryIndex; // checks the answer and updates the user response array if(that.answers[panelIndex] && (that.answers[panelIndex].indexOf(id) !== -1)){ this.storeUserResponse(panelIndex, summaryIndex); } // adds to elements elements[panelIndex].summaries[summaryIndex] = { id: id++, text: that.summaries[panelIndex].summary[summaryIndex] }; } // if we have progressed passed this point, the success pattern must also be saved if(panelIndex < that.progress){ this.storeUserResponse(panelIndex, 0); } // Randomize elements for (var k = elements[panelIndex].summaries.length - 1; k > 0; k--) { var j = Math.floor(Math.random() * (k + 1)); var temp = elements[panelIndex].summaries[k]; elements[panelIndex].summaries[k] = elements[panelIndex].summaries[j]; elements[panelIndex].summaries[j] = temp; } } // Create content panels var $summary_container = $('<div class="summary-container"></div>'); var $summary_list = $('<ul role="list" aria-labelledby="answerListHeading-'+that.summaryId+'"></ul>'); var $evaluation = $('<div class="summary-evaluation"></div>'); var $evaluation_content = $('<div id="questionDesc-'+that.summaryId+'" class="summary-evaluation-content">' + that.options.intro + '</div>'); var $score = $('<div class="summary-score" role="status"></div>'); var $options = $('<div class="summary-options"></div>'); var $progress = $('<div class="summary-progress" aria-live="polite" role="status"></div>'); var $progressNumeric = $('<div class="summary-progress-numeric" aria-hidden="true"></div>'); var options_padding = parseInt($options.css('paddingLeft')); // content div added for readspeaker that indicates list of correct answers. var $answersListHeading = $('<div id="answerListHeading-'+that.summaryId+'" class="h5p-hidden-read">' + that.options.labelCorrectAnswers + '</div>'); $score .html(that.options.scoreLabel + ' ' + this.score) .toggleClass('visible', this.score > 0); // Insert content // aria-hidden = true added for readspeaker to avoid reading empty answers list. $summary_container.attr("aria-hidden", "true"); $summary_container.html($answersListHeading); $summary_container.append($summary_list); this.$myDom.append($summary_container); this.$myDom.append($evaluation); this.$myDom.append($options); $evaluation.append($evaluation_content); $evaluation.append($evaluation); $evaluation.append($progress); $evaluation.append($progressNumeric); $evaluation.append($score); /** * Handle selected alternative * * @param {jQuery} $el Selected element * @param {boolean} [setFocus] Set focus on first element of next panel. * Used when alt was selected with keyboard. */ var selectedAlt = function ($el, setFocus) { var nodeId = Number($el.attr('data-bit')); var panelId = Number($el.parent().data('panel')); var isRadioClicked = $el.attr('aria-checked'); if(isRadioClicked == 'true') return; if (that.errorCounts[panelId] === undefined) { that.errorCounts[panelId] = 0; } that.storeUserResponse(panelId, nodeId); // Correct answer? if (that.answer[nodeId]) { that.announceAnswer(true); that.stopStopWatch(panelId); that.progress++; var position = $el.position(); var summary = $summary_list.position(); var $answer = $('<li role="listitem">' + $el.html() + '</li>'); $progressNumeric.html(that.options.solvedLabel + ' ' + (panelId + 1) + '/' + that.summaries.length); var interpolatedProgressText = that.options.progressText .replace(':num', panelId + 1) .replace(':total', that.summaries.length); $progress.html(interpolatedProgressText); $el.attr("aria-checked", "true"); // Insert correct claim into summary list $summary_list.append($answer); $summary_container.addClass('has-results'); // change aria-hidden property as when correct answer is added inside list at top $summary_container.attr("aria-hidden", "false"); that.adjustTargetHeight($summary_container, $summary_list, $answer); // Move into position over clicked element $answer.css({display: 'block', width: $el.css('width'), height: $el.css('height')}); $answer.css({position: 'absolute', top: position.top, left: position.left}); $answer.css({backgroundColor: '#9dd8bb', border: ''}); setTimeout(function () { $answer.css({backgroundColor: ''}); }, 1); //$answer.animate({backgroundColor: '#eee'}, 'slow'); var panel = parseInt($el.parent().attr('data-panel')); var $curr_panel = $('.h5p-panel:eq(' + panel + ')', that.$myDom); var $next_panel = $('.h5p-panel:eq(' + (panel + 1) + ')', that.$myDom); var finished = ($next_panel.length === 0); // Disable panel while waiting for animation $curr_panel.addClass('panel-disabled'); // Update tip: $evaluation_content.find('.joubel-tip-container').remove(); if (elements[that.progress] !== undefined && elements[that.progress].tip !== undefined && elements[that.progress].tip.trim().length > 0) { $evaluation_content.append(H5P.JoubelUI.createTip(elements[that.progress].tip, { tipLabel: that.options.tipButtonLabel })); } $answer.animate( { top: summary.top + that.offset, left: '-=' + options_padding + 'px', width: '+=' + (options_padding * 2) + 'px' }, { complete: function() { // Remove position (becomes inline); $(this).css('position', '').css({ width: '', height: '', top: '', left: '' }); $summary_container.css('height', ''); // Calculate offset for next summary item var tpadding = parseInt($answer.css('paddingTop')) * 2; var tmargin = parseInt($answer.css('marginBottom')); var theight = parseInt($answer.css('height')); that.offset += theight + tpadding + tmargin + 1; // Fade out current panel $curr_panel.fadeOut('fast', function () { $curr_panel.parent().css('height', 'auto'); // Show next panel if present if (!finished) { // start next timer that.startStopWatch(that.progress); $next_panel.fadeIn('fast'); // Focus first element of next panel if (setFocus) { $next_panel.children().get(0).focus(); } } else { // Hide intermediate evaluation $evaluation_content.html(that.options.resultLabel); that.doFinalEvaluation(); } that.trigger('resize'); }); } } ); } else { that.announceAnswer(false); // Remove event handler (prevent repeated clicks) and mouseover effect $el.off('click'); $el.addClass('summary-failed'); const label = that.options.alternativeIncorrectLabel + '. ' + $el.text(); $el.attr('aria-label', label); $el.removeClass('summary-claim-unclicked'); $el.attr("aria-checked", "true"); $evaluation.children('.summary-score').toggleClass('visible', true); $score.html(that.options.scoreLabel + ' ' + (++that.score)); that.errorCounts[panelId]++; if (that.answers[panelId] === undefined) { that.answers[panelId] = []; } that.answers[panelId].push(nodeId); } that.trigger('resize'); that.triggerXAPI('interacted'); // Trigger answered xAPI event on first try for the current // statement group if (that.userResponses[panelId].length === 1) { that.trigger(that.createXApiAnsweredEvent( that.summaries[panelId], that.userResponses[panelId] || [], panelId, that.timePassedInStopWatch(panelId))); } // Trigger overall answered xAPI event when finished if (finished) { that.triggerXAPIScored(that.getScore(), that.getMaxScore(), 'answered'); } }; // Initialize the visible and invisible progress counters $progressNumeric.html(that.options.solvedLabel + ' ' + this.progress + '/' + that.summaries.length); var interpolatedProgressText = that.options.progressText .replace(':num', that.progress) .replace(':total', that.summaries.length); $progress.html(interpolatedProgressText); // Add elements to content for (var i = 0; i < elements.length; i++) { var element = elements[i]; if (i < that.progress) { // i is panelId for (var j = 0; j < element.summaries.length; j++) { var sum = element.summaries[j]; if (that.answer[sum.id]) { $summary_list.append('<li style="display:block">' + sum.text + '</li>'); $summary_container.addClass('has-results'); break; } } // Cannot use continue; due to id/animation system } // added aria-labelledby property for readspeaker to read, when first option receive focus var $page = $('<ul aria-labelledby="questionDesc-'+that.summaryId+'" role="radiogroup" class="h5p-panel" data-panel="' + i + '"></ul>'); // Create initial tip for first summary-list if tip is available if (i==0 && element.tip !== undefined && element.tip.trim().length > 0) { $evaluation_content.append(H5P.JoubelUI.createTip(element.tip, { tipLabel: that.options.tipButtonLabel })); } for (var j = 0; j < element.summaries.length; j++) { var summaryLineClass = 'summary-claim-unclicked'; // If progress is at current task if (that.progress === i && that.answers[that.progress]) { // Check if there are any previous wrong answers. for (var k = 0; k < that.answers[that.progress].length; k++) { if (that.answers[that.progress][k] === element.summaries[j].id) { summaryLineClass = 'summary-failed'; break; } } } var $node = $('' + '<li role="radio" aria-checked="false" data-name="'+j+'" data-bit="' + element.summaries[j].id + '" class="' + summaryLineClass + '">' + element.summaries[j].text + '</li>'); // added tabindex = 0 for the first option to avoid accessing rest of the options via TAB (j == 0) ? $node.attr("tabindex", "0") : $node.attr("tabindex", "-1"); $node.on('focus', function() { var ind = $(this).attr('data-name'); setFocusIndex(ind); }); // function captures the index of currently focused option var setFocusIndex = function(idx) { currentFocusedOption = idx; }; // Do not add click event for failed nodes if (summaryLineClass === 'summary-failed') { $page.append($node); continue; } $node.click(function() { selectedAlt($(this)); }).keydown(function (e) { switch (e.which) { case 13: // Enter case 32: // Space selectedAlt($(this), true); e.preventDefault(); break; case 37: // Left Arrow case 38: // Up Arrow // Go to previous Option that.gotoPreviousOption(that, currentFocusedOption); e.preventDefault(); break; case 39: // Right Arrow case 40: // Down Arrow // Go to next Option that.gotoNextOption(that, currentFocusedOption); e.preventDefault(); break; } }); $page.append($node); } $options.append($page); } if (that.progress === elements.length) { $evaluation_content.html(that.options.resultLabel); that.doFinalEvaluation(); } else { // Show first panel $('.h5p-panel:eq(' + (that.progress) + ')', that.$myDom).css({display: 'block'}); if (that.progress) { that.offset = ($('.summary-claim-unclicked:visible:first', that.$myDom).outerHeight() * that.errorCounts.length); } } that.trigger('resize'); return this.$myDom; }; /** * Announce if answered alternative was correct or wrong * @param isCorrect */ Summary.prototype.announceAnswer = function (isCorrect) { const announcement = isCorrect ? this.options.labelCorrect : this.options.labelIncorrect; this.$answerAnnouncer.html(announcement); // Remove text so it can't be navigated to and read at a later point setTimeout(function () { this.$answerAnnouncer.html(''); }.bind(this), 100); }; /** * Returns true if answers have been given * * @return {boolean} */ Summary.prototype.getAnswerGiven = function () { return this.errorCounts.length > 0; }; /** * Handles moving the focus from the current option to the previous option and changes tabindex accorindgly * */ Summary.prototype.gotoPreviousOption = function (that, currentFocusedOption) { this.currentFocusedOption = currentFocusedOption; var totOptions = that.summaries[that.progress].summary.length; var prevRadioEle = $("ul[data-panel="+that.progress+"] li[role='radio']", this.$myDom); //prevRadioEle.removeAttr("tabindex"); prevRadioEle.attr("tabindex", "-1"); this.currentFocusedOption--; if(this.currentFocusedOption < 0) { var num = totOptions - 1; prevRadioEle.eq(num).attr("tabindex", "0"); prevRadioEle.eq(num).focus(); } else { prevRadioEle.eq(this.currentFocusedOption).attr("tabindex", "0"); prevRadioEle.eq(this.currentFocusedOption).focus(); } }; /** * Handles moving the focus from the current option to the next option and changes tabindex accorindgly * */ Summary.prototype.gotoNextOption = function (that, currentFocusedOption) { this.currentFocusedOption = currentFocusedOption; var totOptions = that.summaries[that.progress].summary.length; var nextRadioEle = $("ul[data-panel="+that.progress+"] li[role='radio']", this.$myDom); //nextRadioEle.removeAttr("tabindex"); nextRadioEle.attr("tabindex", "-1"); this.currentFocusedOption++; if(this.currentFocusedOption == totOptions) { nextRadioEle.eq(0).attr("tabindex", "0"); nextRadioEle.eq(0).focus(); } else { nextRadioEle.eq(this.currentFocusedOption).attr("tabindex", "0"); nextRadioEle.eq(this.currentFocusedOption).focus(); } }; /** * Calculate final score and display feedback. * * @param container * @param options_panel * @param list * @param score */ Summary.prototype.doFinalEvaluation = function () { var that = this; var errorCount = this.countErrors(); var maxScore = that.summaries.length; var score = maxScore - errorCount; // Calculate percentage var percent = 100 - (errorCount / that.errorCounts.length * 100); // Show final evaluation var summary = H5P.Question.determineOverallFeedback(that.options.overallFeedback, percent / 100) .replace('@score', score) .replace('@total', maxScore) .replace('@percent', Math.round(percent)); $(".summary-evaluation-content", this.$myDom).removeAttr("tabindex"); var scoreBarLabel = that.options.scoreBarLabel.replace(':num', score).replace(':total', maxScore); this.setFeedback(summary, score, maxScore, scoreBarLabel); // Only read out the score after the progress is read setTimeout(function() { that.setBehaviour({disableReadSpeaker: false}); that.readFeedback(); that.read(scoreBarLabel); }, 3000); that.trigger('resize'); }; /** * Resets the complete task back to its' initial state. * Used for contracts. */ Summary.prototype.resetTask = function () { this.offset = 0; this.score = 0; this.progress = 0; this.answers = []; this.answer = []; this.errorCounts = []; this.userResponses = []; this.dataBitMap = []; if (this.$myDom) { const contentWrapper = this.$myDom[0].parentNode; contentWrapper.innerHTML = ''; this.createQuestion(); contentWrapper.appendChild(this.$myDom[0]); this.removeFeedback(); } }; /** * Adjust height of container. * * @param container * @param elements * @param el */ Summary.prototype.adjustTargetHeight = function (container, elements, el) { var new_height = parseInt(elements.outerHeight()) + parseInt(el.outerHeight()) + parseInt(el.css('marginBottom')) + parseInt(el.css('marginTop')); if (new_height > parseInt(container.css('height'))) { container.animate({height: new_height}); } }; /** * Count amount of wrong answers * * @returns {number} */ Summary.prototype.countErrors = function() { var error_count = 0; // Count boards without errors for (var i = 0; i < this.summaries.length; i++) { if (this.errorCounts[i] === undefined) { error_count++; } else { error_count += this.errorCounts[i] ? 1 : 0; } } return error_count; }; /** * Returns the choices array for xApi statements * * @param {String[]} answers * * @return {{ choices: []}} */ Summary.prototype.getXApiChoices = function (answers) { var choices = answers.map(function(answer, index){ return XApiEventBuilder.createChoice(index.toString(), answer); }); return { choices: choices }; }; /** * Saves the user response * * @param {number} questionIndex * @param {number} answerIndex */ Summary.prototype.storeUserResponse = function (questionIndex, answerIndex) { var self = this; if(self.userResponses[questionIndex] === undefined){ self.userResponses[questionIndex] = []; } self.userResponses[questionIndex].push(this.dataBitMap[questionIndex][answerIndex]); }; /** * Starts a stopwatch for indexed slide * * @param {number} index */ Summary.prototype.startStopWatch = function (index) { this.stopWatches[index] = this.stopWatches[index] || new StopWatch(); this.stopWatches[index].start(); }; /** * Stops a stopwatch for indexed slide * * @param {number} [index] */ Summary.prototype.stopStopWatch = function (index) { if(this.stopWatches[index]){ this.stopWatches[index].stop(); } }; /** * Returns the passed time in seconds of a stopwatch on an indexed slide, * or 0 if not existing * * @param {number} index * @return {number} */ Summary.prototype.timePassedInStopWatch = function (index) { if(this.stopWatches[index] !== undefined){ return this.stopWatches[index].passedTime(); } else { // if not created, return no passed time, return 0; } }; /** * Returns the time the user has spent on all questions so far * * @return {number} */ Summary.prototype.getTotalPassedTime = function () { return this.stopWatches .filter(function(watch){ return watch !== undefined; }) .reduce(function(sum, watch){ return sum + watch.passedTime(); }, 0); }; /** * Creates an xAPI answered event for a single statement list * * @param {object} panel * @param {number[]} userAnswer * @param {number} panelIndex * @param {number} duration * * @return {H5P.XAPIEvent} */ Summary.prototype.createXApiAnsweredEvent = function (panel, userAnswer, panelIndex, duration) { var self = this; // creates the definition object var definition = XApiEventBuilder.createDefinition() .name('Summary statement') .description(self.options.intro) .interactionType(XApiEventBuilder.interactionTypes.CHOICE) .correctResponsesPattern(['0']) .optional(self.getXApiChoices(panel.summary)) .build(); // create the result object var result = XApiEventBuilder.createResult() .response(userAnswer.join('[,]')) .duration(duration) .score((self.correctOnFirstTry(userAnswer) ? 1 : 0), 1) .build(); return XApiEventBuilder.create() .verb(XApiEventBuilder.verbs.ANSWERED) .objectDefinition(definition) .context(self.contentId, self.subContentId) .contentId(self.contentId, panel.subContentId) .result(result) .build(); }; Summary.prototype.correctOnFirstTry = function(userAnswer){ return (userAnswer.length === 1) && userAnswer[0] === 0; }; /** * Retrieves the xAPI data necessary for generating result reports. * * @return {object} */ Summary.prototype.getXAPIData = function(){ var self = this; // create array with userAnswer var children = self.summaries.map(function(panel, index) { var userResponse = self.userResponses[index] || []; var duration = self.timePassedInStopWatch(index); var event = self.createXApiAnsweredEvent(panel, userResponse, index, duration); return { statement: event.data.statement }; }); var result = XApiEventBuilder.createResult() .score(self.getScore(), self.getMaxScore()) .duration(self.getTotalPassedTime()) .build(); // creates the definition object var definition = XApiEventBuilder.createDefinition() .interactionType(XApiEventBuilder.interactionTypes.COMPOUND) .name(self.getTitle()) .description(self.options.intro) .build(); var xAPIEvent = XApiEventBuilder.create() .verb(XApiEventBuilder.verbs.ANSWERED) .contentId(self.contentId, self.subContentId) .context(self.getParentAttribute('contentId'), self.getParentAttribute('subContentId')) .objectDefinition(definition) .result(result) .build(); return { statement: xAPIEvent.data.statement, children: children }; }; /** * Returns an attribute from this.parent if it exists * * @param {string} attributeName * @return {*|undefined} */ Summary.prototype.getParentAttribute = function (attributeName) { var self = this; if(self.parent !== undefined){ return self.parent[attributeName]; } }; return Summary; })(H5P.jQuery, H5P.Question, H5P.Summary.XApiEventBuilder, H5P.Summary.StopWatch);
CANCEL
Name
Type
Size
Modified
Actions
↩ ..
DIR
—
—
📄 2b52c5354c4a015ae440deeeace2b8ccb02f3f88
?
27.9 KB
2025-02-01 12:33
EDIT