UNPKG

@oat-sa/tao-test-runner-qti

Version:
447 lines (428 loc) 18.9 kB
define(['jquery', 'i18n', 'handlebars', 'lib/handlebars/helpers', 'ui/component', 'interact', 'ui/component/stackable', 'ui/component/placeable', 'ui/feedback', 'nouislider'], function ($$1, __, Handlebars, Helpers0, component, interact, makeStackable, makePlaceable, feedback, nouislider) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; component = component && Object.prototype.hasOwnProperty.call(component, 'default') ? component['default'] : component; interact = interact && Object.prototype.hasOwnProperty.call(interact, 'default') ? interact['default'] : interact; makeStackable = makeStackable && Object.prototype.hasOwnProperty.call(makeStackable, 'default') ? makeStackable['default'] : makeStackable; makePlaceable = makePlaceable && Object.prototype.hasOwnProperty.call(makePlaceable, 'default') ? makePlaceable['default'] : makePlaceable; feedback = feedback && Object.prototype.hasOwnProperty.call(feedback, 'default') ? feedback['default'] : feedback; if (!Helpers0.__initialized) { Helpers0(Handlebars); Helpers0.__initialized = true; } var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) { this.compilerInfo = [4,'>= 1.0.0']; helpers = this.merge(helpers, Handlebars.helpers); data = data || {}; var buffer = "", helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression; buffer += "<div class=\"tts-container\">\n <div class=\"tts-controls\">\n <div class=\"tts-control-container\">\n <a class=\"tts-control tts-control-close\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Close", options) : helperMissing.call(depth0, "__", "Close", options))) + "\">\n <span class=\"icon-result-nok tts-icon\"></span>\n </a>\n </div>\n <div class=\"tts-control-container\">\n <a class=\"tts-control tts-control-drag\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Move", options) : helperMissing.call(depth0, "__", "Move", options))) + "\">\n <span class=\"icon-grip tts-icon\"></span>\n <span class=\"tts-control-label\">\n " + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Text to Speech", options) : helperMissing.call(depth0, "__", "Text to Speech", options))) + "\n </span>\n </a>\n </div>\n <div class=\"tts-control-container\">\n <a class=\"tts-control tts-control-playback\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Toggle playback", options) : helperMissing.call(depth0, "__", "Toggle playback", options))) + "\">\n <span class=\"icon-play tts-icon\"></span>\n <span class=\"icon-pause tts-icon\"></span>\n </a>\n </div>\n <div class=\"tts-control-container\">\n <a class=\"tts-control tts-control-mode\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Toggle start from here mode", options) : helperMissing.call(depth0, "__", "Toggle start from here mode", options))) + "\">\n <span class=\"icon-play-from-here tts-icon\"></span>\n </a>\n </div>\n <div class=\"tts-control-container\">\n <a class=\"tts-control tts-control-settings\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Settings", options) : helperMissing.call(depth0, "__", "Settings", options))) + "\">\n <span class=\"icon-property-advanced tts-icon\"></span>\n </a>\n <div class=\"tts-slider-container\">\n " + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Speed", options) : helperMissing.call(depth0, "__", "Speed", options))) + "<div class=\"tts-slider\"></div>\n </div>\n </div>\n </div>\n</div>\n"; return buffer; }); function ttsTemplate(data, options, asString) { var html = Template(data, options); return (asString || true) ? html : $(html); } /** * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2019 (original work) Open Assessment Technologies SA; */ const defaultConfig = { activeElementClass: 'tts-active-content-node', elementClass: 'tts-content-node', left: -10, maxPlaybackRate: 2, minPlaybackRate: 0.5, playbackRate: 1, top: 50 }; const stackingOptions = { stackingScope: 'test-runner' }; /** * Creates an instance of Text to Speech component * * @param {Element} container * @param {Object} config - component configurations * @param {String} config.activeElementClass - class applied to active content element. Default value 'tts-active-content-node' * @param {String} config.elementClass - class applied to content element. Default value 'tts-content-node' * @param {Number} config.left - initial left position of component. Default value 50 * @param {Number} config.maxPlaybackRate - max playback rate. Default value 2 * @param {Number} config.minPlaybackRate - min playback rate. Default value 0.5 * @param {Number} config.playbackRate - playback rate. Default value 1 * @param {Number} config.top - initial top position of component. Default value 50 * @returns {ttsComponent} the textToSpeech component (uninitialized) */ function maskingComponentFactory(container, config) { const audio = new Audio(); let currentPlayback = []; let currentItem; let mediaContentData = []; let playbackRate; // Browser does not support selection Api If getSelection is not defined const selection = window.getSelection && window.getSelection(); // component API const spec = { /** * Remove APIP element class and click handlers from APIP elements */ clearAPIPElements() { const { elementClass } = this.config; const $contentNodes = $$1(mediaContentData.map(_ref => { let { selector } = _ref; return selector; }).join(', '), container); $contentNodes.removeClass(elementClass); $contentNodes.off('click', this.handleContentNodeClick); }, /** * Update componet state and stop playback * * @fires close */ close() { this.setTTSStateOnContainer('playing', false); this.setTTSStateOnContainer('sfhMode', false); this.setState('settings', false); this.stop(); this.trigger('close'); }, /** * Get current active APIP item * * @returns {Object} active APIP item */ getCurrentItem() { return currentItem; }, /** * When component in start from here mode, switch to clicked content element * * @param {Object} e - event object */ handleContentNodeClick(e) { const $target = $$1(e.target); // Allow default behaviour for inputs if ($target.hasClass('icon-checkbox') || $target.hasClass('icon-radio') || $target.is('input')) { return; } // Prevent default behaviour for lables and links e.stopPropagation(); e.preventDefault(); if (!this.is('sfhMode')) { return; } const $currentTarget = $$1(e.currentTarget); // Find APIP item associated with clicked element const selectedItemIndex = mediaContentData.findIndex(_ref2 => { let { selector } = _ref2; return $currentTarget.is(selector); }); currentPlayback = mediaContentData.slice(selectedItemIndex); this.stop(); this.initNextItem(); this.togglePlayback(); }, /** * Select APIP item for default mode */ initDefaultModeItem() { this.initItemWithTextSelection(); if (!currentItem) { this.initDefaultModePlayback(); } }, /** * Check if there is some selected content inside APIP elelemts on the page */ initItemWithTextSelection() { // Check if there is selected content if (this.is('sfhMode') || !selection || !selection.toString()) { return; } // Get APIP item by current selection const currentSelection = selection.getRangeAt(0); const { commonAncestorContainer } = currentSelection; const selectedItem = mediaContentData.find(_ref3 => { let { selector } = _ref3; const $item = $$1(selector, container); return $item.is(commonAncestorContainer) || $$1.contains($item[0], commonAncestorContainer); }); if (selectedItem && selectedItem !== currentItem) { currentPlayback = [selectedItem]; this.initNextItem(); } }, /** * Check if there is next APIP item to play and start playback if component in playing state. * If there is no APIP item to play stop playback * * @fires finish * @fires next */ initNextItem() { const { activeElementClass } = this.config; currentItem && $$1(currentItem.selector, container).removeClass(activeElementClass); currentItem = currentPlayback.shift(); if (currentItem) { const { selector, url } = currentItem; $$1(selector, container).addClass(activeElementClass); audio.setAttribute('src', url); audio.load(); audio.playbackRate = playbackRate; if (this.is('playing')) { audio.play(); } this.trigger('next'); return; } this.trigger('finish'); this.stop(); }, /** * Init default mode playback */ initDefaultModePlayback() { currentPlayback = [...mediaContentData]; this.initNextItem(); }, /** * Set APIP data. Apply handlers to APIP elements. Stop current playback * * @param {Array} data - APIP data items */ setMediaContentData(data) { this.clearAPIPElements(); const { elementClass } = this.config; mediaContentData = data; const $contentNodes = $$1(mediaContentData.map(_ref4 => { let { selector } = _ref4; return selector; }).join(', '), container); $contentNodes.addClass(elementClass); $contentNodes.on('click', this.handleContentNodeClick); this.stop(); }, /** * Set playback rate * * @param {Object} e - event object * @param {Number} value - playback rate */ setPlaybackRate(e, value) { playbackRate = value; audio.playbackRate = value; }, /** * Update component state. Toggle state class on page body * * @param {String} name * @param {Boolean} value */ setTTSStateOnContainer(name, value) { this.setState(name, value); $$1(container).toggleClass(`tts-${name}`, value); }, /** * Pause playback and update component state. Set current item to null */ stop() { const { activeElementClass } = this.config; audio.pause(); audio.currentTime = 0; currentItem && $$1(currentItem.selector, container).removeClass(activeElementClass); currentItem = null; this.setTTSStateOnContainer('playing', false); }, /** * Toggle playback * * @param {Object} e - event object */ togglePlayback(e) { e && e.preventDefault(); const isPlaying = this.is('playing'); this.initDefaultModeItem(); if (!isPlaying && currentItem) { audio.play(); this.setTTSStateOnContainer('playing', true); } else { audio.pause(); this.setTTSStateOnContainer('playing', false); } }, /** * Toggle start from here mode */ toggleSFHMode() { const isSFHMode = this.is('sfhMode'); this.setTTSStateOnContainer('sfhMode', !isSFHMode); this.stop(); }, /** * Toggle settings element */ toggleSettings() { const isSettings = this.is('settings'); this.setState('settings', !isSettings); // if settings was enabled make sure that component still inside the container if (!isSettings) { this.handleResize(); } }, /** * Handle browser resize */ handleResize() { // offset from right const offsetFromRight = 10; const { x, y } = this.getPosition(); const maxXPosition = window.innerWidth - this.getElement().width() - offsetFromRight; this.moveTo(x > maxXPosition ? maxXPosition : x, y); } }; const ttsComponent = component(spec, defaultConfig); makePlaceable(ttsComponent); makeStackable(ttsComponent, stackingOptions); ttsComponent.setTemplate(ttsTemplate).on('init', function () { if (container.hasClass('tts-component-container')) { throw new Error('Container already has assigned text to speech component'); } container.addClass('tts-component-container'); this.render(container); }).on('render', function () { let { left, maxPlaybackRate, minPlaybackRate, playbackRate: defaultPlaybackRate, top } = this.getConfig(); if (left < 0) { left = window.innerWidth - this.getElement().width() + left; } const $element = this.getElement(); const $closeElement = $$1('.tts-control-close', $element); const $dragElement = $$1('.tts-control-drag', $element); const $playbackElement = $$1('.tts-control-playback', $element); const $sfhModeElement = $$1('.tts-control-mode', $element); const $sliderElement = $$1('.tts-slider', $element); const $settingsElement = $$1('.tts-control-settings', $element); playbackRate = defaultPlaybackRate; $element.css('touch-action', 'none'); // make component dragable const interactElement = interact($element).draggable({ autoScroll: true, manualStart: true, restrict: { restriction: container[0], elementRect: { left: 0, right: 1, top: 0, bottom: 1 } }, onmove: event => { const xOffset = Math.round(event.dx), yOffset = Math.round(event.dy); this.moveBy(xOffset, yOffset); } }); interact($dragElement[0]).on('down', event => { const interaction = event.interaction; interaction.start({ name: 'drag' }, interactElement, $element[0]); }); // initialise slider $sliderElement.noUiSlider({ animate: true, connected: true, range: { min: minPlaybackRate, max: maxPlaybackRate }, start: defaultPlaybackRate, step: 0.1 }).on('change', this.setPlaybackRate); // handle controls $closeElement.on('click', this.close); // handle mousedown instead of click to prevent selection lose $playbackElement.on('mousedown touchstart', this.togglePlayback); $sfhModeElement.on('click', this.toggleSFHMode); $settingsElement.on('click', this.toggleSettings); audio.addEventListener('ended', this.initNextItem); audio.addEventListener('error', () => { feedback().error(__('Can not playback media file!')); this.initNextItem(); }); window.addEventListener('resize', this.handleResize); // move to initial position this.moveTo(left, top); }).on('hide', function () { this.setTTSStateOnContainer('visible', false); }).on('show', function () { this.setTTSStateOnContainer('visible', true); }).on('destroy', function () { container.removeClass('tts-component-container'); this.clearAPIPElements(); this.stop(); window.removeEventListener('resize', this.handleResize); }); ttsComponent.init(config); return ttsComponent; } return maskingComponentFactory; });