aboutsummaryrefslogtreecommitdiff
path: root/static/jquery.countdown.js
diff options
context:
space:
mode:
authorPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
committerPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
commit89a67770e66d11740948e90a41db6cee0482cf8e (patch)
treebe858515fb789a89d68f94975690ab019813726c /static/jquery.countdown.js
new version.
Diffstat (limited to 'static/jquery.countdown.js')
-rw-r--r--static/jquery.countdown.js759
1 files changed, 759 insertions, 0 deletions
diff --git a/static/jquery.countdown.js b/static/jquery.countdown.js
new file mode 100644
index 0000000..27e2f4a
--- /dev/null
+++ b/static/jquery.countdown.js
@@ -0,0 +1,759 @@
+/* http://keith-wood.name/countdown.html
+ Countdown for jQuery v1.5.8.
+ Written by Keith Wood (kbwood{at}iinet.com.au) January 2008.
+ Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
+ MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
+ Please attribute the author if you use it. */
+
+/* Display a countdown timer.
+ Attach it with options like:
+ $('div selector').countdown(
+ {until: new Date(2009, 1 - 1, 1, 0, 0, 0), onExpiry: happyNewYear}); */
+
+(function($) { // Hide scope, no $ conflict
+
+/* Countdown manager. */
+function Countdown() {
+ this.regional = []; // Available regional settings, indexed by language code
+ this.regional[''] = { // Default regional settings
+ // The display texts for the counters
+ labels: ['Years', 'Months', 'Weeks', 'Days', 'Hours', 'Minutes', 'Seconds'],
+ // The display texts for the counters if only one
+ labels1: ['Year', 'Month', 'Week', 'Day', 'Hour', 'Minute', 'Second'],
+ compactLabels: ['y', 'm', 'w', 'd'], // The compact texts for the counters
+ whichLabels: null, // Function to determine which labels to use
+ timeSeparator: ':', // Separator for time periods
+ isRTL: false // True for right-to-left languages, false for left-to-right
+ };
+ this._defaults = {
+ until: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count down to
+ // or numeric for seconds offset, or string for unit offset(s):
+ // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
+ since: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count up from
+ // or numeric for seconds offset, or string for unit offset(s):
+ // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
+ timezone: null, // The timezone (hours or minutes from GMT) for the target times,
+ // or null for client local
+ serverSync: null, // A function to retrieve the current server time for synchronisation
+ format: 'dHMS', // Format for display - upper case for always, lower case only if non-zero,
+ // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
+ layout: '', // Build your own layout for the countdown
+ compact: false, // True to display in a compact format, false for an expanded one
+ significant: 0, // The number of periods with values to show, zero for all
+ description: '', // The description displayed for the countdown
+ expiryUrl: '', // A URL to load upon expiry, replacing the current page
+ expiryText: '', // Text to display upon expiry, replacing the countdown
+ alwaysExpire: false, // True to trigger onExpiry even if never counted down
+ onExpiry: null, // Callback when the countdown expires -
+ // receives no parameters and 'this' is the containing division
+ onTick: null, // Callback when the countdown is updated -
+ // receives int[7] being the breakdown by period (based on format)
+ // and 'this' is the containing division
+ tickInterval: 1 // Interval (seconds) between onTick callbacks
+ };
+ $.extend(this._defaults, this.regional['']);
+ this._serverSyncs = [];
+}
+
+var PROP_NAME = 'countdown';
+
+var Y = 0; // Years
+var O = 1; // Months
+var W = 2; // Weeks
+var D = 3; // Days
+var H = 4; // Hours
+var M = 5; // Minutes
+var S = 6; // Seconds
+
+$.extend(Countdown.prototype, {
+ /* Class name added to elements to indicate already configured with countdown. */
+ markerClassName: 'hasCountdown',
+
+ /* Shared timer for all countdowns. */
+ _timer: setInterval(function() { $.countdown._updateTargets(); }, 980),
+ /* List of currently active countdown targets. */
+ _timerTargets: [],
+
+ /* Override the default settings for all instances of the countdown widget.
+ @param options (object) the new settings to use as defaults */
+ setDefaults: function(options) {
+ this._resetExtraLabels(this._defaults, options);
+ extendRemove(this._defaults, options || {});
+ },
+
+ /* Convert a date/time to UTC.
+ @param tz (number) the hour or minute offset from GMT, e.g. +9, -360
+ @param year (Date) the date/time in that timezone or
+ (number) the year in that timezone
+ @param month (number, optional) the month (0 - 11) (omit if year is a Date)
+ @param day (number, optional) the day (omit if year is a Date)
+ @param hours (number, optional) the hour (omit if year is a Date)
+ @param mins (number, optional) the minute (omit if year is a Date)
+ @param secs (number, optional) the second (omit if year is a Date)
+ @param ms (number, optional) the millisecond (omit if year is a Date)
+ @return (Date) the equivalent UTC date/time */
+ UTCDate: function(tz, year, month, day, hours, mins, secs, ms) {
+ if (typeof year == 'object' && year.constructor == Date) {
+ ms = year.getMilliseconds();
+ secs = year.getSeconds();
+ mins = year.getMinutes();
+ hours = year.getHours();
+ day = year.getDate();
+ month = year.getMonth();
+ year = year.getFullYear();
+ }
+ var d = new Date();
+ d.setUTCFullYear(year);
+ d.setUTCDate(1);
+ d.setUTCMonth(month || 0);
+ d.setUTCDate(day || 1);
+ d.setUTCHours(hours || 0);
+ d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz));
+ d.setUTCSeconds(secs || 0);
+ d.setUTCMilliseconds(ms || 0);
+ return d;
+ },
+
+ /* Convert a set of periods into seconds.
+ Averaged for months and years.
+ @param periods (number[7]) the periods per year/month/week/day/hour/minute/second
+ @return (number) the corresponding number of seconds */
+ periodsToSeconds: function(periods) {
+ return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 +
+ periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6];
+ },
+
+ /* Retrieve one or more settings values.
+ @param name (string, optional) the name of the setting to retrieve
+ or 'all' for all instance settings or omit for all default settings
+ @return (any) the requested setting(s) */
+ _settingsCountdown: function(target, name) {
+ if (!name) {
+ return $.countdown._defaults;
+ }
+ var inst = $.data(target, PROP_NAME);
+ return (name == 'all' ? inst.options : inst.options[name]);
+ },
+
+ /* Attach the countdown widget to a div.
+ @param target (element) the containing division
+ @param options (object) the initial settings for the countdown */
+ _attachCountdown: function(target, options) {
+ var $target = $(target);
+ if ($target.hasClass(this.markerClassName)) {
+ return;
+ }
+ $target.addClass(this.markerClassName);
+ var inst = {options: $.extend({}, options),
+ _periods: [0, 0, 0, 0, 0, 0, 0]};
+ $.data(target, PROP_NAME, inst);
+ this._changeCountdown(target);
+ },
+
+ /* Add a target to the list of active ones.
+ @param target (element) the countdown target */
+ _addTarget: function(target) {
+ if (!this._hasTarget(target)) {
+ this._timerTargets.push(target);
+ }
+ },
+
+ /* See if a target is in the list of active ones.
+ @param target (element) the countdown target
+ @return (boolean) true if present, false if not */
+ _hasTarget: function(target) {
+ return ($.inArray(target, this._timerTargets) > -1);
+ },
+
+ /* Remove a target from the list of active ones.
+ @param target (element) the countdown target */
+ _removeTarget: function(target) {
+ this._timerTargets = $.map(this._timerTargets,
+ function(value) { return (value == target ? null : value); }); // delete entry
+ },
+
+ /* Update each active timer target. */
+ _updateTargets: function() {
+ for (var i = this._timerTargets.length - 1; i >= 0; i--) {
+ this._updateCountdown(this._timerTargets[i]);
+ }
+ },
+
+ /* Redisplay the countdown with an updated display.
+ @param target (jQuery) the containing division
+ @param inst (object) the current settings for this instance */
+ _updateCountdown: function(target, inst) {
+ var $target = $(target);
+ inst = inst || $.data(target, PROP_NAME);
+ if (!inst) {
+ return;
+ }
+ $target.html(this._generateHTML(inst));
+ $target[(this._get(inst, 'isRTL') ? 'add' : 'remove') + 'Class']('countdown_rtl');
+ var onTick = this._get(inst, 'onTick');
+ if (onTick) {
+ var periods = inst._hold != 'lap' ? inst._periods :
+ this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date());
+ var tickInterval = this._get(inst, 'tickInterval');
+ if (tickInterval == 1 || this.periodsToSeconds(periods) % tickInterval == 0) {
+ onTick.apply(target, [periods]);
+ }
+ }
+ var expired = inst._hold != 'pause' &&
+ (inst._since ? inst._now.getTime() < inst._since.getTime() :
+ inst._now.getTime() >= inst._until.getTime());
+ if (expired && !inst._expiring) {
+ inst._expiring = true;
+ if (this._hasTarget(target) || this._get(inst, 'alwaysExpire')) {
+ this._removeTarget(target);
+ var onExpiry = this._get(inst, 'onExpiry');
+ if (onExpiry) {
+ onExpiry.apply(target, []);
+ }
+ var expiryText = this._get(inst, 'expiryText');
+ if (expiryText) {
+ var layout = this._get(inst, 'layout');
+ inst.options.layout = expiryText;
+ this._updateCountdown(target, inst);
+ inst.options.layout = layout;
+ }
+ var expiryUrl = this._get(inst, 'expiryUrl');
+ if (expiryUrl) {
+ window.location = expiryUrl;
+ }
+ }
+ inst._expiring = false;
+ }
+ else if (inst._hold == 'pause') {
+ this._removeTarget(target);
+ }
+ $.data(target, PROP_NAME, inst);
+ },
+
+ /* Reconfigure the settings for a countdown div.
+ @param target (element) the containing division
+ @param options (object) the new settings for the countdown or
+ (string) an individual property name
+ @param value (any) the individual property value
+ (omit if options is an object) */
+ _changeCountdown: function(target, options, value) {
+ options = options || {};
+ if (typeof options == 'string') {
+ var name = options;
+ options = {};
+ options[name] = value;
+ }
+ var inst = $.data(target, PROP_NAME);
+ if (inst) {
+ this._resetExtraLabels(inst.options, options);
+ extendRemove(inst.options, options);
+ this._adjustSettings(target, inst);
+ $.data(target, PROP_NAME, inst);
+ var now = new Date();
+ if ((inst._since && inst._since < now) ||
+ (inst._until && inst._until > now)) {
+ this._addTarget(target);
+ }
+ this._updateCountdown(target, inst);
+ }
+ },
+
+ /* Reset any extra labelsn and compactLabelsn entries if changing labels.
+ @param base (object) the options to be updated
+ @param options (object) the new option values */
+ _resetExtraLabels: function(base, options) {
+ var changingLabels = false;
+ for (var n in options) {
+ if (n != 'whichLabels' && n.match(/[Ll]abels/)) {
+ changingLabels = true;
+ break;
+ }
+ }
+ if (changingLabels) {
+ for (var n in base) { // Remove custom numbered labels
+ if (n.match(/[Ll]abels[0-9]/)) {
+ base[n] = null;
+ }
+ }
+ }
+ },
+
+ /* Calculate interal settings for an instance.
+ @param target (element) the containing division
+ @param inst (object) the current settings for this instance */
+ _adjustSettings: function(target, inst) {
+ var now;
+ var serverSync = this._get(inst, 'serverSync');
+ var serverOffset = 0;
+ var serverEntry = null;
+ for (var i = 0; i < this._serverSyncs.length; i++) {
+ if (this._serverSyncs[i][0] == serverSync) {
+ serverEntry = this._serverSyncs[i][1];
+ break;
+ }
+ }
+ if (serverEntry != null) {
+ serverOffset = (serverSync ? serverEntry : 0);
+ now = new Date();
+ }
+ else {
+ var serverResult = (serverSync ? serverSync.apply(target, []) : null);
+ now = new Date();
+ serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
+ this._serverSyncs.push([serverSync, serverOffset]);
+ }
+ var timezone = this._get(inst, 'timezone');
+ timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
+ inst._since = this._get(inst, 'since');
+ if (inst._since != null) {
+ inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null));
+ if (inst._since && serverOffset) {
+ inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset);
+ }
+ }
+ inst._until = this.UTCDate(timezone, this._determineTime(this._get(inst, 'until'), now));
+ if (serverOffset) {
+ inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset);
+ }
+ inst._show = this._determineShow(inst);
+ },
+
+ /* Remove the countdown widget from a div.
+ @param target (element) the containing division */
+ _destroyCountdown: function(target) {
+ var $target = $(target);
+ if (!$target.hasClass(this.markerClassName)) {
+ return;
+ }
+ this._removeTarget(target);
+ $target.removeClass(this.markerClassName).empty();
+ $.removeData(target, PROP_NAME);
+ },
+
+ /* Pause a countdown widget at the current time.
+ Stop it running but remember and display the current time.
+ @param target (element) the containing division */
+ _pauseCountdown: function(target) {
+ this._hold(target, 'pause');
+ },
+
+ /* Pause a countdown widget at the current time.
+ Stop the display but keep the countdown running.
+ @param target (element) the containing division */
+ _lapCountdown: function(target) {
+ this._hold(target, 'lap');
+ },
+
+ /* Resume a paused countdown widget.
+ @param target (element) the containing division */
+ _resumeCountdown: function(target) {
+ this._hold(target, null);
+ },
+
+ /* Pause or resume a countdown widget.
+ @param target (element) the containing division
+ @param hold (string) the new hold setting */
+ _hold: function(target, hold) {
+ var inst = $.data(target, PROP_NAME);
+ if (inst) {
+ if (inst._hold == 'pause' && !hold) {
+ inst._periods = inst._savePeriods;
+ var sign = (inst._since ? '-' : '+');
+ inst[inst._since ? '_since' : '_until'] =
+ this._determineTime(sign + inst._periods[0] + 'y' +
+ sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' +
+ sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' +
+ sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's');
+ this._addTarget(target);
+ }
+ inst._hold = hold;
+ inst._savePeriods = (hold == 'pause' ? inst._periods : null);
+ $.data(target, PROP_NAME, inst);
+ this._updateCountdown(target, inst);
+ }
+ },
+
+ /* Return the current time periods.
+ @param target (element) the containing division
+ @return (number[7]) the current periods for the countdown */
+ _getTimesCountdown: function(target) {
+ var inst = $.data(target, PROP_NAME);
+ return (!inst ? null : (!inst._hold ? inst._periods :
+ this._calculatePeriods(inst, inst._show, this._get(inst, 'significant'), new Date())));
+ },
+
+ /* Get a setting value, defaulting if necessary.
+ @param inst (object) the current settings for this instance
+ @param name (string) the name of the required setting
+ @return (any) the setting's value or a default if not overridden */
+ _get: function(inst, name) {
+ return (inst.options[name] != null ?
+ inst.options[name] : $.countdown._defaults[name]);
+ },
+
+ /* A time may be specified as an exact value or a relative one.
+ @param setting (string or number or Date) - the date/time value
+ as a relative or absolute value
+ @param defaultTime (Date) the date/time to use if no other is supplied
+ @return (Date) the corresponding date/time */
+ _determineTime: function(setting, defaultTime) {
+ var offsetNumeric = function(offset) { // e.g. +300, -2
+ var time = new Date();
+ time.setTime(time.getTime() + offset * 1000);
+ return time;
+ };
+ var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m'
+ offset = offset.toLowerCase();
+ var time = new Date();
+ var year = time.getFullYear();
+ var month = time.getMonth();
+ var day = time.getDate();
+ var hour = time.getHours();
+ var minute = time.getMinutes();
+ var second = time.getSeconds();
+ var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g;
+ var matches = pattern.exec(offset);
+ while (matches) {
+ switch (matches[2] || 's') {
+ case 's': second += parseInt(matches[1], 10); break;
+ case 'm': minute += parseInt(matches[1], 10); break;
+ case 'h': hour += parseInt(matches[1], 10); break;
+ case 'd': day += parseInt(matches[1], 10); break;
+ case 'w': day += parseInt(matches[1], 10) * 7; break;
+ case 'o':
+ month += parseInt(matches[1], 10);
+ day = Math.min(day, $.countdown._getDaysInMonth(year, month));
+ break;
+ case 'y':
+ year += parseInt(matches[1], 10);
+ day = Math.min(day, $.countdown._getDaysInMonth(year, month));
+ break;
+ }
+ matches = pattern.exec(offset);
+ }
+ return new Date(year, month, day, hour, minute, second, 0);
+ };
+ var time = (setting == null ? defaultTime :
+ (typeof setting == 'string' ? offsetString(setting) :
+ (typeof setting == 'number' ? offsetNumeric(setting) : setting)));
+ if (time) time.setMilliseconds(0);
+ return time;
+ },
+
+ /* Determine the number of days in a month.
+ @param year (number) the year
+ @param month (number) the month
+ @return (number) the days in that month */
+ _getDaysInMonth: function(year, month) {
+ return 32 - new Date(year, month, 32).getDate();
+ },
+
+ /* Determine which set of labels should be used for an amount.
+ @param num (number) the amount to be displayed
+ @return (number) the set of labels to be used for this amount */
+ _normalLabels: function(num) {
+ return num;
+ },
+
+ /* Generate the HTML to display the countdown widget.
+ @param inst (object) the current settings for this instance
+ @return (string) the new HTML for the countdown display */
+ _generateHTML: function(inst) {
+ // Determine what to show
+ var significant = this._get(inst, 'significant');
+ inst._periods = (inst._hold ? inst._periods :
+ this._calculatePeriods(inst, inst._show, significant, new Date()));
+ // Show all 'asNeeded' after first non-zero value
+ var shownNonZero = false;
+ var showCount = 0;
+ var sigCount = significant;
+ var show = $.extend({}, inst._show);
+ for (var period = Y; period <= S; period++) {
+ shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0);
+ show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]);
+ showCount += (show[period] ? 1 : 0);
+ sigCount -= (inst._periods[period] > 0 ? 1 : 0);
+ }
+ var showSignificant = [false, false, false, false, false, false, false];
+ for (var period = S; period >= Y; period--) { // Determine significant periods
+ if (inst._show[period]) {
+ if (inst._periods[period]) {
+ showSignificant[period] = true;
+ }
+ else {
+ showSignificant[period] = sigCount > 0;
+ sigCount--;
+ }
+ }
+ }
+ var compact = this._get(inst, 'compact');
+ var layout = this._get(inst, 'layout');
+ var labels = (compact ? this._get(inst, 'compactLabels') : this._get(inst, 'labels'));
+ var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
+ var timeSeparator = this._get(inst, 'timeSeparator');
+ var description = this._get(inst, 'description') || '';
+ var showCompact = function(period) {
+ var labelsNum = $.countdown._get(inst,
+ 'compactLabels' + whichLabels(inst._periods[period]));
+ return (show[period] ? inst._periods[period] +
+ (labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
+ };
+ var showFull = function(period) {
+ var labelsNum = $.countdown._get(inst, 'labels' + whichLabels(inst._periods[period]));
+ return ((!significant && show[period]) || (significant && showSignificant[period]) ?
+ '<span class="countdown_section"><span class="countdown_amount">' +
+ inst._periods[period] + '</span><br/>' +
+ (labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
+ };
+ return (layout ? this._buildLayout(inst, show, layout, compact, significant, showSignificant) :
+ ((compact ? // Compact version
+ '<span class="countdown_row countdown_amount' +
+ (inst._hold ? ' countdown_holding' : '') + '">' +
+ showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) +
+ (show[H] ? this._minDigits(inst._periods[H], 2) : '') +
+ (show[M] ? (show[H] ? timeSeparator : '') +
+ this._minDigits(inst._periods[M], 2) : '') +
+ (show[S] ? (show[H] || show[M] ? timeSeparator : '') +
+ this._minDigits(inst._periods[S], 2) : '') :
+ // Full version
+ '<span class="countdown_row countdown_show' + (significant || showCount) +
+ (inst._hold ? ' countdown_holding' : '') + '">' +
+ showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
+ showFull(H) + showFull(M) + showFull(S)) + '</span>' +
+ (description ? '<span class="countdown_row countdown_descr">' + description + '</span>' : '')));
+ },
+
+ /* Construct a custom layout.
+ @param inst (object) the current settings for this instance
+ @param show (string[7]) flags indicating which periods are requested
+ @param layout (string) the customised layout
+ @param compact (boolean) true if using compact labels
+ @param significant (number) the number of periods with values to show, zero for all
+ @param showSignificant (boolean[7]) other periods to show for significance
+ @return (string) the custom HTML */
+ _buildLayout: function(inst, show, layout, compact, significant, showSignificant) {
+ var labels = this._get(inst, (compact ? 'compactLabels' : 'labels'));
+ var whichLabels = this._get(inst, 'whichLabels') || this._normalLabels;
+ var labelFor = function(index) {
+ return ($.countdown._get(inst,
+ (compact ? 'compactLabels' : 'labels') + whichLabels(inst._periods[index])) ||
+ labels)[index];
+ };
+ var digit = function(value, position) {
+ return Math.floor(value / position) % 10;
+ };
+ var subs = {desc: this._get(inst, 'description'), sep: this._get(inst, 'timeSeparator'),
+ yl: labelFor(Y), yn: inst._periods[Y], ynn: this._minDigits(inst._periods[Y], 2),
+ ynnn: this._minDigits(inst._periods[Y], 3), y1: digit(inst._periods[Y], 1),
+ y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
+ y1000: digit(inst._periods[Y], 1000),
+ ol: labelFor(O), on: inst._periods[O], onn: this._minDigits(inst._periods[O], 2),
+ onnn: this._minDigits(inst._periods[O], 3), o1: digit(inst._periods[O], 1),
+ o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
+ o1000: digit(inst._periods[O], 1000),
+ wl: labelFor(W), wn: inst._periods[W], wnn: this._minDigits(inst._periods[W], 2),
+ wnnn: this._minDigits(inst._periods[W], 3), w1: digit(inst._periods[W], 1),
+ w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
+ w1000: digit(inst._periods[W], 1000),
+ dl: labelFor(D), dn: inst._periods[D], dnn: this._minDigits(inst._periods[D], 2),
+ dnnn: this._minDigits(inst._periods[D], 3), d1: digit(inst._periods[D], 1),
+ d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
+ d1000: digit(inst._periods[D], 1000),
+ hl: labelFor(H), hn: inst._periods[H], hnn: this._minDigits(inst._periods[H], 2),
+ hnnn: this._minDigits(inst._periods[H], 3), h1: digit(inst._periods[H], 1),
+ h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
+ h1000: digit(inst._periods[H], 1000),
+ ml: labelFor(M), mn: inst._periods[M], mnn: this._minDigits(inst._periods[M], 2),
+ mnnn: this._minDigits(inst._periods[M], 3), m1: digit(inst._periods[M], 1),
+ m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
+ m1000: digit(inst._periods[M], 1000),
+ sl: labelFor(S), sn: inst._periods[S], snn: this._minDigits(inst._periods[S], 2),
+ snnn: this._minDigits(inst._periods[S], 3), s1: digit(inst._periods[S], 1),
+ s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100),
+ s1000: digit(inst._periods[S], 1000)};
+ var html = layout;
+ // Replace period containers: {p<}...{p>}
+ for (var i = Y; i <= S; i++) {
+ var period = 'yowdhms'.charAt(i);
+ var re = new RegExp('\\{' + period + '<\\}(.*)\\{' + period + '>\\}', 'g');
+ html = html.replace(re, ((!significant && show[i]) ||
+ (significant && showSignificant[i]) ? '$1' : ''));
+ }
+ // Replace period values: {pn}
+ $.each(subs, function(n, v) {
+ var re = new RegExp('\\{' + n + '\\}', 'g');
+ html = html.replace(re, v);
+ });
+ return html;
+ },
+
+ /* Ensure a numeric value has at least n digits for display.
+ @param value (number) the value to display
+ @param len (number) the minimum length
+ @return (string) the display text */
+ _minDigits: function(value, len) {
+ value = '' + value;
+ if (value.length >= len) {
+ return value;
+ }
+ value = '0000000000' + value;
+ return value.substr(value.length - len);
+ },
+
+ /* Translate the format into flags for each period.
+ @param inst (object) the current settings for this instance
+ @return (string[7]) flags indicating which periods are requested (?) or
+ required (!) by year, month, week, day, hour, minute, second */
+ _determineShow: function(inst) {
+ var format = this._get(inst, 'format');
+ var show = [];
+ show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null));
+ show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null));
+ show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null));
+ show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null));
+ show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null));
+ show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null));
+ show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null));
+ return show;
+ },
+
+ /* Calculate the requested periods between now and the target time.
+ @param inst (object) the current settings for this instance
+ @param show (string[7]) flags indicating which periods are requested/required
+ @param significant (number) the number of periods with values to show, zero for all
+ @param now (Date) the current date and time
+ @return (number[7]) the current time periods (always positive)
+ by year, month, week, day, hour, minute, second */
+ _calculatePeriods: function(inst, show, significant, now) {
+ // Find endpoints
+ inst._now = now;
+ inst._now.setMilliseconds(0);
+ var until = new Date(inst._now.getTime());
+ if (inst._since) {
+ if (now.getTime() < inst._since.getTime()) {
+ inst._now = now = until;
+ }
+ else {
+ now = inst._since;
+ }
+ }
+ else {
+ until.setTime(inst._until.getTime());
+ if (now.getTime() > inst._until.getTime()) {
+ inst._now = now = until;
+ }
+ }
+ // Calculate differences by period
+ var periods = [0, 0, 0, 0, 0, 0, 0];
+ if (show[Y] || show[O]) {
+ // Treat end of months as the same
+ var lastNow = $.countdown._getDaysInMonth(now.getFullYear(), now.getMonth());
+ var lastUntil = $.countdown._getDaysInMonth(until.getFullYear(), until.getMonth());
+ var sameDay = (until.getDate() == now.getDate() ||
+ (until.getDate() >= Math.min(lastNow, lastUntil) &&
+ now.getDate() >= Math.min(lastNow, lastUntil)));
+ var getSecs = function(date) {
+ return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds();
+ };
+ var months = Math.max(0,
+ (until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() +
+ ((until.getDate() < now.getDate() && !sameDay) ||
+ (sameDay && getSecs(until) < getSecs(now)) ? -1 : 0));
+ periods[Y] = (show[Y] ? Math.floor(months / 12) : 0);
+ periods[O] = (show[O] ? months - periods[Y] * 12 : 0);
+ // Adjust for months difference and end of month if necessary
+ now = new Date(now.getTime());
+ var wasLastDay = (now.getDate() == lastNow);
+ var lastDay = $.countdown._getDaysInMonth(now.getFullYear() + periods[Y],
+ now.getMonth() + periods[O]);
+ if (now.getDate() > lastDay) {
+ now.setDate(lastDay);
+ }
+ now.setFullYear(now.getFullYear() + periods[Y]);
+ now.setMonth(now.getMonth() + periods[O]);
+ if (wasLastDay) {
+ now.setDate(lastDay);
+ }
+ }
+ var diff = Math.floor((until.getTime() - now.getTime()) / 1000);
+ var extractPeriod = function(period, numSecs) {
+ periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0);
+ diff -= periods[period] * numSecs;
+ };
+ extractPeriod(W, 604800);
+ extractPeriod(D, 86400);
+ extractPeriod(H, 3600);
+ extractPeriod(M, 60);
+ extractPeriod(S, 1);
+ if (diff > 0 && !inst._since) { // Round up if left overs
+ var multiplier = [1, 12, 4.3482, 7, 24, 60, 60];
+ var lastShown = S;
+ var max = 1;
+ for (var period = S; period >= Y; period--) {
+ if (show[period]) {
+ if (periods[lastShown] >= max) {
+ periods[lastShown] = 0;
+ diff = 1;
+ }
+ if (diff > 0) {
+ periods[period]++;
+ diff = 0;
+ lastShown = period;
+ max = 1;
+ }
+ }
+ max *= multiplier[period];
+ }
+ }
+ if (significant) { // Zero out insignificant periods
+ for (var period = Y; period <= S; period++) {
+ if (significant && periods[period]) {
+ significant--;
+ }
+ else if (!significant) {
+ periods[period] = 0;
+ }
+ }
+ }
+ return periods;
+ }
+});
+
+/* jQuery extend now ignores nulls!
+ @param target (object) the object to update
+ @param props (object) the new settings
+ @return (object) the updated object */
+function extendRemove(target, props) {
+ $.extend(target, props);
+ for (var name in props) {
+ if (props[name] == null) {
+ target[name] = null;
+ }
+ }
+ return target;
+}
+
+/* Process the countdown functionality for a jQuery selection.
+ @param command (string) the command to run (optional, default 'attach')
+ @param options (object) the new settings to use for these countdown instances
+ @return (jQuery) for chaining further calls */
+$.fn.countdown = function(options) {
+ var otherArgs = Array.prototype.slice.call(arguments, 1);
+ if (options == 'getTimes' || options == 'settings') {
+ return $.countdown['_' + options + 'Countdown'].
+ apply($.countdown, [this[0]].concat(otherArgs));
+ }
+ return this.each(function() {
+ if (typeof options == 'string') {
+ $.countdown['_' + options + 'Countdown'].apply($.countdown, [this].concat(otherArgs));
+ }
+ else {
+ $.countdown._attachCountdown(this, options);
+ }
+ });
+};
+
+/* Initialise the countdown functionality. */
+$.countdown = new Countdown(); // singleton instance
+
+})(jQuery);