UNPKG

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

Version:
679 lines (625 loc) 24.4 kB
define(['jquery', 'lodash', 'ui/component', 'handlebars', 'lib/handlebars/helpers', 'ui/dynamicComponent'], function ($$1, _, component, Handlebars, Helpers0, dynamicComponent) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; component = component && Object.prototype.hasOwnProperty.call(component, 'default') ? component['default'] : component; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; dynamicComponent = dynamicComponent && Object.prototype.hasOwnProperty.call(dynamicComponent, 'default') ? dynamicComponent['default'] : dynamicComponent; 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 = "", stack1, helper, options, functionType="function", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing; buffer += "<div class=\"magnifier\">\n <div class=\"level\">"; if (helper = helpers.level) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.level); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "</div>\n <div class=\"overlay\"></div>\n <div class=\"inner\"></div>\n <div class=\"controls close\">\n <a href=\"#\" class=\"control\" data-control=\"zoomIn\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Magnify more", options) : helperMissing.call(depth0, "__", "Magnify more", options))) + "\"><span class=\"icon-add\"></span></a>\n <a href=\"#\" class=\"control\" data-control=\"zoomOut\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Magnify less", options) : helperMissing.call(depth0, "__", "Magnify less", options))) + "\"><span class=\"icon-remove\"></span></a>\n <a href=\"#\" class=\"closeMagnifier\" data-control=\"closeMagnifier\" title=\"" + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Close Magnifier", options) : helperMissing.call(depth0, "__", "Close Magnifier", options))) + "\"><span class=\"icon-result-nok\"></span></a>\n </div>\n</div>\n"; return buffer; }); function magnifierPanelTpl(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) 2016 (original work) Open Assessment Technologies SA ; */ /** * The screen pixel ratio * @type {Number} */ var screenRatio = window.screen.width / window.screen.height; /** * Standard debounce delay for heavy process * @type {Number} */ var debounceDelay = 50; /** * Standard scrolling throttling for the scrolling * It can be lower than the debounce delay as it is lighter in process and it improves the user experience * @type {Number} */ var scrollingDelay = 20; /** * The default base size * @type {Number} */ var defaultBaseSize = 116; /** * The minimum zoom level * @type {Number} */ var defaultLevelMin = 2; /** * The maximum zoom level * @type {Number} */ var defaultLevelMax = 8; /** * The default zoom level * @type {Number} */ var defaultLevel = defaultLevelMin; /** * Some default values * @type {Object} */ var defaultConfig = { level: defaultLevel, levelMin: defaultLevelMin, levelMax: defaultLevelMax, levelStep: 0.5, baseSize: defaultBaseSize, maxRatio: 0.5 }; var dynamicComponentDefaultConfig = { draggable: true, resizable: true, preserveAspectRatio: false, width: defaultBaseSize * defaultLevel, height: defaultBaseSize * defaultLevel / screenRatio, minWidth: defaultBaseSize * defaultLevelMin, minHeight: defaultBaseSize * defaultLevelMin / screenRatio, stackingScope: 'test-runner', top: 50, left: 10 }; /** * Creates a magnifier panel component * @param {Object} config * @param {Number} [config.level] - The default zoom level * @param {Number} [config.levelMin] - The minimum allowed zoom level * @param {Number} [config.levelMax] - The maximum allowed zoom level * @param {Number} [config.levelStep] - The level increment applied when using the controls + and - * @param {Number} [config.baseSize] - The base size used to assign the width and the height according to the zoom level * @param {Number} [config.maxRatio] - The ratio for the maximum size regarding the size of the window * @returns {magnifierPanel} the component (initialized) */ function magnifierPanelFactory(config) { var initConfig = _.defaults(config || {}, defaultConfig); var zoomLevelMin = parseFloat(initConfig.levelMin); var zoomLevelMax = parseFloat(initConfig.levelMax); var zoomLevelStep = parseFloat(initConfig.levelStep); var zoomLevel = adjustZoomLevel(initConfig.level); var maxRatio = parseFloat(initConfig.maxRatio); var $initTarget = null; var controls = null; var observer = null; var targetWidth, targetHeight, dx, dy; var scrolling = []; var dynamicComponentInstance; var dynamicComponentConfig = _.defaults(config ? config.component || {} : {}, dynamicComponentDefaultConfig); /** * @typedef {Object} magnifierPanel */ var magnifierPanel = component({ /** * Gets the current zoom level * @returns {Number} */ getZoomLevel: function getZoomLevel() { return zoomLevel; }, /** * Gets the targeted content the magnifier will zoom * @returns {jQuery} */ getTarget: function getTarget() { return controls && controls.$target; }, /** * Sets the targeted content the magnifier will zoom * @param {jQuery} $newTarget * @returns {magnifierPanel} * @fires targetchange * @fires update */ setTarget: function setTarget($newTarget) { if (controls) { controls.$target = $newTarget; controls.$viewTarget = null; setScrollingListener(); /** * @event magnifierPanel#targetchange * @param {jQuery} $target */ this.trigger('targetchange', controls.$target); this.update(); } else { $initTarget = $newTarget; } return this; }, /** * Sets the zoom level of the magnifier * @param {Number} level * @returns {magnifierPanel} * @fires zoom */ zoomTo: function zoomTo(level) { if (level && _.isFinite(level)) { zoomLevel = adjustZoomLevel(level); } applyZoomLevel(); showZoomLevel(); updateMaxSize(); updateZoom(); /** * @event magnifierPanel#zoom * @param {Number} zoomLevel */ this.trigger('zoom', zoomLevel); return this; }, /** * Increments the zoom level of the magnifier * @param {Number} step * @returns {magnifierPanel} * @fires zoom */ zoomBy: function zoomBy(step) { if (step && _.isFinite(step)) { this.zoomTo(zoomLevel + parseFloat(step)); } return this; }, /** * Zoom-in using the configured level step * @returns {magnifierPanel} * @fires zoom */ zoomIn: function zoomIn() { return this.zoomBy(zoomLevelStep); }, /** * Zoom-out using the configured level step * @returns {magnifierPanel} * @fires zoom */ zoomOut: function zoomOut() { return this.zoomBy(-zoomLevelStep); }, /** * Places the magnifier sight at a particular position on the target content * @param {Number} x * @param {Number} y * @returns {magnifierPanel} */ zoomAt: function zoomAt(x, y) { var position; if (controls) { position = this.translate(x, y); controls.$inner.css({ top: -position.top, left: -position.left }); } }, /** * Translates screen coordinates to zoom coordinates * @param {Number} x * @param {Number} y * @returns {Object} */ translate: function translate(x, y) { return { top: translateMagnifier(y, targetHeight, dynamicComponentInstance.position.height), left: translateMagnifier(x, targetWidth, dynamicComponentInstance.position.width) }; }, /** * Updates the magnifier with the target content * @returns {magnifierPanel} * @fires update */ update: function update() { if (controls && controls.$target) { controls.$clone = controls.$target.clone().removeAttr('id'); controls.$clone.find('iframe').remove(); controls.$clone.find('[name],[id],[data-serial]').removeAttr('name id data-serial'); controls.$inner.empty().append(controls.$clone); controls.$clone.find('audio').prop('muted', true); applySize(); applyZoomLevel(); updateZoom(); updateMaxSize(); applyScrolling(); /** * @event magnifierPanel#update */ this.trigger('update'); } return this; } }, defaultConfig); /** * Will update the magnifier content with the actual content * @type {Function} */ var updateMagnifier = _.debounce(_.bind(magnifierPanel.update, magnifierPanel), debounceDelay); /** * Will update the magnifier content with the scrolling position * @type {Function} */ var scrollingListenerCallback = _.throttle(function (event) { var $target = $$1(event.target); var scrollingTop = event.target.scrollTop; var scrollLeft = event.target.scrollLeft; var scrollId, scrollData; //check if the element is already known as a scrollable element if (controls && controls.$clone && $target.data('magnifier-scroll')) { scrollId = $target.data('magnifier-scroll'); scrollData = _.find(scrolling, { id: scrollId }); scrollData.scrollTop = scrollingTop; scrollData.scrollLeft = scrollLeft; //if in clone, scroll it scrollInClone(scrollData); } else { //if the element is not yet identified as a scrollable element, tag it and register its id scrollId = _.uniqueId('scrolling_'); $target.attr('data-magnifier-scroll', scrollId); scrolling.push({ id: scrollId, scrollTop: scrollingTop, scrollLeft: scrollLeft }); //update all magnifierPanel.update(); } }, scrollingDelay); /** * Scroll an element in the clone * * @param {Object} scrollData * @param {String} scrollData.id * @param {Number} [scrollData.scrollTop] * @param {Number} [scrollData.scrollLeft] */ function scrollInClone(scrollData) { var $clonedTarget; if (controls && controls.$clone && scrollData && scrollData.id) { $clonedTarget = controls.$clone.find(`[data-magnifier-scroll=${scrollData.id}]`); if ($clonedTarget.length) { if (_.isNumber(scrollData.scrollTop)) { $clonedTarget[0].scrollTop = scrollData.scrollTop; } if (_.isNumber(scrollData.scrollLeft)) { $clonedTarget[0].scrollLeft = scrollData.scrollLeft; } } } } /** * Capture all scroll positions of elements inside current target */ function updateScrollPositions() { if (!controls || !controls.$target) { return; } const elements = [controls.$target]; let scrollOffsetsChanged = false; while (elements.length) { const $currentElement = $$1(elements.shift()); const scrollLeft = $currentElement.scrollLeft(); const scrollTop = $currentElement.scrollTop(); let scrollId = $currentElement.data('magnifier-scroll'); elements.push(...Array.from($currentElement.children())); if (scrollLeft > 0 || scrollTop > 0 || scrollId) { scrollOffsetsChanged = true; if (scrollId) { const scrollData = _.find(scrolling, { id: scrollId }); scrollData.scrollTop = scrollTop; scrollData.scrollLeft = scrollLeft; } else { scrollId = _.uniqueId('scrolling_'); $currentElement.attr('data-magnifier-scroll', scrollId); scrolling.push({ id: scrollId, scrollTop, scrollLeft }); } } } // If there is any changes to scroll offset inside the target the magnifier should be updated if (scrollOffsetsChanged) { magnifierPanel.update(); } } /** * Initializes the listener for scrolling event and transfer the scrolling */ function setScrollingListener() { updateScrollPositions(); window.addEventListener('scroll', scrollingListenerCallback, true); } /** * Stops the listener for scrolling event */ function removeScrollingListener() { window.removeEventListener('scroll', scrollingListenerCallback, true); } /** * Applies scrolling programmatically from the recorded list of elements to be scrolled */ function applyScrolling() { _.forEach(scrolling, scrollInClone); } /** * Adjusts a provided zoom level to fit the constraints * @param {Number|String} level * @returns {Number} */ function adjustZoomLevel(level) { return Math.max(zoomLevelMin, Math.min(parseFloat(level), zoomLevelMax)); } /** * Applies the zoom level to the content */ function applyZoomLevel() { if (controls) { controls.$inner.css({ transform: `scale(${zoomLevel})` }); } } /** * Shows the zoom level using a CSS animation */ function showZoomLevel() { var $newZoomLevel; if (controls) { $newZoomLevel = controls.$zoomLevel.clone(true).html(zoomLevel); controls.$zoomLevel.before($newZoomLevel).remove(); controls.$zoomLevel = $newZoomLevel; } } /** * Updates the max size according to the window's size */ function updateMaxSize() { if (!dynamicComponentInstance) { return; } const $window = $$1(window); dynamicComponentInstance.config.maxWidth = $window.width() * maxRatio; dynamicComponentInstance.config.maxHeight = $window.height() * maxRatio; } /** * Forwards the size of the target to the cloned content */ function applySize() { if (controls && controls.$clone) { targetWidth = controls.$target.width(); targetHeight = controls.$target.height(); controls.$clone.width(targetWidth).height(targetHeight); } } /** * Place the zoom sight at the right place inside the magnifier */ function updateZoom() { var position; if (controls && controls.$target) { position = dynamicComponentInstance.position; position.x += dx + controls.$target.scrollLeft(); position.y += dy + controls.$target.scrollTop(); magnifierPanel.zoomAt(position.x, position.y); } } /** * Creates the observer that will react to DOM changes to update the magnifier */ function createObserver() { observer = new window.MutationObserver(updateMagnifier); } /** * Starts to observe the DOM of the magnifier target */ function startObserver() { if (controls && controls.$target) { observer.observe(controls.$target.get(0), { childList: true, // Set to true if additions and removals of the target node's child elements (including text nodes) are to be observed. attributes: true, // Set to true if mutations to target's attributes are to be observed. characterData: true, // Set to true if mutations to target's data are to be observed. subtree: true // Set to true if mutations to target and target's descendants are to be observed. }); } setScrollingListener(); } /** * Stops to observe the DOM of the magnifier target */ function stopObserver() { observer.disconnect(); removeScrollingListener(); } /** * Translates a screen coordinate into the magnifier * @param {Number} coordinate * @param {Number} actualSize * @param {Number} magnifierSize * @returns {Number} */ function translateMagnifier(coordinate, actualSize, magnifierSize) { var delta = 0; var ratio = zoomLevel; if (actualSize) { delta = actualSize * (zoomLevel - 1) / 2; ratio = (actualSize * zoomLevel - magnifierSize) / (actualSize - magnifierSize); } return coordinate * ratio - delta; } /** * Gets the top element from a particular absolute point. * @param {Number} x - the page X-coordinate of the point * @param {Number} y - the page Y-coordinate of the point * @returns {HTMLElement} */ function getElementFromPoint(x, y) { var el; // this is done to prevent working with undefined coordinates x = x || 0; y = y || 0; if (controls) { controls.$overlay.addClass('hidden'); } el = document.elementFromPoint(x, y); if (controls) { controls.$overlay.removeClass('hidden'); } return el; } /** * Find the related node in the target. The both trees must have the same content. * @param {jQuery|HTMLElement} node - the node for which find a relation * @param {jQuery|HTMLElement} root - the root of the tree that contains the actual node * @param {jQuery|HTMLElement} target - the root of the tree that could contains the related node * @returns {jQuery} */ function findSourceNode(node, root, target) { var $node = $$1(node); var $root = $$1(root); var $target = $$1(target); var indexes = [$node.index()]; // compute map of node's parents indexes $node.parents().each(function () { var $this = $$1(this); if (!$this.is($root)) { indexes.push($this.index()); } else { return false; } }); // the last index is related to the root, so ignore it indexes.pop(); // now try to find the same node using the path provided by the indexes map if (indexes.length) { $node = $target; _.forEachRight(indexes, function (index) { $node = $node.children().eq(index); if (!$node.length) { return false; } }); } else { // nothing to search for... $node = $$1(); } return $node; } dynamicComponentInstance = dynamicComponent({}).on('rendercontent', function ($content) { // eslint-disable-next-line consistent-this var dynamicComponentContext = this; var $element = this.getElement(); $element.addClass('magnifier-container'); magnifierPanel.setTemplate(magnifierPanelTpl).on('render', function () { var self = this; var $component = this.getElement(); this.setState('hidden', true); // compute the padding of the magnifier content dx = ($component.outerWidth() - $component.width()) / 2; dy = ($component.outerHeight() - $component.height()) / 2; controls = { $target: $initTarget, $inner: $$1('.inner', $component), $zoomLevel: $$1('.level', $component), $overlay: $$1('.overlay', $component) }; $initTarget = null; // click on zoom-out control $component.on('click touchstart', '.control[data-control="zoomOut"]', function (event) { event.preventDefault(); self.zoomOut(); }); // click on zoom-in control $component.on('click touchstart', '.control[data-control="zoomIn"]', function (event) { event.preventDefault(); self.zoomIn(); }); // click on close controls $component.on('click touchstart', '.closeMagnifier', function (event) { event.preventDefault(); self.hide(); self.trigger('close'); }); // interact through the magnifier glass with the zoomed content $component.on('click touchstart', '.overlay', function (event) { findSourceNode(getElementFromPoint(event.pageX, event.pageY), controls.$inner, controls.$target).click().focus(); }); createObserver(); updateMaxSize(); applyZoomLevel(); }).on('show', function () { updateMagnifier(); startObserver(); dynamicComponentContext.show(); }).on('hide', function () { stopObserver(); dynamicComponentContext.hide(); }).on('destroy', function () { stopObserver(); $initTarget = null; controls = null; observer = null; dynamicComponentContext.destroy(); }).init(initConfig).render($content); }).on('down move resize', function () { updateZoom(); }).on('resize', function () { updateMaxSize(); }).init(dynamicComponentConfig); return magnifierPanel; } return magnifierPanelFactory; });