UNPKG

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

Version:
644 lines (602 loc) 25 kB
define(['jquery', 'lodash', 'ui/component', 'ui/hider', 'ui/stacker', 'handlebars', 'lib/handlebars/helpers'], function ($$1, _, componentFactory, hider, stackerFactory, Handlebars, Helpers0) { 'use strict'; $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; componentFactory = componentFactory && Object.prototype.hasOwnProperty.call(componentFactory, 'default') ? componentFactory['default'] : componentFactory; hider = hider && Object.prototype.hasOwnProperty.call(hider, 'default') ? hider['default'] : hider; stackerFactory = stackerFactory && Object.prototype.hasOwnProperty.call(stackerFactory, 'default') ? stackerFactory['default'] : stackerFactory; Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars; Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0; 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, functionType="function", escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { var buffer = "", stack1, helper; buffer += " "; if (helper = helpers.className) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.className); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1); return buffer; } function program3(depth0,data) { var buffer = "", stack1, helper; buffer += "<span class=\"icon icon-"; if (helper = helpers.icon) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.icon); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1); stack1 = helpers.unless.call(depth0, (depth0 && depth0.text), {hash:{},inverse:self.noop,fn:self.program(4, program4, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"></span>"; return buffer; } function program4(depth0,data) { return " no-label"; } function program6(depth0,data) { var buffer = "", stack1, helper; buffer += "<span class=\"text\">"; if (helper = helpers.text) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.text); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "</span>"; return buffer; } buffer += "<li\n data-control=\""; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n class=\"small btn-info action "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.className), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n title=\""; if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n aria-label=\""; if (helper = helpers.title) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.title); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n aria-haspopup=\"listbox\"\n role=\"button\"\n>\n <a class=\"li-inner\" data-control=\""; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "-button\" href=\"#\" aria-hidden=\"true\">\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.icon), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.text), {hash:{},inverse:self.noop,fn:self.program(6, program6, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n &nbsp; <span class=\"icon icon-up\"></span>\n </a>\n <div data-control=\""; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "-menu\" class=\"hidden "; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "-menu\" tabindex=\"1\">\n <ul\n data-control=\""; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "-list\"\n class=\"menu "; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "-list\"\n role=\"listbox\"\n ></ul>\n </div>\n</li>\n"; return buffer; }); function menuTpl(data, options, asString) { var html = Template(data, options); return (asString || true) ? html : $(html); } if (!Helpers0.__initialized) { Helpers0(Handlebars); Helpers0.__initialized = true; } var Template$1 = 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, functionType="function", escapeExpression=this.escapeExpression, self=this; function program1(depth0,data) { var buffer = "", stack1, helper; buffer += " "; if (helper = helpers.className) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.className); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1); return buffer; } function program3(depth0,data) { var buffer = "", stack1, helper; buffer += "\n role=\""; if (helper = helpers.role) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.role); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n "; return buffer; } buffer += "<li\n class=\"small action menu-item "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.className), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\"\n data-control=\""; if (helper = helpers.control) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.control); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "\"\n tabindex=\"-1\"\n "; stack1 = helpers['if'].call(depth0, (depth0 && depth0.role), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data}); if(stack1 || stack1 === 0) { buffer += stack1; } buffer += "\n>\n <a class=\"li-inner menu-inner\">\n <span class=\"icon icon-checkbox\"></span><span class=\"icon icon-checkbox-checked\"></span>\n <span class=\"label\">"; if (helper = helpers.text) { stack1 = helper.call(depth0, {hash:{},data:data}); } else { helper = (depth0 && depth0.text); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; } buffer += escapeExpression(stack1) + "</span>\n </a>\n</li>"; return buffer; }); function menuItemTpl(data, options, asString) { var html = Template$1(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) 2017-2020 (original work) Open Assessment Technologies SA; */ var keyCodes = { TAB: 9, ESC: 27, ENTER: 13, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 }; var stacker = stackerFactory('test-runner'); var menuComponentApi = { /** * Initialise the menu */ initMenu: function initMenu() { this.id = this.config.control; this.navType = this.config.navType ? this.config.navType : 'fromLast'; this.menuItems = []; }, /** * Get the type of the component */ getType: function getType() { return 'menu'; }, /** * Get the menu Id * @returns {String} */ getId: function getId() { return this.id; }, /** * Set the menu as active, essentially meaning that the menu panel is opened */ turnOn: function turnOn() { this.setState('active', true); }, /** * Set the menu as inactive */ turnOff: function turnOff() { this.setState('active', false); }, /** * ===================== * Actions on menu panel * ===================== */ /** * open/close the menu */ toggleMenu: function showMenu() { if (!this.is('disabled')) { if (this.is('opened')) { this.closeMenu(); } else { this.openMenu(); } } }, /** * It needs to find closest visible item. * * @param {Number} last - index to stop. * @param {-1|1} inc - incrementor. -1 - navigate to the top, 1 - to the bottom. * * @returns {Number} returns index > 0 if a visible item was found and -1 otherwise. */ indexOfClosestVisibleItem(inc, last) { if (!this.menuItems.length) { return -1; } let elem; let position = this.hoverIndex; do { position += inc; if (position === last) { return -1; } elem = this.menuItems[position].getElement(); } while (elem && elem.hasClass('hidden')); return position; }, /** * Changes hoverIndex and hover item. * * @param {Number} index - item index to hover. * * @returns the menu item. */ hoverByIndex(index) { const elem = this.menuItems[index]; this.hoverIndex = index; if (elem) { this.hoverItem(elem.id); } return elem; }, hoverNextVisibleItem() { const index = this.indexOfClosestVisibleItem(1, this.menuItems.length); const elem = this.hoverByIndex(index); return elem; }, hoverPreviousVisibleItem() { const index = this.indexOfClosestVisibleItem(-1, -1); const elem = this.hoverByIndex(index); return elem; }, /** * open the menu */ openMenu: function openMenu() { // show the DOM element hider.show(this.$menuContainer); stacker.bringToFront(this.$menuContent); // change the menu button icon this.$menuStateIcon.removeClass('icon-up'); this.$menuStateIcon.addClass('icon-down'); // turn on the menu button this.turnOn(); // setup keyboard navigation & highlighting this.enableShortcuts(); this.hoverOffAll(); if (document.activeElement) { document.activeElement.blur(); } const activeItemIndex = _.findIndex(this.menuItems, item => item.is('active')); if (activeItemIndex >= 0) { this.hoverIndex = activeItemIndex; this.$menuItems[this.hoverIndex].focus(); this.hoverItem(this.menuItems[activeItemIndex].id); } else if (this.navType === 'fromLast') { // fromLast (default) navigation: focus on button and then using UP go to last item this.hoverIndex = this.menuItems.length; // we start on the button, not at the max array index // which would be menuItems.length-1 this.hoverPreviousVisibleItem(); } else if (this.navType === 'fromFirst') { // fromFirst navigation: focus on button and then using DOWN go to first item this.hoverIndex = -1; // we start on the button, not the first element // which would be 0 this.hoverNextVisibleItem(); } // component inner state this.setState('opened', true); this.trigger('openmenu', this); }, /** * close the menu */ closeMenu() { // hide the DOM element hider.hide(this.$menuContainer); // change the menu button icon this.$menuStateIcon.removeClass('icon-down'); this.$menuStateIcon.addClass('icon-up'); // turn off the button this.turnOff(); // disable keyboard navigation & highlighting this.disableShortcuts(); this.hoverOffAll(); // component inner state this.setState('opened', false); this.trigger('closemenu', this); // Move focus if the menu wasn't disabled before the close action was launched. if (!this.is('disabled') && !this.$component.prop('disabled')) { this.$menuButton.parent().focus(); // It needs for screenreaders to correctly read menu button after submenu was closed } }, /** * ===================== * Actions on menu items * ===================== */ /** * Look for a item in the internal item registry * @param {String} itemId * @returns {Object|undefined} */ getItemById: function getItemById(itemId) { return _.find(this.menuItems, function (item) { return item.getId() === itemId; }); }, /** * Adds an item to the menu * @param {Component} item */ addItem: function addItem(item) { if (item) { this.menuItems.push(item); } }, /** * Render menu items into the menu panel */ renderItems: function renderItems() { var self = this; this.menuItems.forEach(function (item) { item.setTemplate(menuItemTpl); // the item has been created as generic. Let's give him now the menu entry template item.render(self.$menuContent); item.enable(); }); // bind mouse behavior on menu items this.$menuItems = this.$menuContent.find('.menu-item'); this.$menuItems.on('mouseenter', function highlightHoveredEntry(e) { var itemId = e.currentTarget.getAttribute('data-control'); self.mouseOverItem(itemId); }); }, /** * Highlight the currently hovered item * @param {String} itemId */ mouseOverItem: function mouseOverItem(itemId) { var self = this; // look for item index this.menuItems.forEach(function (item, index) { if (item.id === itemId) { self.hoverIndex = index; } }); this.hoverItem(itemId); }, /** * Check that the menu has at least one of its entries displayed * @returns {boolean} */ hasDisplayedItems: function hasDisplayedItems() { return this.menuItems.some(function (item) { return !item.is('disabled') && !item.is('hidden'); }); }, /** * Set all entries in the menu to inactive */ turnOffAll: function turnOffAll() { this.menuItems.forEach(function (current) { current.turnOff(); }); }, /** * ===================== * Menu items navigation * ===================== */ /** * register the event handlers for keyboard navigation */ enableShortcuts: function enableShortcuts() { var self = this; this.$menuContainer.on('keydown.menuNavigation', function (e) { var currentKeyCode = e.keyCode ? e.keyCode : e.charCode; e.preventDefault(); switch (currentKeyCode) { case keyCodes.ESC: case keyCodes.TAB: self.closeMenu(); break; case keyCodes.SPACE: case keyCodes.ENTER: self.triggerHighlightedItem(); e.stopPropagation(); break; case keyCodes.LEFT: case keyCodes.UP: self.moveUp(); e.stopPropagation(); break; case keyCodes.RIGHT: case keyCodes.DOWN: self.moveDown(); e.stopPropagation(); break; } }); this.$menuButton.on('keydown.menuNavigation', function (e) { var currentKeyCode = e.keyCode ? e.keyCode : e.charCode; function setFocusToItem(index) { self.hoverIndex = index; self.$menuContainer.focus(); self.hoverItem(self.menuItems[self.hoverIndex].id); } if (currentKeyCode === keyCodes.UP && self.navType === 'fromLast') { e.stopPropagation(); setFocusToItem(self.menuItems.length - 1); } if (currentKeyCode === keyCodes.DOWN && self.navType === 'fromFirst') { e.stopPropagation(); setFocusToItem(0); } }); }, /** * remove the event handlers for keyboard navigation */ disableShortcuts: function disableShortcuts() { this.$menuContainer.off('.menuNavigation'); this.$menuButton.off('.menuNavigation'); }, /** * Move the highlight to the previous not hidden item */ moveUp: function moveUp() { if (this.hoverIndex > 0) { const elem = this.hoverPreviousVisibleItem(); if (!elem) { this.closeMenu(); } // move to the menu button } else if (this.hoverIndex === 0) { this.hoverIndex--; this.closeMenu(); } }, /** * Move the highlight to the next not hidden item, or to the menu button if we are on the last item */ moveDown: function moveDown() { // move to the next item if (this.hoverIndex < this.menuItems.length - 1) { const elem = this.hoverNextVisibleItem(); if (!elem) { this.closeMenu(); } // move to the menu button } else if (this.hoverIndex === this.menuItems.length - 1) { this.hoverIndex++; this.closeMenu(); } }, /** * Highlight the given item * @param {String} itemId */ hoverItem: function hoverItem(itemId) { var itemToHover = this.getItemById(itemId); this.hoverOffAll(); if (itemToHover) { itemToHover.hoverOn(); itemToHover.getElement().focus(); } }, /** * Remove highlight from all items */ hoverOffAll: function hoverOffAll() { this.menuItems.forEach(function (current) { if (current) { current.hoverOff(); } }); }, /** * Run a click event on the DOM element of the currently highlighted item */ triggerHighlightedItem: function triggerHighlightedItem() { var activeItem; if (this.menuItems[this.hoverIndex]) { activeItem = this.menuItems[this.hoverIndex]; activeItem.getElement().trigger('click'); this.closeMenu(); // give back the focus this.$component.focus(); } }, /** * Set navigation type * @param {String} type - 'fromLast', 'fromFirst' */ setNavigationType: function setNavigationType(type) { if (['fromLast', 'fromFirst'].includes(type)) { this.navType = type; } } }; /** * The menu component factory */ function menuComponentFactory(specs, defaults) { var _defaults, menuComponent; _defaults = { $component: $$1(), $menuButton: $$1(), $menuContainer: $$1(), $menuContent: $$1(), $menuItems: $$1(), $menuStateIcon: $$1(), hoverIndex: null, id: null, menuItems: [] }; specs = _.defaults(specs || {}, menuComponentApi); menuComponent = componentFactory(specs, defaults).setTemplate(menuTpl).on('enable', function enable() { if (this.is('rendered')) { this.$component.removeProp('disabled'); } }).on('disable', function disable() { if (this.is('rendered')) { this.$component.prop('disabled', true); this.closeMenu(); this.turnOff(); } }).on('hide', function disable() { if (this.is('rendered')) { this.closeMenu(); } }).on('init', function init() { this.initMenu(); }).on('render', function render() { var self = this; // get access to DOM elements this.$menuButton = this.$component.find(`[data-control="${this.config.control}-button"]`); this.$menuContainer = this.$component.find(`[data-control="${this.config.control}-menu"]`); this.$menuContent = this.$component.find(`[data-control="${this.config.control}-list"]`); this.$menuStateIcon = this.$menuButton.find('.icon-up'); this.disable(); // always render disabled by default // add behavior this.$component.on('click', function toggleMenu(e) { e.preventDefault(); if (!self.is('opened')) { e.stopPropagation(); // prevent higher handler to auto-close the menu on click } self.toggleMenu(); }); this.$menuContainer.on('click', function closeMenuOnItemClick(e) { e.preventDefault(); e.stopPropagation(); // so the menu doesn't get toggled again when the event bubble to the component self.closeMenu(); }); this.$menuContent.on('mouseleave', this.hoverOffAll); }).on('destroy', function () { if (this.is('rendered')) { this.$menuContainer.off('.menuNavigation'); this.$menuButton.off('.menuNavigation'); } }); // Apply default properties to the menuComponent _.defaults(menuComponent, _defaults); return menuComponent; } return menuComponentFactory; });