@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
447 lines (428 loc) • 18.9 kB
JavaScript
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;
});