@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
279 lines (248 loc) • 8.46 kB
JavaScript
/**
* 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 (original work) Open Assessment Technologies SA;
*/
/**
* Answer Eliminator Plugin
*
* While the platform's answer eliminator works on a per-item base, this variation allows
* answer elimination on a test level. Essentially it allows to add a class 'eliminable'
* to a choice interaction, from there the aforementioned item-based behaviour and styling takes
* over.
*
* Alternative styling will be on a per customer basis and should always be published as a recipe
* in the theme-toolkit.
*
* @author Dieter Raber <dieter@taotesting.com>
*/
import _ from 'lodash';
import __ from 'i18n';
import 'ui/hider';
import shortcut from 'util/shortcut';
import namespaceHelper from 'util/namespace';
import mapHelper from 'taoQtiTest/runner/helpers/map';
import pluginFactory from 'taoTests/runner/plugin';
/**
* The public name of the plugin
* @type {String}
*/
var pluginName = 'eliminator';
/**
* The prefix of actions triggered through the event loop
* @type {String}
*/
var actionPrefix = `tool-${pluginName}-`;
/**
* Some default options for the plugin
* @type {Object}
*/
var defaultConfig = {
// when hiding the buttons, don't remove existing eliminations
removeEliminationsOnClose: false,
// when showing the buttons, restore previously set eliminations
restoreEliminationsOnOpen: false
};
/**
* Returns the configured plugin
*/
export default pluginFactory({
name: pluginName,
/**
* Initialize the plugin (called during runner's init)
*/
init() {
const self = this;
const testRunner = this.getTestRunner();
const $container = testRunner
.getAreaBroker()
.getContentArea()
.parent();
const testRunnerOptions = testRunner.getOptions();
const pluginShortcuts = (testRunnerOptions.shortcuts || {})[pluginName] || {};
const config = Object.assign({}, defaultConfig, this.getConfig());
// register the button in the toolbox
this.button = this.getAreaBroker()
.getToolbox()
.createEntry({
control: 'eliminator',
title: __('Eliminate choices'),
icon: 'eliminate',
text: __('Answer Eliminator')
});
/**
* Checks if the plugin is currently available
* @returns {Boolean}
*/
function isPluginEnabled() {
//to be activated with the special category x-tao-option-eliminator
return mapHelper.hasItemCategory(
testRunner.getTestMap(),
testRunner.getTestContext().itemIdentifier,
'eliminator',
true
);
}
/**
* Is plugin activated ? if not, then we hide the plugin
*/
function togglePluginButton() {
if (isPluginEnabled()) {
self.show();
} else {
self.hide();
}
}
function togglePlugin() {
if (!self.$choiceInteractions) {
return;
}
self.$choiceInteractions.toggleClass('eliminable');
if (isEliminable()) {
enableEliminator();
} else {
disableEliminator();
}
}
function isEliminable() {
if (!self.$choiceInteractions) {
return;
}
return self.$choiceInteractions.hasClass('eliminable');
}
function enableEliminator() {
var $choices;
if (!self.$choiceInteractions) {
return;
}
$choices = self.$choiceInteractions.find('.qti-choice');
self.button.turnOn();
self.trigger('start');
if (config.restoreEliminationsOnOpen) {
$choices.each(function() {
var input = this.querySelector('.real-label input');
if (this.dataset.wasEliminated) {
this.dataset.wasEliminated = null;
this.classList.add('eliminated');
input.setAttribute('disabled', 'disabled');
input.checked = false;
}
});
}
}
function disableEliminator() {
var $choices;
if (!self.$choiceInteractions) {
return;
}
$choices = self.$choiceInteractions.find('.qti-choice');
self.$choiceInteractions.removeClass('eliminable');
self.button.turnOff();
self.trigger('end');
$choices.each(function() {
if (this.classList.contains('eliminated')) {
this.dataset.wasEliminated = true;
this.classList.remove('eliminated');
this.querySelector('.real-label input').removeAttribute('disabled');
}
});
}
//add a new mask each time the button is pressed
this.button.on('click', function(e) {
e.preventDefault();
testRunner.trigger(`${actionPrefix}toggle`);
});
// handle the plugin's shortcuts
if (testRunnerOptions.allowShortcuts) {
_.forEach(pluginShortcuts, function(command, key) {
shortcut.add(
namespaceHelper.namespaceAll(command, pluginName, true),
function() {
// just fire the action using the event loop
testRunner.trigger(actionPrefix + key);
},
{
avoidInput: true
}
);
});
}
//start disabled
this.disable();
//update plugin state based on changes
testRunner
.on('loaditem', togglePluginButton)
.on('renderitem', function conditionalInit() {
// show button only when in the presence of choice interactions
self.$choiceInteractions = $container.find('.qti-choiceInteraction');
if (!self.$choiceInteractions.length) {
self.hide();
return;
}
if (isPluginEnabled()) {
self.show();
}
})
.on('enabletools renderitem', function() {
self.enable();
})
.on('disabletools unloaditem', function() {
self.disable();
disableEliminator();
})
// commands that controls the plugin
.on(`${actionPrefix}toggle`, function() {
if (isPluginEnabled()) {
togglePlugin();
}
})
// Answer-eliminator and Answer-masking are mutually exclusive tools
.on('tool-answer-masking-toggle', function() {
if (isEliminable()) {
disableEliminator();
}
});
},
/**
* Called during the runner's destroy phase
*/
destroy: function destroy() {
shortcut.remove(`.${pluginName}`);
},
/**
* Enable the button
*/
enable: function enable() {
this.button.enable();
},
/**
* Disable the button
*/
disable: function disable() {
this.button.disable();
},
/**
* Show the button
*/
show: function show() {
this.button.show();
},
/**
* Hide the button
*/
hide: function hide() {
this.button.hide();
}
});