708 lines
18 KiB
JavaScript
708 lines
18 KiB
JavaScript
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
|
|
|
|
var H5P = H5P || {};
|
|
|
|
/**
|
|
* H5P-Timer
|
|
*
|
|
* General purpose timer that can be used by other H5P libraries.
|
|
*
|
|
* @param {H5P.jQuery} $
|
|
*/
|
|
H5P.Timer = function ($, EventDispatcher) {
|
|
/**
|
|
* Create a timer.
|
|
*
|
|
* @constructor
|
|
* @param {number} [interval=Timer.DEFAULT_INTERVAL] - The update interval.
|
|
*/
|
|
function Timer() {
|
|
var interval = arguments.length <= 0 || arguments[0] === undefined ? Timer.DEFAULT_INTERVAL : arguments[0];
|
|
|
|
var self = this;
|
|
|
|
// time on clock and the time the clock has run
|
|
var clockTimeMilliSeconds = 0;
|
|
var playingTimeMilliSeconds = 0;
|
|
|
|
// used to update recurring notifications
|
|
var clockUpdateMilliSeconds = 0;
|
|
|
|
// indicators for total running time of the timer
|
|
var firstDate = null;
|
|
var startDate = null;
|
|
var lastDate = null;
|
|
|
|
// update loop
|
|
var loop = null;
|
|
|
|
// timer status
|
|
var status = Timer.STOPPED;
|
|
|
|
// indicate counting direction
|
|
var mode = Timer.FORWARD;
|
|
|
|
// notifications
|
|
var notifications = [];
|
|
|
|
// counter for notifications;
|
|
var notificationsIdCounter = 0;
|
|
|
|
// Inheritance
|
|
H5P.EventDispatcher.call(self);
|
|
|
|
// sanitize interval
|
|
if (Timer.isInteger(interval)) {
|
|
interval = Math.max(interval, 1);
|
|
}
|
|
else {
|
|
interval = Timer.DEFAULT_INTERVAL;
|
|
}
|
|
|
|
/**
|
|
* Get the timer status.
|
|
*
|
|
* @public
|
|
* @return {number} The timer status.
|
|
*/
|
|
self.getStatus = function () {
|
|
return status;
|
|
};
|
|
|
|
/**
|
|
* Get the timer mode.
|
|
*
|
|
* @public
|
|
* @return {number} The timer mode.
|
|
*/
|
|
self.getMode = function () {
|
|
return mode;
|
|
};
|
|
|
|
/**
|
|
* Get the time that's on the clock.
|
|
*
|
|
* @private
|
|
* @return {number} The time on the clock.
|
|
*/
|
|
var getClockTime = function getClockTime() {
|
|
return clockTimeMilliSeconds;
|
|
};
|
|
|
|
/**
|
|
* Get the time the timer was playing so far.
|
|
*
|
|
* @private
|
|
* @return {number} The time played.
|
|
*/
|
|
var getPlayingTime = function getPlayingTime() {
|
|
return playingTimeMilliSeconds;
|
|
};
|
|
|
|
/**
|
|
* Get the total running time from play() until stop().
|
|
*
|
|
* @private
|
|
* @return {number} The total running time.
|
|
*/
|
|
var getRunningTime = function getRunningTime() {
|
|
if (!firstDate) {
|
|
return 0;
|
|
}
|
|
if (status !== Timer.STOPPED) {
|
|
return new Date().getTime() - firstDate.getTime();
|
|
}
|
|
else {
|
|
return !lastDate ? 0 : lastDate.getTime() - firstDate;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get one of the times.
|
|
*
|
|
* @public
|
|
* @param {number} [type=Timer.TYPE_CLOCK] - Type of the time to get.
|
|
* @return {number} Clock Time, Playing Time or Running Time.
|
|
*/
|
|
self.getTime = function () {
|
|
var type = arguments.length <= 0 || arguments[0] === undefined ? Timer.TYPE_CLOCK : arguments[0];
|
|
|
|
if (!Timer.isInteger(type)) {
|
|
return;
|
|
}
|
|
// break will never be reached, but for consistency...
|
|
switch (type) {
|
|
case Timer.TYPE_CLOCK:
|
|
return getClockTime();
|
|
break;
|
|
case Timer.TYPE_PLAYING:
|
|
return getPlayingTime();
|
|
break;
|
|
case Timer.TYPE_RUNNING:
|
|
return getRunningTime();
|
|
break;
|
|
default:
|
|
return getClockTime();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the clock time.
|
|
*
|
|
* @public
|
|
* @param {number} time - The time in milliseconds.
|
|
*/
|
|
self.setClockTime = function (time) {
|
|
if ($.type(time) === 'string') {
|
|
time = Timer.toMilliseconds(time);
|
|
}
|
|
if (!Timer.isInteger(time)) {
|
|
return;
|
|
}
|
|
// notifications only need an update if changing clock against direction
|
|
clockUpdateMilliSeconds = (time - clockTimeMilliSeconds) * mode < 0 ? time - clockTimeMilliSeconds : 0;
|
|
clockTimeMilliSeconds = time;
|
|
};
|
|
|
|
/**
|
|
* Reset the timer.
|
|
*
|
|
* @public
|
|
*/
|
|
self.reset = function () {
|
|
if (status !== Timer.STOPPED) {
|
|
return;
|
|
}
|
|
clockTimeMilliSeconds = 0;
|
|
playingTimeMilliSeconds = 0;
|
|
|
|
firstDate = null;
|
|
lastDate = null;
|
|
|
|
loop = null;
|
|
|
|
notifications = [];
|
|
notificationsIdCounter = 0;
|
|
self.trigger('reset', {}, {bubbles: true, external: true});
|
|
};
|
|
|
|
/**
|
|
* Set timer mode.
|
|
*
|
|
* @public
|
|
* @param {number} mode - The timer mode.
|
|
*/
|
|
self.setMode = function (direction) {
|
|
if (direction !== Timer.FORWARD && direction !== Timer.BACKWARD) {
|
|
return;
|
|
}
|
|
mode = direction;
|
|
};
|
|
|
|
/**
|
|
* Start the timer.
|
|
*
|
|
* @public
|
|
*/
|
|
self.play = function () {
|
|
if (status === Timer.PLAYING) {
|
|
return;
|
|
}
|
|
if (!firstDate) {
|
|
firstDate = new Date();
|
|
}
|
|
startDate = new Date();
|
|
status = Timer.PLAYING;
|
|
self.trigger('play', {}, {bubbles: true, external: true});
|
|
update();
|
|
};
|
|
|
|
/**
|
|
* Pause the timer.
|
|
*
|
|
* @public
|
|
*/
|
|
self.pause = function () {
|
|
if (status !== Timer.PLAYING) {
|
|
return;
|
|
}
|
|
status = Timer.PAUSED;
|
|
self.trigger('pause', {}, {bubbles: true, external: true});
|
|
};
|
|
|
|
/**
|
|
* Stop the timer.
|
|
*
|
|
* @public
|
|
*/
|
|
self.stop = function () {
|
|
if (status === Timer.STOPPED) {
|
|
return;
|
|
}
|
|
lastDate = new Date();
|
|
status = Timer.STOPPED;
|
|
self.trigger('stop', {}, {bubbles: true, external: true});
|
|
};
|
|
|
|
/**
|
|
* Update the timer until Timer.STOPPED.
|
|
*
|
|
* @private
|
|
*/
|
|
var update = function update() {
|
|
var currentMilliSeconds = 0;
|
|
// stop because requested
|
|
if (status === Timer.STOPPED) {
|
|
clearTimeout(loop);
|
|
return;
|
|
}
|
|
|
|
//stop because countdown reaches 0
|
|
if (mode === Timer.BACKWARD && clockTimeMilliSeconds <= 0) {
|
|
self.stop();
|
|
return;
|
|
}
|
|
|
|
// update times
|
|
if (status === Timer.PLAYING) {
|
|
currentMilliSeconds = new Date().getTime() - startDate;
|
|
clockTimeMilliSeconds += currentMilliSeconds * mode;
|
|
playingTimeMilliSeconds += currentMilliSeconds;
|
|
}
|
|
startDate = new Date();
|
|
|
|
checkNotifications();
|
|
|
|
loop = setTimeout(function () {
|
|
update();
|
|
}, interval);
|
|
};
|
|
|
|
/**
|
|
* Get next notification id.
|
|
*
|
|
* @private
|
|
* @return {number} id - The next id.
|
|
*/
|
|
var getNextNotificationId = function getNextNotificationId() {
|
|
return notificationsIdCounter++;
|
|
};
|
|
|
|
/**
|
|
* Set a notification
|
|
*
|
|
* @public
|
|
* @param {Object|String} params - Parameters for the notification.
|
|
* @callback callback - Callback function.
|
|
* @return {number} ID of the notification.
|
|
*/
|
|
self.notify = function (params, callback) {
|
|
var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getNextNotificationId();
|
|
|
|
// common default values for the clock timer
|
|
// TODO: find a better place for this, maybe a JSON file?
|
|
var defaults = {};
|
|
defaults['every_tenth_second'] = { "repeat": 100 };
|
|
defaults['every_second'] = { "repeat": 1000 };
|
|
defaults['every_minute'] = { "repeat": 60000 };
|
|
defaults['every_hour'] = { "repeat": 3600000 };
|
|
|
|
// Sanity check for callback function
|
|
if (!callback instanceof Function) {
|
|
return;
|
|
}
|
|
|
|
// Get default values
|
|
if ($.type(params) === 'string') {
|
|
params = defaults[params];
|
|
}
|
|
|
|
if (params !== null && (typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') {
|
|
// Sanitize type
|
|
if (!params.type) {
|
|
params.type = Timer.TYPE_CLOCK;
|
|
}
|
|
else {
|
|
if (!Timer.isInteger(params.type)) {
|
|
return;
|
|
}
|
|
if (params.type < Timer.TYPE_CLOCK || params.type > Timer.TYPE_RUNNING) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Sanitize mode
|
|
if (!params.mode) {
|
|
params.mode = Timer.NOTIFY_ABSOLUTE;
|
|
}
|
|
else {
|
|
if (!Timer.isInteger(params.mode)) {
|
|
return;
|
|
}
|
|
if (params.mode < Timer.NOTIFY_ABSOLUTE || params.type > Timer.NOTIFY_RELATIVE) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Sanitize calltime
|
|
if (!params.calltime) {
|
|
params.calltime = params.mode === Timer.NOTIFY_ABSOLUTE ? self.getTime(params.type) : 0;
|
|
}
|
|
else {
|
|
if ($.type(params.calltime) === 'string') {
|
|
params.calltime = Timer.toMilliseconds(params.calltime);
|
|
}
|
|
if (!Timer.isInteger(params.calltime)) {
|
|
return;
|
|
}
|
|
if (params.calltime < 0) {
|
|
return;
|
|
}
|
|
if (params.mode === Timer.NOTIFY_RELATIVE) {
|
|
params.calltime = Math.max(params.calltime, interval);
|
|
if (params.type === Timer.TYPE_CLOCK) {
|
|
// clock could be running backwards
|
|
params.calltime *= mode;
|
|
}
|
|
params.calltime += self.getTime(params.type);
|
|
}
|
|
}
|
|
|
|
// Sanitize repeat
|
|
if ($.type(params.repeat) === 'string') {
|
|
params.repeat = Timer.toMilliseconds(params.repeat);
|
|
}
|
|
// repeat must be >= interval (ideally multiple of interval)
|
|
if (params.repeat !== undefined) {
|
|
if (!Timer.isInteger(params.repeat)) {
|
|
return;
|
|
}
|
|
params.repeat = Math.max(params.repeat, interval);
|
|
}
|
|
}
|
|
else {
|
|
// neither object nor string
|
|
return;
|
|
}
|
|
|
|
// add notification
|
|
notifications.push({
|
|
'id': id,
|
|
'type': params.type,
|
|
'calltime': params.calltime,
|
|
'repeat': params.repeat,
|
|
'callback': callback
|
|
});
|
|
|
|
return id;
|
|
};
|
|
|
|
/**
|
|
* Remove a notification.
|
|
*
|
|
* @public
|
|
* @param {number} id - The id of the notification.
|
|
*/
|
|
self.clearNotification = function (id) {
|
|
notifications = $.grep(notifications, function (item) {
|
|
return item.id === id;
|
|
}, true);
|
|
};
|
|
|
|
/**
|
|
* Set a new starting time for notifications.
|
|
*
|
|
* @private
|
|
* @param elements {Object] elements - The notifications to be updated.
|
|
* @param deltaMilliSeconds {Number} - The time difference to be set.
|
|
*/
|
|
var updateNotificationTime = function updateNotificationTime(elements, deltaMilliSeconds) {
|
|
if (!Timer.isInteger(deltaMilliSeconds)) {
|
|
return;
|
|
}
|
|
elements.forEach(function (element) {
|
|
// remove notification
|
|
self.clearNotification(element.id);
|
|
|
|
//rebuild notification with new data
|
|
self.notify({
|
|
'type': element.type,
|
|
'calltime': self.getTime(element.type) + deltaMilliSeconds,
|
|
'repeat': element.repeat
|
|
}, element.callback, element.id);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Check notifications for necessary callbacks.
|
|
*
|
|
* @private
|
|
*/
|
|
var checkNotifications = function checkNotifications() {
|
|
var backwards = 1;
|
|
var elements = [];
|
|
|
|
// update recurring clock notifications if clock was changed
|
|
if (clockUpdateMilliSeconds !== 0) {
|
|
elements = $.grep(notifications, function (item) {
|
|
return item.type === Timer.TYPE_CLOCK && item.repeat != undefined;
|
|
});
|
|
updateNotificationTime(elements, clockUpdateMilliSeconds);
|
|
clockUpdateMilliSeconds = 0;
|
|
}
|
|
|
|
// check all notifications for triggering
|
|
notifications.forEach(function (element) {
|
|
/*
|
|
* trigger if notification time is in the past
|
|
* which means calltime >= Clock Time if mode is BACKWARD (= -1)
|
|
*/
|
|
backwards = element.type === Timer.TYPE_CLOCK ? mode : 1;
|
|
if (element.calltime * backwards <= self.getTime(element.type) * backwards) {
|
|
// notify callback function
|
|
element.callback.apply(this);
|
|
|
|
// remove notification
|
|
self.clearNotification(element.id);
|
|
|
|
// You could use updateNotificationTime() here, but waste some time
|
|
|
|
// rebuild notification if it should be repeated
|
|
if (element.repeat) {
|
|
self.notify({
|
|
'type': element.type,
|
|
'calltime': self.getTime(element.type) + element.repeat * backwards,
|
|
'repeat': element.repeat
|
|
}, element.callback, element.id);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
// Inheritance
|
|
Timer.prototype = Object.create(H5P.EventDispatcher.prototype);
|
|
Timer.prototype.constructor = Timer;
|
|
|
|
/**
|
|
* Generate timecode elements from milliseconds.
|
|
*
|
|
* @private
|
|
* @param {number} milliSeconds - The milliseconds.
|
|
* @return {Object} The timecode elements.
|
|
*/
|
|
var toTimecodeElements = function toTimecodeElements(milliSeconds) {
|
|
var years = 0;
|
|
var month = 0;
|
|
var weeks = 0;
|
|
var days = 0;
|
|
var hours = 0;
|
|
var minutes = 0;
|
|
var seconds = 0;
|
|
var tenthSeconds = 0;
|
|
|
|
if (!Timer.isInteger(milliSeconds)) {
|
|
return;
|
|
}
|
|
milliSeconds = Math.round(milliSeconds / 100);
|
|
tenthSeconds = milliSeconds - Math.floor(milliSeconds / 10) * 10;
|
|
seconds = Math.floor(milliSeconds / 10);
|
|
minutes = Math.floor(seconds / 60);
|
|
hours = Math.floor(minutes / 60);
|
|
days = Math.floor(hours / 24);
|
|
weeks = Math.floor(days / 7);
|
|
month = Math.floor(days / 30.4375); // roughly (30.4375 = mean of 4 years)
|
|
years = Math.floor(days / 365); // roughly (no leap years considered)
|
|
return {
|
|
years: years,
|
|
month: month,
|
|
weeks: weeks,
|
|
days: days,
|
|
hours: hours,
|
|
minutes: minutes,
|
|
seconds: seconds,
|
|
tenthSeconds: tenthSeconds
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Extract humanized time element from time for concatenating.
|
|
*
|
|
* @public
|
|
* @param {number} milliSeconds - The milliSeconds.
|
|
* @param {string} element - Time element: hours, minutes, seconds or tenthSeconds.
|
|
* @param {boolean} [rounded=false] - If true, element value will be rounded.
|
|
* @return {number} The time element.
|
|
*/
|
|
Timer.extractTimeElement = function (time, element) {
|
|
var rounded = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
|
|
|
|
var timeElements = null;
|
|
|
|
if ($.type(time) === 'string') {
|
|
time = Timer.toMilliseconds(time);
|
|
}
|
|
if (!Timer.isInteger(time)) {
|
|
return;
|
|
}
|
|
if ($.type(element) !== 'string') {
|
|
return;
|
|
}
|
|
if ($.type(rounded) !== 'boolean') {
|
|
return;
|
|
}
|
|
|
|
if (rounded) {
|
|
timeElements = {
|
|
years: Math.round(time / 31536000000),
|
|
month: Math.round(time / 2629800000),
|
|
weeks: Math.round(time / 604800000),
|
|
days: Math.round(time / 86400000),
|
|
hours: Math.round(time / 3600000),
|
|
minutes: Math.round(time / 60000),
|
|
seconds: Math.round(time / 1000),
|
|
tenthSeconds: Math.round(time / 100)
|
|
};
|
|
}
|
|
else {
|
|
timeElements = toTimecodeElements(time);
|
|
}
|
|
|
|
return timeElements[element];
|
|
};
|
|
|
|
/**
|
|
* Convert time in milliseconds to timecode.
|
|
*
|
|
* @public
|
|
* @param {number} milliSeconds - The time in milliSeconds.
|
|
* @return {string} The humanized timecode.
|
|
*/
|
|
Timer.toTimecode = function (milliSeconds) {
|
|
var timecodeElements = null;
|
|
var timecode = '';
|
|
|
|
var minutes = 0;
|
|
var seconds = 0;
|
|
|
|
if (!Timer.isInteger(milliSeconds)) {
|
|
return;
|
|
}
|
|
if (milliSeconds < 0) {
|
|
return;
|
|
}
|
|
|
|
timecodeElements = toTimecodeElements(milliSeconds);
|
|
minutes = Math.floor(timecodeElements['minutes'] % 60);
|
|
seconds = Math.floor(timecodeElements['seconds'] % 60);
|
|
|
|
// create timecode
|
|
if (timecodeElements['hours'] > 0) {
|
|
timecode += timecodeElements['hours'] + ':';
|
|
}
|
|
if (minutes < 10) {
|
|
timecode += '0';
|
|
}
|
|
timecode += minutes + ':';
|
|
if (seconds < 10) {
|
|
timecode += '0';
|
|
}
|
|
timecode += seconds + '.';
|
|
timecode += timecodeElements['tenthSeconds'];
|
|
|
|
return timecode;
|
|
};
|
|
|
|
/**
|
|
* Convert timecode to milliseconds.
|
|
*
|
|
* @public
|
|
* @param {string} timecode - The timecode.
|
|
* @return {number} Milliseconds derived from timecode
|
|
*/
|
|
Timer.toMilliseconds = function (timecode) {
|
|
var head = [];
|
|
var tail = '';
|
|
|
|
var hours = 0;
|
|
var minutes = 0;
|
|
var seconds = 0;
|
|
var tenthSeconds = 0;
|
|
|
|
if (!Timer.isTimecode(timecode)) {
|
|
return;
|
|
}
|
|
|
|
// thx to the regexp we know everything can be converted to a legit integer in range
|
|
head = timecode.split('.')[0].split(':');
|
|
while (head.length < 3) {
|
|
head = ['0'].concat(head);
|
|
}
|
|
hours = parseInt(head[0]);
|
|
minutes = parseInt(head[1]);
|
|
seconds = parseInt(head[2]);
|
|
|
|
tail = timecode.split('.')[1];
|
|
if (tail) {
|
|
tenthSeconds = Math.round(parseInt(tail) / Math.pow(10, tail.length - 1));
|
|
}
|
|
|
|
return (hours * 36000 + minutes * 600 + seconds * 10 + tenthSeconds) * 100;
|
|
};
|
|
|
|
/**
|
|
* Check if a string is a timecode.
|
|
*
|
|
* @public
|
|
* @param {string} value - String to check
|
|
* @return {boolean} true, if string is a timecode
|
|
*/
|
|
Timer.isTimecode = function (value) {
|
|
var reg_timecode = /((((((\d+:)?([0-5]))?\d:)?([0-5]))?\d)(\.\d+)?)/;
|
|
|
|
if ($.type(value) !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
return value === value.match(reg_timecode)[0] ? true : false;
|
|
};
|
|
|
|
// Workaround for IE and potentially other browsers within Timer object
|
|
Timer.isInteger = Timer.isInteger || function(value) {
|
|
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
|
|
};
|
|
|
|
// Timer states
|
|
/** @constant {number} */
|
|
Timer.STOPPED = 0;
|
|
/** @constant {number} */
|
|
Timer.PLAYING = 1;
|
|
/** @constant {number} */
|
|
Timer.PAUSED = 2;
|
|
|
|
// Timer directions
|
|
/** @constant {number} */
|
|
Timer.FORWARD = 1;
|
|
/** @constant {number} */
|
|
Timer.BACKWARD = -1;
|
|
|
|
/** @constant {number} */
|
|
Timer.DEFAULT_INTERVAL = 10;
|
|
|
|
// Counter types
|
|
/** @constant {number} */
|
|
Timer.TYPE_CLOCK = 0;
|
|
/** @constant {number} */
|
|
Timer.TYPE_PLAYING = 1;
|
|
/** @constant {number} */
|
|
Timer.TYPE_RUNNING = 2;
|
|
|
|
// Notification types
|
|
/** @constant {number} */
|
|
Timer.NOTIFY_ABSOLUTE = 0;
|
|
/** @constant {number} */
|
|
Timer.NOTIFY_RELATIVE = 1;
|
|
|
|
return Timer;
|
|
}(H5P.jQuery, H5P.EventDispatcher);
|