UNPKG

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

Version:
355 lines (334 loc) 14.4 kB
define(['jquery', 'lodash', 'i18n', 'core/logger', 'taoTests/runner/plugin', 'util/shortcut', 'util/namespace', 'taoQtiTest/runner/helpers/currentItem', 'taoQtiTest/runner/plugins/tools/highlighter/collection', 'taoQtiTest/runner/helpers/map'], function ($, _, __, loggerFactory, pluginFactory, shortcut, namespaceHelper, itemHelper, highlighterCollection, mapHelper) { 'use strict'; $ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $; _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _; __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __; loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory; pluginFactory = pluginFactory && Object.prototype.hasOwnProperty.call(pluginFactory, 'default') ? pluginFactory['default'] : pluginFactory; shortcut = shortcut && Object.prototype.hasOwnProperty.call(shortcut, 'default') ? shortcut['default'] : shortcut; namespaceHelper = namespaceHelper && Object.prototype.hasOwnProperty.call(namespaceHelper, 'default') ? namespaceHelper['default'] : namespaceHelper; itemHelper = itemHelper && Object.prototype.hasOwnProperty.call(itemHelper, 'default') ? itemHelper['default'] : itemHelper; highlighterCollection = highlighterCollection && Object.prototype.hasOwnProperty.call(highlighterCollection, 'default') ? highlighterCollection['default'] : highlighterCollection; mapHelper = mapHelper && Object.prototype.hasOwnProperty.call(mapHelper, 'default') ? mapHelper['default'] : mapHelper; /** * 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-2019 (original work) Open Assessment Technologies SA ; */ /** * Returns the configured plugin */ var plugin = pluginFactory({ name: 'highlighter', /** * Install plugin's functions * */ install: function install() { var testRunner = this.getTestRunner(); //define the "highlighter" store as "volatile" (removed on browser change). testRunner.getTestStore().setVolatile(this.getName()); }, /** * Initialize the plugin (called during runner's init) * @returns {void} */ init: function init() { const self = this; const testRunner = this.getTestRunner(); const testRunnerOptions = testRunner.getOptions(); const pluginShortcuts = (testRunnerOptions.shortcuts || {})[this.getName()] || {}; let hasHighlights = false; const logger = loggerFactory('highlighterPlugin'); /** * @var {Object} highlighters - Highlighters collection * See taoQtiTest/views/js/runner/plugins/tools/highlighter/collection.js */ var highlighters = highlighterCollection(); // Create the first (item-level) highlighter instance: highlighters.addHighlighter({ className: 'txt-user-highlight', containerSelector: '.qti-itemBody', containersBlackList: ['.qti-include'], id: 'item-highlighter' }); // create buttons this.buttonMain = this.getAreaBroker().getToolbox().createEntry({ title: __('Highlight Text'), icon: 'text-marker', control: 'highlight-trigger', aria: { pressed: 'false', label: __('(Visual) Highlight Text') }, text: __('Highlight') }); this.buttonRemove = this.getAreaBroker().getToolbox().createEntry({ title: __('Clear all active highlights'), icon: 'result-nok', control: 'highlight-clear', aria: { label: __('(Visual) Clear all active highlights') }, text: __('Clear Highlights') }); // attach user events this.buttonMain.on('mousedown', function (e) { // using 'mousedown' instead of 'click' to avoid losing current selection e.preventDefault(); if (isPluginEnabled()) { _.forEach(highlighters.getAllHighlighters(), function (instance) { if (instance.isEnabled()) { instance.highlight(); } }); } }); this.buttonRemove.on('click', function (e) { e.preventDefault(); if (isPluginEnabled()) { _.forEach(highlighters.getAllHighlighters(), function (instance) { if (instance.isEnabled()) { instance.clearHighlights(); } }); testRunner.trigger('clear'); } }); if (testRunnerOptions.allowShortcuts) { if (pluginShortcuts.toggle) { shortcut.add(namespaceHelper.namespaceAll(pluginShortcuts.toggle, this.getName(), true), function () { if (isPluginEnabled()) { _.forEach(highlighters.getAllHighlighters(), function (instance) { if (instance.isEnabled()) { instance.highlight(); } }); } }, { avoidInput: true, prevent: true }); } } //start disabled this.disable(); /** * Checks if the plugin is currently available * @returns {Boolean} */ function isPluginEnabled() { //to be activated with the special category x-tao-option-highlighter return mapHelper.hasItemCategory(testRunner.getTestMap(), testRunner.getTestContext().itemIdentifier, 'highlighter', true); } /** * Is plugin activated ? if not, then we hide the plugin */ function togglePlugin() { if (isPluginEnabled()) { self.show(); } else { self.hide(); } } /** * Gets the browser test store * Can be in volatile or non-volatile mode, depending on config * @returns {Promise} */ function getStore() { return testRunner.getTestStore().getStore(self.getName()); } /** * Load the stores and hook the behavior */ return getStore().then(function (highlighterStore) { /** * Saves a highlighter's state to the appropriate store * @param {String} [itemId] - must be provided to save item-level highlights, * will be used as store key if no key provided * @param {String} [key] - a key (e.g. a stimulus href) under which we store non-item-level highlights * @returns {Boolean} true if save was done */ function saveHighlight(itemId, key) { var instance; var highlightsIndex; if (!itemId) { instance = highlighters.getHighlighterById(key); } else { key = itemId; instance = highlighters.getItemHighlighter(); } if (!instance) return Promise.resolve(false); highlightsIndex = instance.getIndex(); if (isPluginEnabled() && hasHighlights && key) { logger.debug(`Saving ${highlightsIndex.length} highlights for id ${key}`); return highlighterStore.setItem(key, highlightsIndex); } return false; } /** * Saves all the highlighters states in the store * First the non-item highlighters, then the item highlighter (index 0) * @returns {Promise} resolves once the save is done */ function saveAll() { var nonItemHighlighters = highlighters.getNonItemHighlighters(); return Promise.all(_(nonItemHighlighters).filter(function (instance) { return instance.isEnabled(); }).map(function (instance) { var key = instance.getId(); return saveHighlight(null, key); }).value()).then(function (results) { // Now save the main item highlight // and if every setItem() returned true, return true var itemId = testRunner.getTestContext().itemIdentifier; return saveHighlight(itemId) && _.every(results); }); } /** * Retrieves a highlighter's state from a store and applies it to the DOM * @param {String} [itemId] - must be provided to save item-level highlights, * will be used as store key if no key provided * @param {String} [key] - a key (e.g. a stimulus href) under which we store non-item-level highlights * @returns {Promise} resolves once the load is done */ function loadHighlight(itemId, key) { var instance; if (!itemId) { instance = highlighters.getHighlighterById(key); } else { key = itemId; instance = highlighters.getItemHighlighter(); } if (!instance) return Promise.resolve(false); return highlighterStore.getItem(key).then(function (index) { if (index) { logger.debug(`Loading ${index.length} highlights for key ${key}`); hasHighlights = true; instance.restoreIndex(index); } }).then(function () { //save highlighter state during the item session, //when the highlighting ends instance.on('end.save', function () { return saveHighlight(itemId, key); }); }); } //update plugin state based on changes testRunner.on('loaditem', togglePlugin).on('enabletools renderitem', function () { self.enable(); if (isPluginEnabled()) { _.forEach(highlighters.getAllHighlighters(), function (instance) { if (!instance.isEnabled()) { instance.on('start').toggleHighlighting(false).enable(); } }); } }).on('renderitem', function () { var textStimuli; var itemId = testRunner.getTestContext().itemIdentifier; if (itemId && isPluginEnabled()) { hasHighlights = false; highlighters.getItemHighlighter().enable(); // Load item-level highlights from store: loadHighlight(itemId); // Count stimuli in this item: textStimuli = itemHelper.getTextStimuliHrefs(testRunner); // NOW we can instantiate the extra highlighters: _.forEach(textStimuli, function (textStimulusHref) { var stimHighlighter = highlighters.getHighlighterById(textStimulusHref); // Instantiate, if id not already present in highlighters... if (!stimHighlighter) { stimHighlighter = highlighters.addHighlighter({ className: 'txt-user-highlight', containerSelector: `.qti-include[data-href="${textStimulusHref}"]`, id: textStimulusHref }); } stimHighlighter.enable(); // And load its data: loadHighlight(null, textStimulusHref); }); } }).after('renderitem', function () { // Attach start/end listeners only to item level highlighter const instance = highlighters.getItemHighlighter(); if (instance.isEnabled()) { instance.on('start', function () { self.buttonMain.turnOn(); self.buttonMain.$component.attr('aria-pressed', 'true'); self.trigger('start'); hasHighlights = true; }).on('end', function () { self.buttonMain.turnOff(); self.buttonMain.$component.attr('aria-pressed', 'false'); self.trigger('end'); }); } }).after('clear.highlighter', function () { saveAll(); }).before('skip move timeout', function () { return saveAll(); }).on('disabletools unloaditem', function () { self.disable(); if (isPluginEnabled()) { _.forEach(highlighters.getAllHighlighters(), function (instance) { if (instance.isEnabled()) { instance.off('end.save end start').toggleHighlighting(false).disable(); } }); } }); }); }, /** * Called during the runner's destroy phase */ destroy: function destroy() { shortcut.remove(`.${this.getName()}`); $(document).off('.highlighter'); }, /** * Enable the button */ enable: function enable() { this.buttonMain.enable(); this.buttonRemove.enable(); }, /** * Disable the button */ disable: function disable() { this.buttonMain.disable(); this.buttonRemove.disable(); }, /** * Show the button */ show: function show() { this.buttonMain.show(); this.buttonRemove.show(); }, /** * Hide the button */ hide: function hide() { this.buttonMain.hide(); this.buttonRemove.hide(); } }); return plugin; });