Files
Chamilo/main/inc/lib/javascript/epiclock/javascript/jquery.epiclock.js
2025-04-10 12:49:05 +02:00

1137 lines
31 KiB
JavaScript

/*!
* epiClock 3.0
*
* Copyright (c) 2008 Eric Garside
* Dual licensed under:
* MIT: http://www.opensource.org/licenses/mit-license.php
* GPLv3: http://www.opensource.org/licenses/gpl-3.0.html
*/
"use strict";
/*global Class, window, jQuery */
/*jslint white: true, browser: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, strict: true, newcap: true, immed: true, maxerr: 50, indent: 4 */
/**
* This plugin requires the jquery.dateformat plugin, which is included alongside this
* script, and is packed into the minified version.
*/
(function ($) {
//------------------------------
//
// Constants
//
//------------------------------
/**
* Clock modes for epiclock. As of epiclock 3.0, users can add: "epiclock epiclock-<mode>" classes
* to an element to automate the setting of a clock on the page.
*/
var mode =
{
/**
* Default clock type. Renders a clock which displays the current time, based
* on the client's clock.
*/
clock: 'clock',
/**
* Renders a clock which displays the current time, based on the datetime value
* passed to the clock. Allows for some server to explicitly set the starting value
* of the clock (to show the proper time, regardless of the value of the client's clock).
*/
explicit: 'explicit',
/**
* Countdown clock, displaying the time remaining until a certain datetime. Expires
* when the countdown finishes.
*/
countdown: 'countdown',
/**
* Countup clock, displaying the time since a certain datetime. If the datetime has not
* yet arrived, the clock will display negative numbers.
*/
countup: 'countup',
/**
* Exactly like the "countdown" clock, except on expiry, the clock will become a
* "countup" clock.
*/
rollover: 'rollover',
/**
* A clock which displays the number of remaining seconds until the expiry.
*/
expire: 'expire',
/**
* Similar to the "expire" clock, only it will refresh the duration when the expiry hits. Also, this clock
* tares on pause.
*/
loop: 'loop',
/**
* A counter which displays the number of seconds since the clock was started. Tares any
* time when paused. (if the clock is at 10 and is paused for 3 seconds, when resumed, it will
* be at 10 seconds).
*/
stopwatch: 'stopwatch',
/**
* A countup clock, displaying the time since a certain datetime. If the datetime has not
* yet arrived, the clock will display all zeroes.
*/
holdup: 'holdup',
/**
* A countdown clock displaying the remaining time on a timer. Similar to the expire counter, except it
* tares on pause, like the stopwatch clock.
*/
timer: 'timer'
},
//------------------------------
//
// Property Declaration
//
//------------------------------
/**
* Create a placeholder object for the epiclock manager.
*
* @param selector The selector to grab an epiclock object for.
*
* @return The epiclock instance for the given selector, or null.
*/
_ = function (selector)
{
return $(selector).data('epiclock');
},
/**
* A collection of listeners for clock events.
*
* Signature:
* {
* <clockInstance>:
* {
* <eventType>: [listener, ...],
*
* ...,
* },
*
* ...
* }
*/
events = {},
/**
* A collection of the clock instances created. By default, Clocks render each formatting
* character and label in their own HTML element. These elements are indexed by their
* formatting character and stored as elements of the "frame" object in this object.
*
* Non-formatting characters (such as labels) are still contained within their own HTML
* elements, but are not tracked in the frame object, as they are not intended to change.
*
* Signature:
* {
* <clockInstance>:
* {
* clock: <clockObject>,
*
* frame:
* {
* <formattingCharacter>: <jQueryObject>,
*
* ...
* }
* },
*
* ...
* }
*/
instances = {},
/**
* A unique identifier for each created clock.
*/
uid = 0,
/**
* Whenever a tick process begins, this variable will contain the current datetime value.
*/
now,
/**
* A "zero" date for clocks.
*/
zero = new Date(0),
/**
* The interval for tracking the clock timeout.
*/
intervalID,
/**
* Create the Clock class, defined below.
*/
Clock = function ()
{
this.__uid = uid++;
instances[this.__uid] = {clock: this, frame: {}};
};
//------------------------------
//
// Internal Methods
//
//------------------------------
/**
* Trigger an event for a clock instance.
*
* @param uid The unique id of the clock instance.
*
* @param event The event being triggered.
*
* @param params The parameters for the event listener.
*/
function triggerEvent(uid, event, params)
{
instances[uid].clock.container.triggerHandler(event, params);
if (events[uid] === undefined || events[uid][event] === undefined)
{
return;
}
$.each(events[uid][event], function (index, value)
{
if ($.isFunction(value))
{
value.apply(instances[uid].clock, params || []);
}
});
}
/**
* Terminate a counter clock.
*
* @param clock The clock to terminate.
*
* @param current The current time.
*
* @return The time to render.
*/
function terminate(clock, current)
{
triggerEvent(clock.__uid, 'timer');
switch (clock.mode)
{
case mode.holdup:
case mode.rollover:
clock.mode = mode.countup;
clock.restart(0);
return zero;
case mode.expire:
case mode.countdown:
case mode.timer:
clock.destroy();
return zero;
case mode.loop:
clock.restart();
return zero;
}
}
/**
* Given the settings for a clock, determine what the state would be if the clock ticked forward.
*
* @param clock The clock to tick foward using the current value of "now".
*
* @return False to indicate rendering should be skipped, or the date to render for the clock.
*/
function tick(clock)
{
if (clock.__paused !== undefined && clock.mode)
{
return false;
}
var current = now + clock.__displacement,
days;
switch (clock.mode)
{
case mode.holdup:
current -= clock.time;
if (current > 0 || current > -1e3)
{
return terminate(clock, current);
}
return zero;
case mode.countup:
case mode.stopwatch:
current -= clock.time;
break;
case mode.explicit:
current += clock.time;
break;
case mode.rollover:
case mode.loop:
case mode.expire:
case mode.countdown:
case mode.timer:
current = clock.time - current;
if (current < 1e3)
{
return terminate(clock, current);
}
break;
}
if (clock.displayOffset !== undefined)
{
days = parseInt($.dateformat.get('V', current), 10);
if (days > clock.__days_added)
{
clock.__days_added = days;
clock.displayOffset.days += days;
if (clock.displayOffset.days >= 365)
{
clock.displayOffset.years += Math.floor(clock.displayOffset.days / 365.4 % 365.4);
clock.displayOffset.days = Math.floor(clock.displayOffset.days % 365.4);
}
}
}
return new Date(current);
}
/**
* Compute the value for a formatting symbol for a given clock using the provided current time.
*
* @param clock The clock object.
*
* @param symbol The formatting symbol.
*
* @param current The current time.
*/
function evaluate(clock, symbol, current)
{
switch (symbol)
{
case 'Q':
return clock.displayOffset.years;
case 'E':
return clock.displayOffset.days;
case 'e':
return $.dateformat.pad(clock.displayOffset.days, 0);
default:
return $.dateformat.get(symbol, current);
}
}
/**
* The default operation for rendering an epiclock.
*
* @param frame The frame getting updated.
*
* @param value The current value for the frame.
*/
function defaultRenderer(frame, value)
{
frame.text(value);
}
/**
* Render a clock.
*
* @param clock The clock to render.
*/
function render(clock)
{
var time = tick(clock);
if (time === false)
{
return false;
}
$.each(instances[clock.__uid].frame, function (symbol, frame)
{
var value = evaluate(clock, symbol, time) + '';
if (frame.data('epiclock-last') !== value)
{
(clock.__render || defaultRenderer)(frame, value);
}
frame.data('epiclock-last', value);
});
triggerEvent(clock.__uid, 'rendered');
if (clock.container.hasClass('epiclock-wait-for-render'))
{
clock.container.removeClass('epiclock-wait-for-render');
}
}
/**
* Process all the clocks currently available.
*/
function cycleClocks()
{
now = new Date().valueOf();
$.each(instances, function ()
{
render(this.clock);
if (this.clock.__destroy)
{
this.clock.container.removeData('epiclock');
delete instances[this.clock.__uid];
}
});
}
/**
* Start the epiclock manager.
*/
function startManager()
{
if (intervalID !== undefined)
{
return;
}
$.each(instances, function ()
{
this.clock.resume();
});
intervalID = setInterval(cycleClocks, _.precision);
}
/**
* Halt the epiclock manager.
*/
function haltManager()
{
clearInterval(intervalID);
intervalID = undefined;
$.each(instances, function ()
{
this.clock.pause();
});
}
/**
* The the default formatting string for a clock.
*
* @param format The format of the clock.
*
* @return The default dateformat string to use.
*/
function defaultFormat(format)
{
switch (format)
{
case mode.clock:
case mode.explicit:
return 'F j, Y g:i:s a';
case mode.countdown:
return 'V{d} x{h} i{m} s{s}';
case mode.countup:
return 'Q{y} K{d} x{h} i{m} s{s}';
case mode.rollover:
return 'V{d} x{h} i{m} s{s}';
case mode.expire:
case mode.timer:
return 'x{h} i{m} s{s}';
case mode.loop:
return 'i{m} s{s}';
case mode.stopwatch:
return 'x{h} C{m} s{s}';
case mode.holdup:
return 'Q{y} K{d} x{h} i{m} s{s}';
}
}
/**
* Given a properties object, configure a clock instance.
*
* @param properties The configuration for the clock.
*
* @return Clock instance.
*/
function configureInstance(properties)
{
var clock = new Clock(),
modifier = 1;
clock.mode = properties.mode || mode.clock;
if (properties.offset !== undefined)
{
clock.__offset = (
(properties.offset.years || 0) * 3157056e4 +
(properties.offset.days || 0) * 864e5 +
(properties.offset.hours || 0) * 36e5 +
(properties.offset.minutes || 0) * 6e4 +
(properties.offset.seconds || 0) * 1e3
);
}
if (properties.startpaused)
{
clock.__paused = new Date().valueOf();
}
clock.__displacement = properties.utc === true || properties.gmt === true ? new Date().getTimezoneOffset() * 6e4 : 0;
clock.__render = $.isFunction(properties.renderer) ? properties.renderer : _.renderers[properties.renderer];
clock.__days_added = 0;
clock.__tare = properties.tare || false;
clock.format = properties.format || defaultFormat(clock.mode);
clock.time = (properties.time || properties.target ? new Date(properties.time || properties.target) : new Date()).valueOf();
clock.displayOffset = $.extend({years: 0, days: 0}, properties.displayOffset);
switch (clock.mode)
{
case mode.clock:
case mode.countup:
break;
case mode.explicit:
clock.__displacement -= new Date().valueOf();
break;
case mode.timer:
case mode.loop:
modifier = -1;
clock.__tare = true;
clock.__displacement -= 1e3;
break;
case mode.expire:
case mode.countdown:
case mode.rollover:
modifier = -1;
clock.__displacement -= 1e3;
break;
case mode.holdup:
if (clock.time < new Date().valueOf())
{
clock.mode = mode.countup;
}
else
{
clock.__displacement -= 1e3;
}
break;
case mode.stopwatch:
clock.__tare = true;
break;
default:
throw 'EPICLOCK_INVALID_MODE';
}
clock.__displacement += modifier * clock.__offset;
return clock;
}
/**
* Perform a tick for printing on the given clock.
*
* @param clock The clock to tick for printing.
*
* @return The current time.
*/
function printTick(clock)
{
now = new Date().valueOf();
return tick(clock);
}
//------------------------------
//
// Plugin Definition
//
//------------------------------
//------------------------------
// Error Declaration
//------------------------------
//------------------------------
// Plugin Creation
//------------------------------
$.extend(_, {
//------------------------------
// Properties
//------------------------------
/**
* The precision for timing out the clocks.
*/
precision: 500,
/**
* The modes for epiclocks.
*/
modes: mode,
/**
* Custom renderers for epiclock.
*
* Signature:
* {
* <renderer>: <renderingFunction>,
*
* ...
* }
*/
renderers: {},
//------------------------------
// Methods
//------------------------------
/**
* Add a renderer.
*
* @param key The key for this rendering function.
*
* @param renderer The rendering function.
*
* @param setup The setup function for the renderer.
*/
addRenderer: function (key, renderer, setup)
{
_.renderers[key] = renderer;
if ($.isFunction(setup))
{
_.renderers[key].setup = setup;
}
return _;
},
/**
* Make a clock inside the given container using the given template html with the provided properties.
*
* @param properties The properties of the clock.
*
* @param container The container the clock will exist within.
*
* @param template The element template.
*/
make: function (properties, container, template)
{
var clock = configureInstance(properties),
output = '',
/**
* The characters '{' and '}' are the start and end characters of an escape. Anything between these
* characters will not be treated as a formatting commands, and will merely be appended to the output
* string. When the buffering property here is true, we are in the midst of appending escaped characters
* to the output, and the formatting check should therefore be skipped.
*/
buffering = false,
char = '',
index = 0,
format = clock.format.split(''),
containerClass = typeof properties.renderer === "string" ? ' epiclock-' + properties.renderer : '';
container = $(container).data('epiclock', clock).addClass('epiclock-container epiclock-wait-for-render' + containerClass);
template = $(template || '<span></span>');
for (; index < format.length; index++)
{
char = format[index] + '';
switch (char)
{
case ' ':
if (buffering)
{
output += char;
}
else
{
template.clone(true).addClass('epiclock epiclock-spacer').appendTo(container);
}
break;
case '{':
case '}':
buffering = char === '{';
if (!buffering)
{
template.clone(true).addClass('epiclock').html(output).appendTo(container);
output = '';
}
break;
default:
if (!buffering && $.dateformat.hasRule(char))
{
instances[clock.__uid].frame[char] = template
.clone(true).addClass('epiclock epiclock-digit').data('epiclock-encoding', char).appendTo(container);
}
else if (!buffering)
{
template.clone(true).addClass('epiclock epiclock-separator').html(char).appendTo(container);
}
else
{
output += char;
}
break;
}
}
clock.container = container;
if (clock.__render !== undefined && $.isFunction(clock.__render.setup))
{
clock.__render.setup.apply(clock, [container]);
}
startManager();
return clock;
},
/**
* Bounce the manager.
*
* @param precision The precision to set in the manager upon restart.
*/
bounce: function (precision)
{
if (precision !== undefined)
{
_.precision = precision;
}
_.pause().resume();
},
/**
* Pause all clocks.
*/
pause: function ()
{
haltManager();
return _;
},
/**
* Start all clocks.
*/
resume: function ()
{
startManager();
return _;
},
/**
* Pause the manager and destroy all clocks.
*/
destroy: function ()
{
clearInterval(intervalID);
$.each(instances, function ()
{
this.clock.destroy();
});
return _;
},
/**
* Restart all clocks.
*/
restart: function ()
{
$.each(instances, function ()
{
this.clock.restart();
});
return _;
}
});
//------------------------------
// Core Extension
//------------------------------
$.dateformat.rules({
//------------------------------
// Custom Date Rules
//------------------------------
Q: function ()
{
return '%displayOffset-years%';
},
E: function ()
{
return '%displayOffset-days%';
},
e: function ()
{
return '%displayOffset-days-pad%';
}
});
//------------------------------
//
// Class Definition
//
//------------------------------
$.extend(Clock.prototype, {
//------------------------------
// Internal Properties
//------------------------------
/**
* A unique identifier for this clock instance.
*/
__uid: undefined,
/**
* The rendering function used by this clock instance.
*/
__render: undefined,
/**
* The displacement of a clock is some number of milliseconds
* which is added to the computed value of the clock before rendering.
*
* This property allows for timers to use the default "tick" mechanics
* which drive all clocks. By setting a negative displacement equal to
* the number of seconds since the epoch when the clock was started, we
* basically zero out the date object, and can then compute timers from
* zero seconds.
*
* This property is generated automatically based on whatever values are
* passed into the "offset" hash, depending on what mode of clock this is.
*/
__displacement: undefined,
/**
* When dealing with display offset days, the clock adds the actual number of days
* since the start time to the display offset days, to update them whenever the day
* actually rolls over. We need to keep track of how many days have been added to the
* display offset, so we can keep computation and display separate, while still letting
* computation impact display.
*/
__days_added: undefined,
/**
* Whenever a clock is paused, this variable tracks the datetime when the pause first
* started. Whenever the clock is resumed, this value is marked as undefined. By the nature
* of this interaction, it should be considered an implicit flag. If it is undefined, the clock
* is not paused. If it is any value other than undefined, the clock is paused.
*/
__paused: undefined,
/**
* Offset to the current time. These values are applied to the
* computed time when running, and are used to add some displacement
* to the clock value. These values directly impact the computed time.
*/
__offset: 0,
/**
* If true, the clock will tare on pause, to reset the start time on the clock to represent a change
* in the start time of the clock, to allow for pauses not to count against the rendered time of a
* clock.
*/
__tare: false,
/**
* When true, the clock will be destroyed after the next render action occurs.
*/
__destroy: false,
//------------------------------
// Properties
//------------------------------
/**
* Tracks extra offsets for days and years for counter clocks. Unlike the
* offset object, displayOffset has no impact on computation, and is merely
* used to add untrackable date support (pre 1970) to the clock.
*
* Signature:
* {
* days: <dayDisplayOffset>,
*
* years: <yearDisplayOffset>
* }
*/
displayOffset: undefined,
/**
* Date object representing the base time used for computation in the clock.
*/
time: undefined,
/**
* String describing the format this clock should be rendered with.
*
* See the jquery.dateformat plugin for formatting characters.
*/
format: undefined,
/**
* The mode this clock is in.
*/
mode: undefined,
/**
* The element representing the clocks container in the DOM.
*/
container: undefined,
//------------------------------
// Methods
//------------------------------
/**
* Bind an event listener to this clock.
*
* @param event The event to bind on.
*
* @param listener The listener function to notify when the event occurs.
*/
bind: function (event, listener)
{
if (events[this.__uid] === undefined)
{
events[this.__uid] = {};
}
if (events[this.__uid][event] === undefined)
{
events[this.__uid][event] = [listener];
}
else
{
events[this.__uid][event].push(listener);
}
return this;
},
/**
* Pause the clock.
*/
pause: function ()
{
if (this.__paused === undefined)
{
this.__paused = new Date().valueOf();
}
triggerEvent(this.__uid, 'pause');
},
/**
* Resume the clock.
*/
resume: function ()
{
if (this.__paused === undefined)
{
return;
}
triggerEvent(this.__uid, 'resume');
if (this.__tare)
{
this.__displacement += (this.__paused - new Date().valueOf());
}
this.__paused = undefined;
},
/**
* Toggle the pause status of the clock.
*/
toggle: function ()
{
if (this.__paused === undefined)
{
this.pause();
}
else
{
this.resume();
}
},
/**
* Destroy the clock.
*/
destroy: function ()
{
this.__destroy = true;
triggerEvent(this.__uid, 'destroy');
},
/**
* Reset the clock to use the current time as the start time for the clock.
*
* @param displacement The new value for the displacement.
*/
restart: function (displacement)
{
if (displacement !== undefined)
{
this.__displacement = displacement;
}
this.time = now.valueOf();
},
/**
* Print a string version of the clock's current time.
*
* @param format If a special format other than the default is to be used.
*
* @return The formatted string.
*/
print: function (format)
{
var value = $.dateformat.format(this.format, printTick(this));
if (this.displayOffset !== undefined)
{
return value
.replace('%displayOffset-days%', this.displayOffset.years)
.replace('%displayOffset-days%', this.displayOffset.days)
.replace('%displayOffset-days-pad%', $.dateformat.pad(this.displayOffset.days, 0));
}
return value;
}
});
//------------------------------
//
// jQuery Hooks
//
//------------------------------
$.epiclock = _;
/**
* Element builder for epiclock.
*
* @param properties The properties for building the epiclock.
*/
$.fn.epiclock = function (properties)
{
return this.each(function ()
{
var container = $(this),
template;
properties = properties || {};
if (properties.template !== undefined)
{
template = properties.template;
delete properties.template;
}
else if (container.children().length > 0)
{
template = container.children().remove().eq(0);
}
$.epiclock.make(properties, container, template);
});
};
//------------------------------
//
// Event Bindings
//
//------------------------------
//------------------------------
//
// Startup Code
//
//------------------------------
}(jQuery));