@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
325 lines (294 loc) • 12.8 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) 2018 (original work) Open Assessment Technologies SA ;
*/
/**
* Component that controls the display of a countdown.
*
* You can either control the countdown externally or internally
*
* @example
* countdown(document.querySelector('.stopwatch'), {
* id : 'timer1',
* label : 'Stop watch',
* remaingTime : 60000
* })
* .on('complete', () => console.log('done'))
* .start();
*
*
* @author Bertrand Chevrier <bertrand@taotesting.com>
*/
import $ from 'jquery';
import _ from 'lodash';
import timeEncoder from 'core/encoder/time';
import component from 'ui/component';
import countdownTpl from 'taoQtiTest/runner/plugins/controls/timer/component/tpl/countdown';
import getTimerMessage from 'taoQtiTest/runner/helpers/getTimerMessage';
import tooltip from 'ui/tooltip';
import 'taoQtiTest/runner/plugins/controls/timer/component/css/countdown.css';
import moment from "moment";
//Precision is milliseconds
var precision = 1000;
/**
* Default config values, see below.
*/
var defaults = {
showBeforeStart: true,
displayWarning: true
};
/**
* time to display warnings
*/
var warningTimeout = {
info: 2000,
success: 2000,
warning: 4000,
danger: 4000,
error: 8000
};
/**
* Creates, initialize and render a countdown component.
*
* @param {jQueryElement|HTMLElement} $container - where to append the countdown
* @param {Object} config
* @param {String} config.id - the timer unique identifier
* @param {String} config.label - the text to display above the timer
* @param {String} config.type - the type of countdown (to categorize them)
* @param {String} [config.scope] - scope of a timer
* @param {Number} [config.unansweredQuestions] - number of unanswered options
* @param {Number} [config.remainingTime] - the current value of the countdown, in milliseconds
* @param {Boolean} [config.showBeforeStart = true] - do we show the time before starting
* @param {Boolean} [config.displayWarning = true] - do we display the warnings or trigger only the event
* @param {Object[]} [config.warnings] - define warnings thresholds
* @param {Number} [config.warnings.threshold] - when the warning is shown, in milliseconds
* @param {String} [config.warnings.message] - the warning message
* @param {String} [config.warnings.level = warn] - the feedback level in (success, info, warn, danger and error)
* @returns {countdown} the component, initialized and rendered
*/
export default function countdownFactory($container, config) {
let $time;
let $timeScreenreader;
/**
* @typedef {Object} countdown
*/
var countdown = component(
{
/**
* Update the countdown
* @param {Number} remainingTime - the time remaining (milliseconds)
* @param {Number} unansweredQuestions
* @returns {countdown} chains
* @fires countdown#change - when the value has changed
* @fires countdown#warn - when a threshold is reached
*/
update: function udpate(remainingTime, unansweredQuestions) {
var self = this;
var encodedTime;
var warningId;
var warningMessage;
if (!this.is('completed')) {
if (remainingTime <= 0) {
this.remainingTime = 0;
} else {
this.remainingTime = parseInt(remainingTime, 10);
}
if (this.is('rendered') && this.is('running')) {
encodedTime = timeEncoder.encode(this.remainingTime / precision);
if (encodedTime !== this.encodedTime) {
this.encodedTime = encodedTime;
const time = moment.duration(this.remainingTime / precision, 'seconds');
const hours = time.get('hours');
const minutes = time.get('minutes');
const seconds = time.get('seconds');
$time.text(this.encodedTime);
$timeScreenreader.text(
getTimerMessage(
hours,
minutes,
seconds,
unansweredQuestions,
this.config.scope
)
);
}
if (this.warnings) {
//the warnings have already be sorted
warningId = _.findLastKey(this.warnings, function(warning) {
return (
warning &&
!warning.shown &&
warning.threshold > 0 &&
warning.threshold >= self.remainingTime
);
});
if (warningId) {
this.warnings[warningId].shown = true;
if (_.isFunction(this.warnings[warningId].message)) {
warningMessage = this.warnings[warningId].message(this.remainingTime);
} else {
warningMessage = this.warnings[warningId].message;
}
/**
* Warn user the timer reach a threshold
* @event countdown#warn
* @param {String} message
* @param {String} level
*/
this.trigger('warn', warningMessage, this.warnings[warningId].level);
}
}
if (this.warningsForScreenreader) {
//the warnings have already be sorted
const screenreaderWarningId = _.findLastKey(this.warningsForScreenreader, (warning) => (
warning &&
!warning.shown &&
warning.threshold > 0 &&
warning.threshold >= self.remainingTime
));
if (screenreaderWarningId) {
this.warningsForScreenreader[screenreaderWarningId].shown = true;
/**
* Warn user the timer reach a threshold
* @event countdown#warnscreenreader
* @param {Function} message
* @param {Number} remainingTime
* @param {String} scope
*/
this.trigger(
'warnscreenreader',
this.warningsForScreenreader[screenreaderWarningId].message,
self.remainingTime,
this.warningsForScreenreader[screenreaderWarningId].scope
);
}
}
/**
* The current value has changed
* @event countdown#change
* @param {Number} remainingTime - the updated time
* @param {String} displayed - the displayed value
*/
this.trigger('change', this.remainingTime, encodedTime);
}
if (this.remainingTime === 0) {
this.complete();
}
}
return this;
},
/**
* Starts the countdown
* @returns {countdown} chains
* @fires countdown#start
*/
start: function start() {
if (this.is('rendered') && !this.is('running') && !this.is('completed')) {
this.enable();
this.setState('running', true);
/**
* The count has started
* @event countdown#start
*/
this.trigger('start');
}
return this;
},
/**
* Stops the countdown (can be restarted then)
* @returns {countdown} chains
* @fires countdown#stop
*/
stop: function stop() {
if (this.is('rendered') && this.is('running')) {
this.setState('running', false);
/**
* The count is stopped
* @event countdown#stop
*/
this.trigger('stop');
}
return this;
},
/**
* Calls to complete the countdown,
* it can't be resumed after.
*
* @returns {countdown} chains
*
* @fires countdown#complete
* @fires countdown#end
*/
complete: function complete() {
if (this.is('rendered') && this.is('running') && !this.is('completed')) {
this.stop();
this.setState('completed', true);
/**
* The countdown has ended, is completed
* @event countdown#complete
* @event countdown#end (alias)
*/
this.trigger('complete end');
}
return this;
}
},
defaults
)
.on('init', function() {
this.remainingTime = this.config.remainingTime;
this.unansweredQuestions = this.config.unansweredQuestions;
if (this.config.warnings) {
this.warnings = _.sortBy(this.config.warnings, 'threshold');
}
if (this.config.warningsForScreenreader) {
this.warningsForScreenreader = _.sortBy(this.config.warningsForScreenreader, 'threshold');
}
//auto renders
this.render($container);
})
.on('render', function() {
$time = $('.time', this.getElement());
$timeScreenreader = $('.time--screenreader', this.getElement());
if (this.config.showBeforeStart === true) {
$time.text(timeEncoder.encode(this.remainingTime / precision));
}
})
.on('warn', function(message, level) {
var countdownTooltip;
level = level || 'warning';
if (this.is('rendered') && this.is('running') && _.isString(message) && !_.isEmpty(message)) {
$time.removeClass('txt-success txt-info txt-warning txt-danger txt-error').addClass(`txt-${level}`);
if (this.config.displayWarning === true) {
countdownTooltip = tooltip.create(this.getElement(), message, {
trigger: 'manual',
theme: level,
placement: 'bottom'
});
countdownTooltip.show();
setTimeout(function() {
countdownTooltip.hide();
countdownTooltip.dispose();
}, warningTimeout[level] || 2000);
}
}
});
countdown.setTemplate(countdownTpl);
_.defer(function() {
countdown.init(config);
});
return countdown;
}