@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
270 lines (249 loc) • 9.16 kB
JavaScript
define(['jquery', 'i18n', 'ui/hider', 'ui/transformer', 'util/shortcut', 'util/namespace', 'taoTests/runner/plugin', 'taoQtiTest/runner/helpers/map'], function ($, __, hider, transformer, shortcut, namespaceHelper, pluginFactory, mapHelper) { 'use strict';
$ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $;
__ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
transformer = transformer && Object.prototype.hasOwnProperty.call(transformer, 'default') ? transformer['default'] : transformer;
shortcut = shortcut && Object.prototype.hasOwnProperty.call(shortcut, 'default') ? shortcut['default'] : shortcut;
namespaceHelper = namespaceHelper && Object.prototype.hasOwnProperty.call(namespaceHelper, 'default') ? namespaceHelper['default'] : namespaceHelper;
pluginFactory = pluginFactory && Object.prototype.hasOwnProperty.call(pluginFactory, 'default') ? pluginFactory['default'] : pluginFactory;
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-2022 (original work) Open Assessment Technologies SA;
*
* @author dieter <dieter@taotesting.com>
* @author Alexander Zagovorichev <zagovorichev@1pt.com>
*/
/**
* The standard zoom level, in percentage
* @type {Number}
*/
const standard = 100;
/**
* Zoom-In/Zoom-Out steps
* @type {Number}
*/
const increment = 10;
/**
* The zoom boundaries, in percentage
* @type {Object}
*/
const threshold = {
lower: 10,
upper: 200
};
/**
* Sets the zoom level
* @param {jQuery} $target
* @param {Number} level - Zoom percentage
*/
const _setZoomLevel = ($target, level) => {
const $parent = $target.parent();
const newScale = level / standard;
const isOverZoom = $parent.outerWidth(true) < $target.width() * newScale;
if (isOverZoom) {
transformer.setTransformOrigin($target, '0 0');
$parent.css('margin-left', '0');
} else {
transformer.setTransformOrigin($target, '50% 0');
$parent.css('margin-left', '');
}
transformer.scale($target, newScale);
};
/**
* Restores the standard zoom level
* @param {jQuery} $target
*/
const _resetZoom = $target => {
transformer.reset($target);
};
/**
* Forces a browser repaint
* Solution from http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes?answertab=votes#tab-top
* @param {jQuery} $target
*/
const forceRepaint = $target => {
const sel = $target[0];
if (sel) {
sel.style.display = 'none';
sel.offsetHeight; // no need to store this anywhere, the reference is enough
sel.style.display = '';
}
};
/**
* Returns the configured plugin
*/
var zoom = pluginFactory({
name: 'zoom',
/**
* Initialize the plugin (called during runner's init)
*/
init() {
const testRunner = this.getTestRunner();
const testRunnerOptions = testRunner.getOptions();
const pluginShortcuts = (testRunnerOptions.shortcuts || {})[this.getName()] || {};
const testRunnerContainer = this.getAreaBroker().getContainer().get(0);
/**
* Checks if the plugin is currently available
* @returns {Boolean}
*/
const isConfigured = () => {
//to be activated with the special category x-tao-option-zoom
return mapHelper.hasItemCategory(testRunner.getTestMap(), testRunner.getTestContext().itemIdentifier, 'zoom', true);
};
/**
* Is zoom activated ? if not, then we hide the plugin
*/
const togglePlugin = () => {
if (isConfigured()) {
//allow zoom
this.show();
} else {
this.hide();
}
};
const zoomAction = dir => {
const inc = increment * dir;
if (this.$zoomTarget) {
const el = this.$zoomTarget[0];
const before = el.getBoundingClientRect();
let sx = this.$container.scrollLeft();
let sy = this.$container.scrollTop();
this.zoom = Math.max(threshold.lower, Math.min(threshold.upper, this.zoom + inc));
if (this.zoom === standard) {
_resetZoom(this.$zoomTarget);
} else {
_setZoomLevel(this.$zoomTarget, this.zoom);
}
testRunnerContainer.style.setProperty('--tool-zoom-level', this.zoom / standard);
// force a browser repaint to fix a scrollbar issue with WebKit
forceRepaint(this.$zoomTarget);
const after = el.getBoundingClientRect();
sx = Math.max(0, sx + (after.width - before.width) / 2);
sy = Math.max(0, sy + (after.height - before.height) / 2);
this.$container.scrollLeft(sx).scrollTop(sy);
}
};
const zoomIn = () => {
if (this.getState('enabled') !== false) {
zoomAction(1);
}
};
const zoomOut = () => {
if (this.getState('enabled') !== false) {
zoomAction(-1);
}
};
/**
* Reapplys the same zoom level to the target
* It can be useful if the element was (visually-)hidden why zoom happened
*/
const zoomReApply = () => {
if (this.zoom !== standard) {
_setZoomLevel(this.$zoomTarget, this.zoom);
}
};
//build element (detached)
this.buttonZoomOut = this.getAreaBroker().getToolbox().createEntry({
control: 'zoomOut',
title: __('Zoom out'),
icon: 'remove'
});
this.buttonZoomIn = this.getAreaBroker().getToolbox().createEntry({
control: 'zoomIn',
title: __('Zoom in'),
icon: 'add'
});
//attach behavior
this.buttonZoomIn.on('click', e => {
e.preventDefault();
testRunner.trigger('tool-zoomin');
});
//attach behavior
this.buttonZoomOut.on('click', e => {
e.preventDefault();
testRunner.trigger('tool-zoomout');
});
if (testRunnerOptions.allowShortcuts) {
if (pluginShortcuts.in) {
shortcut.add(namespaceHelper.namespaceAll(pluginShortcuts.in, this.getName(), true), () => {
testRunner.trigger('tool-zoomin');
}, {
avoidInput: true
});
}
if (pluginShortcuts.out) {
shortcut.add(namespaceHelper.namespaceAll(pluginShortcuts.out, this.getName(), true), () => {
testRunner.trigger('tool-zoomout');
}, {
avoidInput: true
});
}
}
//start disabled
togglePlugin();
this.disable();
//update plugin state based on changes
testRunner.on('loaditem', () => {
this.zoom = standard;
togglePlugin();
this.disable();
}).on('renderitem', () => {
this.$container = $('#qti-content');
this.$zoomTarget = $('.qti-item');
this.enable();
}).on('enabletools', () => {
this.enable();
}).on('disabletools unloaditem', () => {
this.disable();
}).on('tool-zoomin', zoomIn).on('tool-zoomout', zoomOut).on('tool-zoomreapply', zoomReApply);
},
/**
* Called during the runner's destroy phase
*/
destroy() {
shortcut.remove(`.${this.getName()}`);
},
/**
* Enable the button
*/
enable() {
this.buttonZoomIn.enable();
this.buttonZoomOut.enable();
},
/**
* Disable the button
*/
disable() {
this.buttonZoomIn.disable();
this.buttonZoomOut.disable();
},
/**
* Show the button
*/
show() {
this.buttonZoomIn.show();
this.buttonZoomOut.show();
},
/**
* Hide the button
*/
hide() {
this.buttonZoomIn.hide();
this.buttonZoomOut.hide();
}
});
return zoom;
});