���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/moodledata/filedir/1c/b5/1cb57a2ae880c2ec6ef1f10b1268d96b3ec4b193
���ѧ٧ѧ�
var H5P = H5P || {}; H5P.SingleChoiceSet = (function ($, UI, Question, SingleChoice, SolutionView, ResultSlide, SoundEffects, XApiEventBuilder, StopWatch) { /** * @constructor * @extends Question * @param {object} options Options for single choice set * @param {string} contentId H5P instance id * @param {Object} contentData H5P instance data */ function SingleChoiceSet(options, contentId, contentData) { var self = this; // Extend defaults with provided options this.contentId = contentId; this.contentData = contentData; /** * The users input on the questions. Uses the same index as this.options.choices * @type {number[]} */ this.userResponses = []; Question.call(this, 'single-choice-set'); this.options = $.extend(true, {}, { choices: [], overallFeedback: [], behaviour: { autoContinue: true, timeoutCorrect: 2000, timeoutWrong: 3000, soundEffectsEnabled: true, enableRetry: true, enableSolutionsButton: true, passPercentage: 100 } }, options); if (contentData && contentData.previousState !== undefined) { this.currentIndex = contentData.previousState.progress; this.results = contentData.previousState.answers; this.userResponses = contentData.previousState.userResponses !== undefined ? contentData.previousState.userResponses : []; } this.currentIndex = this.currentIndex || 0; this.results = this.results || { corrects: 0, wrongs: 0 }; if (!this.options.behaviour.autoContinue) { this.options.behaviour.timeoutCorrect = 0; this.options.behaviour.timeoutWrong = 0; } /** * @property {StopWatch[]} Stop watches for tracking duration of slides */ this.stopWatches = []; this.startStopWatch(this.currentIndex); this.muted = (this.options.behaviour.soundEffectsEnabled === false); this.l10n = H5P.jQuery.extend({ correctText: 'Correct!', incorrectText: 'Incorrect!', shouldSelect: "Should have been selected", shouldNotSelect: "Should not have been selected", nextButtonLabel: 'Next question', showSolutionButtonLabel: 'Show solution', retryButtonLabel: 'Retry', closeButtonLabel: 'Close', solutionViewTitle: 'Solution', slideOfTotal: 'Slide :num of :total', muteButtonLabel: "Mute feedback sound", scoreBarLabel: 'You got :num out of :total points', solutionListQuestionNumber: 'Question :num', a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.', a11yRetry: 'Retry the task. Reset all responses and start the task over again.', }, options.l10n !== undefined ? options.l10n : {}); this.$container = $('<div>', { 'class': 'h5p-sc-set-wrapper navigatable' + (!this.options.behaviour.autoContinue ? ' next-button-mode' : '') }); this.$slides = []; // An array containing the SingleChoice instances this.choices = []; /** * Keeps track of buttons that will be hidden * @type {Array} */ self.buttonsToBeHidden = []; /** * The solution dialog * @type {SolutionView} */ this.solutionView = new SolutionView(contentId, this.options.choices, this.l10n); this.$choices = $('<div>', { 'class': 'h5p-sc-set h5p-sc-animate' }); // sometimes an empty object is in the choices this.options.choices = this.options.choices.filter(function (choice) { return choice !== undefined && !!choice.answers; }); var numQuestions = this.options.choices.length; // Create progressbar self.progressbar = UI.createProgressbar(numQuestions + 1, { progressText: this.l10n.slideOfTotal }); self.progressbar.setProgress(this.currentIndex); for (var i = 0; i < this.options.choices.length; i++) { var choice = new SingleChoice(this.options.choices[i], i, this.contentId, this.options.behaviour.autoContinue); choice.on('finished', this.handleQuestionFinished, this); choice.on('alternative-selected', this.handleAlternativeSelected, this); choice.appendTo(this.$choices, (i === this.currentIndex)); this.choices.push(choice); this.$slides.push(choice.$choice); } this.resultSlide = new ResultSlide(this.options.choices.length); this.resultSlide.appendTo(this.$choices); this.resultSlide.on('retry', function() { self.resetTask(true); }, this); this.resultSlide.on('view-solution', this.handleViewSolution, this); this.$slides.push(this.resultSlide.$resultSlide); this.on('resize', this.resize, this); // Use the correct starting slide this.recklessJump(this.currentIndex); if (this.options.choices.length === this.currentIndex) { // Make sure results slide is displayed this.resultSlide.$resultSlide.addClass('h5p-sc-current-slide'); this.setScore(this.results.corrects, true); } if (!this.muted) { setTimeout(function () { SoundEffects.setup(self.getLibraryFilePath('')); }, 1); } /** * Override Question's hideButton function * to be able to hide buttons after delay * * @override * @param {string} id */ this.superHideButton = self.hideButton; this.hideButton = (function () { return function (id) { if (!self.scoreTimeout) { return self.superHideButton(id); } self.buttonsToBeHidden.push(id); return this; }; })(); } SingleChoiceSet.prototype = Object.create(Question.prototype); SingleChoiceSet.prototype.constructor = SingleChoiceSet; /** * Set if a element is tabbable or not * * @param {jQuery} $element The element * @param {boolean} tabbable If element should be tabbable * @returns {jQuery} The element */ SingleChoiceSet.prototype.setTabbable = function ($element, tabbable) { if ($element) { $element.attr('tabindex', tabbable ? 0 : -1); } }; /** * Handle alternative selected, i.e play sound if sound effects are enabled * * @method handleAlternativeSelected * @param {Object} event Event that was fired */ SingleChoiceSet.prototype.handleAlternativeSelected = function (event) { var self = this; this.lastAnswerIsCorrect = event.data.correct; self.toggleNextButton(true); // Keep track of num correct/wrong answers this.results[this.lastAnswerIsCorrect ? 'corrects' : 'wrongs']++; self.triggerXAPI('interacted'); // Read and set a11y friendly texts self.readA11yFriendlyText(event.data.index, event.data.currentIndex) if (!this.muted) { // Can't play it after the transition end is received, since this is not // accepted on iPad. Therefore we are playing it here with a delay instead SoundEffects.play(this.lastAnswerIsCorrect ? 'positive-short' : 'negative-short', 700); } }; /** * Handler invoked when question is done * * @param {object} event An object containing a single boolean property: "correct". */ SingleChoiceSet.prototype.handleQuestionFinished = function (event) { var self = this; var index = event.data.index; // saves user response var userResponse = self.userResponses[index] = event.data.answerIndex; // trigger answered event var duration = this.stopStopWatch(index); var xapiEvent = self.createXApiAnsweredEvent(self.options.choices[index], userResponse, duration); self.trigger(xapiEvent); self.continue(index); }; /** * Setup auto continue */ SingleChoiceSet.prototype.continue = function (index) { var self = this; self.choices[index].setA11yTextReadable(); if (!self.options.behaviour.autoContinue) { // Set focus to next button self.$nextButton.focus(); return; } var timeout; var letsMove = function () { // Handle impatient users self.$container.off('click.impatient keydown.impatient'); clearTimeout(timeout); self.next(); }; timeout = setTimeout(function () { letsMove(); }, self.lastAnswerIsCorrect ? self.options.behaviour.timeoutCorrect : self.options.behaviour.timeoutWrong); self.onImpatientUser(letsMove); }; /** * Listen to impatience * @param {Function} action Callback */ SingleChoiceSet.prototype.onImpatientUser = function (action) { this.$container.off('click.impatient keydown.impatient'); this.$container.one('click.impatient', action); this.$container.one('keydown.impatient', function (event) { // If return, space or right arrow if ([13,32,39].indexOf(event.which)) { action(); } }); }; /** * Go to next slide */ SingleChoiceSet.prototype.next = function () { this.move(this.currentIndex + 1); }; /** * Creates an xAPI answered event * * @param {object} question * @param {number} userAnswer * @param {number} duration * * @return {H5P.XAPIEvent} */ SingleChoiceSet.prototype.createXApiAnsweredEvent = function (question, userAnswer, duration) { var self = this; var types = XApiEventBuilder.interactionTypes; // creates the definition object var definition = XApiEventBuilder.createDefinition() .interactionType(types.CHOICE) .description(question.question) .correctResponsesPattern(self.getXApiCorrectResponsePattern()) .optional( self.getXApiChoices(question.answers)) .build(); // create the result object var result = XApiEventBuilder.createResult() .response(userAnswer.toString()) .duration(duration) .score((userAnswer === 0) ? 1 : 0, 1) .completion(true) .success(userAnswer === 0) .build(); return XApiEventBuilder.create() .verb(XApiEventBuilder.verbs.ANSWERED) .objectDefinition(definition) .context(self.contentId, self.subContentId) .contentId(self.contentId, question.subContentId) .result(result) .build(); }; /** * Returns the 'correct response pattern' for xApi * * @return {string[]} */ SingleChoiceSet.prototype.getXApiCorrectResponsePattern = function () { return [XApiEventBuilder.createCorrectResponsePattern([(0).toString()])]; // is always '0' for SCS }; /** * Returns the choices array for xApi statements * * @param {String[]} answers * * @return {{ choices: []}} */ SingleChoiceSet.prototype.getXApiChoices = function (answers) { var choices = answers.map(function (answer, index) { return XApiEventBuilder.createChoice(index.toString(), answer); }); return { choices: choices }; }; /** * Handles buttons that are queued for hiding */ SingleChoiceSet.prototype.handleQueuedButtonChanges = function () { var self = this; if (self.buttonsToBeHidden.length) { self.buttonsToBeHidden.forEach(function (id) { self.superHideButton(id); }); } self.buttonsToBeHidden = []; }; /** * Set score and feedback * * @params {Number} score Number of correct answers */ SingleChoiceSet.prototype.setScore = function (score, noXAPI) { var self = this; if (!self.choices.length) { return; } var feedbackText = determineOverallFeedback(self.options.overallFeedback , score / self.options.choices.length) .replace(':numcorrect', score) .replace(':maxscore', self.options.choices.length.toString()); self.setFeedback(feedbackText, score, self.options.choices.length, self.l10n.scoreBarLabel); if (score === self.options.choices.length) { self.hideButton('try-again'); self.hideButton('show-solution'); } else { self.showButton('try-again'); self.showButton('show-solution'); } self.handleQueuedButtonChanges(); self.scoreTimeout = undefined; if (!noXAPI) { self.triggerXAPIScored(score, self.options.choices.length, 'completed', true, (100 * score / self.options.choices.length) >= self.options.behaviour.passPercentage); } self.trigger('resize'); }; /** * Handler invoked when view solution is selected */ SingleChoiceSet.prototype.handleViewSolution = function () { var self = this; var $tryAgainButton = $('.h5p-question-try-again', self.$container); var $showSolutionButton = $('.h5p-question-show-solution', self.$container); var buttons = [self.$muteButton, $tryAgainButton, $showSolutionButton]; // remove tabbable for buttons in result view buttons.forEach(function (button) { self.setTabbable(button, false); }); self.solutionView.on('hide', function () { // re-add tabbable for buttons in result view buttons.forEach(function (button) { self.setTabbable(button, true); }); self.toggleAriaVisibility(true); // Focus on first button when closing solution view self.focusButton(); }); self.solutionView.show(); self.toggleAriaVisibility(false); }; /** * Toggle elements visibility to Assistive Technologies * * @param {boolean} enable Make elements visible */ SingleChoiceSet.prototype.toggleAriaVisibility = function (enable) { var self = this; var ariaHidden = enable ? '' : 'true'; if (self.$muteButton) { self.$muteButton.attr('aria-hidden', ariaHidden); } self.progressbar.$progressbar.attr('aria-hidden', ariaHidden); self.$choices.attr('aria-hidden', ariaHidden); }; /** * Register DOM elements before they are attached. * Called from H5P.Question. */ SingleChoiceSet.prototype.registerDomElements = function () { // Register task content area. this.setContent(this.createQuestion()); // Register buttons with question. this.addButtons(); // Insert feedback and buttons section on the result slide this.insertSectionAtElement('feedback', this.resultSlide.$feedbackContainer); this.insertSectionAtElement('scorebar', this.resultSlide.$feedbackContainer); this.insertSectionAtElement('buttons', this.resultSlide.$buttonContainer); // Question is finished if (this.options.choices.length === this.currentIndex) { this.trigger('question-finished'); } this.trigger('resize'); }; /** * Add Buttons to question. */ SingleChoiceSet.prototype.addButtons = function () { var self = this; if (this.options.behaviour.enableRetry) { this.addButton('try-again', this.l10n.retryButtonLabel, function () { self.resetTask(true); }, self.results.corrects !== self.options.choices.length, { 'aria-label': this.l10n.a11yRetry, }); } if (this.options.behaviour.enableSolutionsButton) { this.addButton('show-solution', this.l10n.showSolutionButtonLabel, function () { self.showSolutions(); }, self.results.corrects !== self.options.choices.length, { 'aria-label': this.l10n.a11yShowSolution, }); } }; /** * Create main content */ SingleChoiceSet.prototype.createQuestion = function () { var self = this; self.progressbar.appendTo(self.$container); self.$container.append(self.$choices); function toggleMute(event) { var $button = $(event.target); event.preventDefault(); self.muted = !self.muted; $button.attr('aria-pressed', self.muted); } // Keep this out of H5P.Question, since we are moving the button & feedback // region to the last slide if (!this.options.behaviour.autoContinue) { var handleNextClick = function () { if (self.$nextButton.attr('aria-disabled') !== 'true') { self.next(); } }; self.$nextButton = UI.createButton({ 'class': 'h5p-ssc-next-button', 'aria-label': self.l10n.nextButtonLabel, click: handleNextClick, keydown: function (event) { switch (event.which) { case 13: // Enter case 32: // Space handleNextClick(); event.preventDefault(); } }, appendTo: self.$container }); self.toggleNextButton(false); } if (self.options.behaviour.soundEffectsEnabled) { self.$muteButton = $('<div>', { 'class': 'h5p-sc-sound-control', 'tabindex': 0, 'role': 'button', 'aria-label': self.l10n.muteButtonLabel, 'aria-pressed': false, 'on': { 'keydown': function (event) { switch (event.which) { case 13: // Enter case 32: // Space toggleMute(event); break; } } }, 'click': toggleMute, prependTo: self.$container }); } // Append solution view - hidden by default: self.solutionView.appendTo(self.$container); self.resize(); // Hide all other slides than the current one: self.$container.addClass('initialized'); return self.$container; }; /** * Resize if something outside resizes */ SingleChoiceSet.prototype.resize = function () { var self = this; var maxHeight = 0; self.choices.forEach(function (choice) { var choiceHeight = choice.$choice.outerHeight(); maxHeight = choiceHeight > maxHeight ? choiceHeight : maxHeight; }); // Set minimum height for choices self.$choices.css({minHeight: maxHeight + 'px'}); }; /** * Disable/enable the next button * @param {boolean} enable */ SingleChoiceSet.prototype.toggleNextButton = function (enable) { if (this.$nextButton) { this.$nextButton.attr('aria-disabled', !enable); } }; /** * Will jump to the given slide without any though to animations, * current slide etc. * * @public */ SingleChoiceSet.prototype.recklessJump = function (index) { var tX = 'translateX(' + (-index * 100) + '%)'; this.$choices.css({ '-webkit-transform': tX, '-moz-transform': tX, '-ms-transform': tX, 'transform': tX }); this.progressbar.setProgress(index + 1); }; /** * Move to slide n * @param {number} index The slide number to move to * @param {boolean} moveFocus True to set focus on first alternative */ SingleChoiceSet.prototype.move = function (index, moveFocus = true) { var self = this; if (index === this.currentIndex || index > self.$slides.length-1) { return; } var $previousSlide = self.$slides[self.currentIndex]; var $currentChoice = self.choices[index]; var $currentSlide = self.$slides[index]; var isResultSlide = (index >= self.choices.length); self.toggleNextButton(false); H5P.Transition.onTransitionEnd(self.$choices, function () { $previousSlide.removeClass('h5p-sc-current-slide'); // on slides with answers focus on first alternative // if content is root and not on result slide - always move focus if (!isResultSlide && (moveFocus || self.isRoot())) { $currentChoice.focusOnAlternative(0); } // on last slide, focus on try again button else { self.resultSlide.focusScore(); } }, 600); // if should show result slide if (isResultSlide) { self.setScore(self.results.corrects); } self.$container.toggleClass('navigatable', !isResultSlide); // start timing of new slide this.startStopWatch(index); // move to slide $currentSlide.addClass('h5p-sc-current-slide'); self.recklessJump(index); self.currentIndex = index; }; /** * Starts a stopwatch for indexed slide * * @param {number} index */ SingleChoiceSet.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 */ SingleChoiceSet.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} */ SingleChoiceSet.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} */ SingleChoiceSet.prototype.getTotalPassedTime = function () { return this.stopWatches .filter(function (watch) { return watch != undefined; }) .reduce(function (sum, watch) { return sum + watch.passedTime(); }, 0); }; /** * The following functions implements the CP and IV - Contracts v 1.0 documented here: * http://h5p.org/node/1009 */ SingleChoiceSet.prototype.getScore = function () { return this.results.corrects; }; SingleChoiceSet.prototype.getMaxScore = function () { return this.options.choices.length; }; SingleChoiceSet.prototype.getAnswerGiven = function () { return (this.results.corrects + this.results.wrongs) > 0; }; SingleChoiceSet.prototype.getTitle = function () { return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Single Choice Set'); }; /** * Retrieves the xAPI data necessary for generating result reports. * * @return {object} */ SingleChoiceSet.prototype.getXAPIData = function () { var self = this; // create array with userAnswer var children = self.options.choices.map(function (question, index) { var userResponse = self.userResponses[index] >= 0 ? self.userResponses[index] : ''; var duration = self.timePassedInStopWatch(index); var event = self.createXApiAnsweredEvent(question, userResponse, 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) .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} */ SingleChoiceSet.prototype.getParentAttribute = function (attributeName) { var self = this; if (self.parent !== undefined) { return self.parent[attributeName]; } }; SingleChoiceSet.prototype.showSolutions = function () { this.handleViewSolution(); }; /** * Reset all answers. This is equal to refreshing the quiz * @param {boolean} moveFocus True to move the focus * This prevents loss of focus if reset from within content */ SingleChoiceSet.prototype.resetTask = function (moveFocus = false) { var self = this; // Close solution view if visible: this.solutionView.hide(); // Reset the user's answers var classes = ['h5p-sc-reveal-wrong', 'h5p-sc-reveal-correct', 'h5p-sc-selected', 'h5p-sc-drummed', 'h5p-sc-correct-answer']; for (var i = 0; i < classes.length; i++) { this.$choices.find('.' + classes[i]).removeClass(classes[i]); } this.results = { corrects: 0, wrongs: 0 }; this.choices.forEach(function (choice) { choice.setAnswered(false); choice.resetA11yText(); choice.resetAriaAttributes(); }); this.stopWatches.forEach(function (stopWatch) { if (stopWatch) { stopWatch.reset(); } }); this.move(0, moveFocus); // Reset userResponses as well this.userResponses = []; // Wait for transition, then remove feedback. H5P.Transition.onTransitionEnd(this.$choices, function () { self.removeFeedback(); }, 600); }; /** * Clever comment. * * @public * @returns {object} */ SingleChoiceSet.prototype.getCurrentState = function () { return this.userResponses.length > 0 ? { progress: this.currentIndex, answers: this.results, userResponses: this.userResponses } : undefined; }; /** * Generate A11y friendly text * * @param {number} index * @param {number} currentIndex */ SingleChoiceSet.prototype.readA11yFriendlyText = function (index, currentIndex) { var self = this; var correctAnswer = self.$choices.find('.h5p-sc-is-correct')[index].textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim(); let selectedOptionText = this.lastAnswerIsCorrect ? self.l10n.correctText : self.l10n.incorrectText; // Announce by ARIA label if (!self.options.behaviour.autoContinue) { // Set text for a11y selectedOptionText = this.lastAnswerIsCorrect ? self.l10n.correctText + self.l10n.shouldSelect : self.l10n.incorrectText + self.l10n.shouldNotSelect; self.$choices.find('.h5p-sc-current-slide .h5p-sc-is-correct .h5p-sc-a11y').text(self.l10n.shouldSelect); self.$choices.find('.h5p-sc-current-slide .h5p-sc-is-wrong .h5p-sc-a11y').text(self.l10n.shouldNotSelect); self.$choices.find('.h5p-sc-current-slide .h5p-sc-alternative').eq(currentIndex).find('.h5p-sc-a11y').text(selectedOptionText); // Utilize same variable for the read text selectedOptionText = this.lastAnswerIsCorrect ? self.l10n.correctText : self.l10n.incorrectText + correctAnswer + self.l10n.shouldSelect; } self.read(selectedOptionText); }; /** * Determine the overall feedback to display for the question. * Returns empty string if no matching range is found. * * @param {Object[]} feedbacks * @param {number} scoreRatio * @return {string} */ var determineOverallFeedback = function (feedbacks, scoreRatio) { scoreRatio = Math.floor(scoreRatio * 100); for (var i = 0; i < feedbacks.length; i++) { var feedback = feedbacks[i]; var hasFeedback = (feedback.feedback !== undefined && feedback.feedback.trim().length !== 0); if (feedback.from <= scoreRatio && feedback.to >= scoreRatio && hasFeedback) { return feedback.feedback; } } return ''; }; return SingleChoiceSet; })(H5P.jQuery, H5P.JoubelUI, H5P.Question, H5P.SingleChoiceSet.SingleChoice, H5P.SingleChoiceSet.SolutionView, H5P.SingleChoiceSet.ResultSlide, H5P.SingleChoiceSet.SoundEffects, H5P.SingleChoiceSet.XApiEventBuilder, H5P.SingleChoiceSet.StopWatch);
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�