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