Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 espaco 1
/* http://keith-wood.name/countdown.html
2
   Countdown for jQuery v1.6.3.
3
   Written by Keith Wood (kbwood{at}iinet.com.au) January 2008.
4
   Available under the MIT (https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) license.
5
   Please attribute the author if you use it. */
6
 
7
/* Display a countdown timer.
8
   Attach it with options like:
9
   $('div selector').countdown(
10
       {until: new Date(2009, 1 - 1, 1, 0, 0, 0), onExpiry: happyNewYear}); */
11
 
12
(function($) { // Hide scope, no $ conflict
13
 
14
/* Countdown manager. */
15
function Countdown() {
16
        this.regional = []; // Available regional settings, indexed by language code
17
        this.regional[''] = { // Default regional settings
18
                // The display texts for the counters
19
                labels: ['Years', 'Months', 'Weeks', 'Days', 'Hours', 'Minutes', 'Seconds'],
20
                // The display texts for the counters if only one
21
                labels1: ['Year', 'Month', 'Week', 'Day', 'Hour', 'Minute', 'Second'],
22
                compactLabels: ['y', 'm', 'w', 'd'], // The compact texts for the counters
23
                whichLabels: null, // Function to determine which labels to use
24
                digits: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], // The digits to display
25
                timeSeparator: ':', // Separator for time periods
26
                isRTL: false // True for right-to-left languages, false for left-to-right
27
        };
28
        this._defaults = {
29
                until: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count down to
30
                        // or numeric for seconds offset, or string for unit offset(s):
31
                        // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
32
                since: null, // new Date(year, mth - 1, day, hr, min, sec) - date/time to count up from
33
                        // or numeric for seconds offset, or string for unit offset(s):
34
                        // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
35
                timezone: null, // The timezone (hours or minutes from GMT) for the target times,
36
                        // or null for client local
37
                serverSync: null, // A function to retrieve the current server time for synchronisation
38
                format: 'dHMS', // Format for display - upper case for always, lower case only if non-zero,
39
                        // 'Y' years, 'O' months, 'W' weeks, 'D' days, 'H' hours, 'M' minutes, 'S' seconds
40
                layout: '', // Build your own layout for the countdown
41
                compact: false, // True to display in a compact format, false for an expanded one
42
                significant: 0, // The number of periods with values to show, zero for all
43
                description: '', // The description displayed for the countdown
44
                expiryUrl: '', // A URL to load upon expiry, replacing the current page
45
                expiryText: '', // Text to display upon expiry, replacing the countdown
46
                alwaysExpire: false, // True to trigger onExpiry even if never counted down
47
                onExpiry: null, // Callback when the countdown expires -
48
                        // receives no parameters and 'this' is the containing division
49
                onTick: null, // Callback when the countdown is updated -
50
                        // receives int[7] being the breakdown by period (based on format)
51
                        // and 'this' is the containing division
52
                tickInterval: 1 // Interval (seconds) between onTick callbacks
53
        };
54
        $.extend(this._defaults, this.regional['']);
55
        this._serverSyncs = [];
56
        var now = (typeof Date.now == 'function' ? Date.now :
57
                function() { return new Date().getTime(); });
58
        var perfAvail = (window.performance && typeof window.performance.now == 'function');
59
        // Shared timer for all countdowns
60
        function timerCallBack(timestamp) {
61
                var drawStart = (timestamp < 1e12 ? // New HTML5 high resolution timer
62
                        (perfAvail ? (performance.now() + performance.timing.navigationStart) : now()) :
63
                        // Integer milliseconds since unix epoch
64
                        timestamp || now());
65
                if (drawStart - animationStartTime >= 1000) {
66
                        plugin._updateTargets();
67
                        animationStartTime = drawStart;
68
                }
69
                requestAnimationFrame(timerCallBack);
70
        }
71
        var requestAnimationFrame = window.requestAnimationFrame ||
72
                window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||
73
                window.oRequestAnimationFrame || window.msRequestAnimationFrame || null;
74
                // This is when we expect a fall-back to setInterval as it's much more fluid
75
        var animationStartTime = 0;
76
        if (!requestAnimationFrame || $.noRequestAnimationFrame) {
77
                $.noRequestAnimationFrame = null;
78
                setInterval(function() { plugin._updateTargets(); }, 980); // Fall back to good old setInterval
79
        }
80
        else {
81
                animationStartTime = window.animationStartTime ||
82
                        window.webkitAnimationStartTime || window.mozAnimationStartTime ||
83
                        window.oAnimationStartTime || window.msAnimationStartTime || now();
84
                requestAnimationFrame(timerCallBack);
85
        }
86
}
87
 
88
var Y = 0; // Years
89
var O = 1; // Months
90
var W = 2; // Weeks
91
var D = 3; // Days
92
var H = 4; // Hours
93
var M = 5; // Minutes
94
var S = 6; // Seconds
95
 
96
$.extend(Countdown.prototype, {
97
        /* Class name added to elements to indicate already configured with countdown. */
98
        markerClassName: 'hasCountdown',
99
        /* Name of the data property for instance settings. */
100
        propertyName: 'countdown',
101
 
102
        /* Class name for the right-to-left marker. */
103
        _rtlClass: 'countdown_rtl',
104
        /* Class name for the countdown section marker. */
105
        _sectionClass: 'countdown_section',
106
        /* Class name for the period amount marker. */
107
        _amountClass: 'countdown_amount',
108
        /* Class name for the countdown row marker. */
109
        _rowClass: 'countdown_row',
110
        /* Class name for the holding countdown marker. */
111
        _holdingClass: 'countdown_holding',
112
        /* Class name for the showing countdown marker. */
113
        _showClass: 'countdown_show',
114
        /* Class name for the description marker. */
115
        _descrClass: 'countdown_descr',
116
 
117
        /* List of currently active countdown targets. */
118
        _timerTargets: [],
119
 
120
        /* Override the default settings for all instances of the countdown widget.
121
           @param  options  (object) the new settings to use as defaults */
122
        setDefaults: function(options) {
123
                this._resetExtraLabels(this._defaults, options);
124
                $.extend(this._defaults, options || {});
125
        },
126
 
127
        /* Convert a date/time to UTC.
128
           @param  tz     (number) the hour or minute offset from GMT, e.g. +9, -360
129
           @param  year   (Date) the date/time in that timezone or
130
                          (number) the year in that timezone
131
           @param  month  (number, optional) the month (0 - 11) (omit if year is a Date)
132
           @param  day    (number, optional) the day (omit if year is a Date)
133
           @param  hours  (number, optional) the hour (omit if year is a Date)
134
           @param  mins   (number, optional) the minute (omit if year is a Date)
135
           @param  secs   (number, optional) the second (omit if year is a Date)
136
           @param  ms     (number, optional) the millisecond (omit if year is a Date)
137
           @return  (Date) the equivalent UTC date/time */
138
        UTCDate: function(tz, year, month, day, hours, mins, secs, ms) {
139
                if (typeof year == 'object' && year.constructor == Date) {
140
                        ms = year.getMilliseconds();
141
                        secs = year.getSeconds();
142
                        mins = year.getMinutes();
143
                        hours = year.getHours();
144
                        day = year.getDate();
145
                        month = year.getMonth();
146
                        year = year.getFullYear();
147
                }
148
                var d = new Date();
149
                d.setUTCFullYear(year);
150
                d.setUTCDate(1);
151
                d.setUTCMonth(month || 0);
152
                d.setUTCDate(day || 1);
153
                d.setUTCHours(hours || 0);
154
                d.setUTCMinutes((mins || 0) - (Math.abs(tz) < 30 ? tz * 60 : tz));
155
                d.setUTCSeconds(secs || 0);
156
                d.setUTCMilliseconds(ms || 0);
157
                return d;
158
        },
159
 
160
        /* Convert a set of periods into seconds.
161
           Averaged for months and years.
162
           @param  periods  (number[7]) the periods per year/month/week/day/hour/minute/second
163
           @return  (number) the corresponding number of seconds */
164
        periodsToSeconds: function(periods) {
165
                return periods[0] * 31557600 + periods[1] * 2629800 + periods[2] * 604800 +
166
                        periods[3] * 86400 + periods[4] * 3600 + periods[5] * 60 + periods[6];
167
        },
168
 
169
        /* Attach the countdown widget to a div.
170
           @param  target   (element) the containing division
171
           @param  options  (object) the initial settings for the countdown */
172
        _attachPlugin: function(target, options) {
173
                target = $(target);
174
                if (target.hasClass(this.markerClassName)) {
175
                        return;
176
                }
177
                var inst = {options: $.extend({}, this._defaults), _periods: [0, 0, 0, 0, 0, 0, 0]};
178
                target.addClass(this.markerClassName).data(this.propertyName, inst);
179
                this._optionPlugin(target, options);
180
        },
181
 
182
        /* Add a target to the list of active ones.
183
           @param  target  (element) the countdown target */
184
        _addTarget: function(target) {
185
                if (!this._hasTarget(target)) {
186
                        this._timerTargets.push(target);
187
                }
188
        },
189
 
190
        /* See if a target is in the list of active ones.
191
           @param  target  (element) the countdown target
192
           @return  (boolean) true if present, false if not */
193
        _hasTarget: function(target) {
194
                return ($.inArray(target, this._timerTargets) > -1);
195
        },
196
 
197
        /* Remove a target from the list of active ones.
198
           @param  target  (element) the countdown target */
199
        _removeTarget: function(target) {
200
                this._timerTargets = $.map(this._timerTargets,
201
                        function(value) { return (value == target ? null : value); }); // delete entry
202
        },
203
 
204
        /* Update each active timer target. */
205
        _updateTargets: function() {
206
                for (var i = this._timerTargets.length - 1; i >= 0; i--) {
207
                        this._updateCountdown(this._timerTargets[i]);
208
                }
209
        },
210
 
211
        /* Reconfigure the settings for a countdown div.
212
           @param  target   (element) the control to affect
213
           @param  options  (object) the new options for this instance or
214
                            (string) an individual property name
215
           @param  value    (any) the individual property value (omit if options
216
                            is an object or to retrieve the value of a setting)
217
           @return  (any) if retrieving a value */
218
        _optionPlugin: function(target, options, value) {
219
                target = $(target);
220
                var inst = target.data(this.propertyName);
221
                if (!options || (typeof options == 'string' && value == null)) { // Get option
222
                        var name = options;
223
                        options = (inst || {}).options;
224
                        return (options && name ? options[name] : options);
225
                }
226
 
227
                if (!target.hasClass(this.markerClassName)) {
228
                        return;
229
                }
230
                options = options || {};
231
                if (typeof options == 'string') {
232
                        var name = options;
233
                        options = {};
234
                        options[name] = value;
235
                }
236
                if (options.layout) {
237
                        options.layout = options.layout.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
238
                }
239
                this._resetExtraLabels(inst.options, options);
240
                var timezoneChanged = (inst.options.timezone != options.timezone);
241
                $.extend(inst.options, options);
242
                this._adjustSettings(target, inst,
243
                        options.until != null || options.since != null || timezoneChanged);
244
                var now = new Date();
245
                if ((inst._since && inst._since < now) || (inst._until && inst._until > now)) {
246
                        this._addTarget(target[0]);
247
                }
248
                this._updateCountdown(target, inst);
249
        },
250
 
251
        /* Redisplay the countdown with an updated display.
252
           @param  target  (jQuery) the containing division
253
           @param  inst    (object) the current settings for this instance */
254
        _updateCountdown: function(target, inst) {
255
                var $target = $(target);
256
                inst = inst || $target.data(this.propertyName);
257
                if (!inst) {
258
                        return;
259
                }
260
                $target.html(this._generateHTML(inst)).toggleClass(this._rtlClass, inst.options.isRTL);
261
                if ($.isFunction(inst.options.onTick)) {
262
                        var periods = inst._hold != 'lap' ? inst._periods :
263
                                this._calculatePeriods(inst, inst._show, inst.options.significant, new Date());
264
                        if (inst.options.tickInterval == 1 ||
265
                                        this.periodsToSeconds(periods) % inst.options.tickInterval == 0) {
266
                                inst.options.onTick.apply(target, [periods]);
267
                        }
268
                }
269
                var expired = inst._hold != 'pause' &&
270
                        (inst._since ? inst._now.getTime() < inst._since.getTime() :
271
                        inst._now.getTime() >= inst._until.getTime());
272
                if (expired && !inst._expiring) {
273
                        inst._expiring = true;
274
                        if (this._hasTarget(target) || inst.options.alwaysExpire) {
275
                                this._removeTarget(target);
276
                                if ($.isFunction(inst.options.onExpiry)) {
277
                                        inst.options.onExpiry.apply(target, []);
278
                                }
279
                                if (inst.options.expiryText) {
280
                                        var layout = inst.options.layout;
281
                                        inst.options.layout = inst.options.expiryText;
282
                                        this._updateCountdown(target, inst);
283
                                        inst.options.layout = layout;
284
                                }
285
                                if (inst.options.expiryUrl) {
286
                                        window.location = inst.options.expiryUrl;
287
                                }
288
                        }
289
                        inst._expiring = false;
290
                }
291
                else if (inst._hold == 'pause') {
292
                        this._removeTarget(target);
293
                }
294
                $target.data(this.propertyName, inst);
295
        },
296
 
297
        /* Reset any extra labelsn and compactLabelsn entries if changing labels.
298
           @param  base     (object) the options to be updated
299
           @param  options  (object) the new option values */
300
        _resetExtraLabels: function(base, options) {
301
                var changingLabels = false;
302
                for (var n in options) {
303
                        if (n != 'whichLabels' && n.match(/[Ll]abels/)) {
304
                                changingLabels = true;
305
                                break;
306
                        }
307
                }
308
                if (changingLabels) {
309
                        for (var n in base) { // Remove custom numbered labels
310
                                if (n.match(/[Ll]abels[02-9]|compactLabels1/)) {
311
                                        base[n] = null;
312
                                }
313
                        }
314
                }
315
        },
316
 
317
        /* Calculate interal settings for an instance.
318
           @param  target  (element) the containing division
319
           @param  inst    (object) the current settings for this instance
320
           @param  recalc  (boolean) true if until or since are set */
321
        _adjustSettings: function(target, inst, recalc) {
322
                var now;
323
                var serverOffset = 0;
324
                var serverEntry = null;
325
                for (var i = 0; i < this._serverSyncs.length; i++) {
326
                        if (this._serverSyncs[i][0] == inst.options.serverSync) {
327
                                serverEntry = this._serverSyncs[i][1];
328
                                break;
329
                        }
330
                }
331
                if (serverEntry != null) {
332
                        serverOffset = (inst.options.serverSync ? serverEntry : 0);
333
                        now = new Date();
334
                }
335
                else {
336
                        var serverResult = ($.isFunction(inst.options.serverSync) ?
337
                                inst.options.serverSync.apply(target, []) : null);
338
                        now = new Date();
339
                        serverOffset = (serverResult ? now.getTime() - serverResult.getTime() : 0);
340
                        this._serverSyncs.push([inst.options.serverSync, serverOffset]);
341
                }
342
                var timezone = inst.options.timezone;
343
                timezone = (timezone == null ? -now.getTimezoneOffset() : timezone);
344
                if (recalc || (!recalc && inst._until == null && inst._since == null)) {
345
                        inst._since = inst.options.since;
346
                        if (inst._since != null) {
347
                                inst._since = this.UTCDate(timezone, this._determineTime(inst._since, null));
348
                                if (inst._since && serverOffset) {
349
                                        inst._since.setMilliseconds(inst._since.getMilliseconds() + serverOffset);
350
                                }
351
                        }
352
                        inst._until = this.UTCDate(timezone, this._determineTime(inst.options.until, now));
353
                        if (serverOffset) {
354
                                inst._until.setMilliseconds(inst._until.getMilliseconds() + serverOffset);
355
                        }
356
                }
357
                inst._show = this._determineShow(inst);
358
        },
359
 
360
        /* Remove the countdown widget from a div.
361
           @param  target  (element) the containing division */
362
        _destroyPlugin: function(target) {
363
                target = $(target);
364
                if (!target.hasClass(this.markerClassName)) {
365
                        return;
366
                }
367
                this._removeTarget(target[0]);
368
                target.removeClass(this.markerClassName).empty().removeData(this.propertyName);
369
        },
370
 
371
        /* Pause a countdown widget at the current time.
372
           Stop it running but remember and display the current time.
373
           @param  target  (element) the containing division */
374
        _pausePlugin: function(target) {
375
                this._hold(target, 'pause');
376
        },
377
 
378
        /* Pause a countdown widget at the current time.
379
           Stop the display but keep the countdown running.
380
           @param  target  (element) the containing division */
381
        _lapPlugin: function(target) {
382
                this._hold(target, 'lap');
383
        },
384
 
385
        /* Resume a paused countdown widget.
386
           @param  target  (element) the containing division */
387
        _resumePlugin: function(target) {
388
                this._hold(target, null);
389
        },
390
 
391
        /* Pause or resume a countdown widget.
392
           @param  target  (element) the containing division
393
           @param  hold    (string) the new hold setting */
394
        _hold: function(target, hold) {
395
                var inst = $.data(target, this.propertyName);
396
                if (inst) {
397
                        if (inst._hold == 'pause' && !hold) {
398
                                inst._periods = inst._savePeriods;
399
                                var sign = (inst._since ? '-' : '+');
400
                                inst[inst._since ? '_since' : '_until'] =
401
                                        this._determineTime(sign + inst._periods[0] + 'y' +
402
                                                sign + inst._periods[1] + 'o' + sign + inst._periods[2] + 'w' +
403
                                                sign + inst._periods[3] + 'd' + sign + inst._periods[4] + 'h' +
404
                                                sign + inst._periods[5] + 'm' + sign + inst._periods[6] + 's');
405
                                this._addTarget(target);
406
                        }
407
                        inst._hold = hold;
408
                        inst._savePeriods = (hold == 'pause' ? inst._periods : null);
409
                        $.data(target, this.propertyName, inst);
410
                        this._updateCountdown(target, inst);
411
                }
412
        },
413
 
414
        /* Return the current time periods.
415
           @param  target  (element) the containing division
416
           @return  (number[7]) the current periods for the countdown */
417
        _getTimesPlugin: function(target) {
418
                var inst = $.data(target, this.propertyName);
419
                return (!inst ? null : (inst._hold == 'pause' ? inst._savePeriods : (!inst._hold ? inst._periods :
420
                        this._calculatePeriods(inst, inst._show, inst.options.significant, new Date()))));
421
        },
422
 
423
        /* A time may be specified as an exact value or a relative one.
424
           @param  setting      (string or number or Date) - the date/time value
425
                                as a relative or absolute value
426
           @param  defaultTime  (Date) the date/time to use if no other is supplied
427
           @return  (Date) the corresponding date/time */
428
        _determineTime: function(setting, defaultTime) {
429
                var offsetNumeric = function(offset) { // e.g. +300, -2
430
                        var time = new Date();
431
                        time.setTime(time.getTime() + offset * 1000);
432
                        return time;
433
                };
434
                var offsetString = function(offset) { // e.g. '+2d', '-4w', '+3h +30m'
435
                        offset = offset.toLowerCase();
436
                        var time = new Date();
437
                        var year = time.getFullYear();
438
                        var month = time.getMonth();
439
                        var day = time.getDate();
440
                        var hour = time.getHours();
441
                        var minute = time.getMinutes();
442
                        var second = time.getSeconds();
443
                        var pattern = /([+-]?[0-9]+)\s*(s|m|h|d|w|o|y)?/g;
444
                        var matches = pattern.exec(offset);
445
                        while (matches) {
446
                                switch (matches[2] || 's') {
447
                                        case 's': second += parseInt(matches[1], 10); break;
448
                                        case 'm': minute += parseInt(matches[1], 10); break;
449
                                        case 'h': hour += parseInt(matches[1], 10); break;
450
                                        case 'd': day += parseInt(matches[1], 10); break;
451
                                        case 'w': day += parseInt(matches[1], 10) * 7; break;
452
                                        case 'o':
453
                                                month += parseInt(matches[1], 10);
454
                                                day = Math.min(day, plugin._getDaysInMonth(year, month));
455
                                                break;
456
                                        case 'y':
457
                                                year += parseInt(matches[1], 10);
458
                                                day = Math.min(day, plugin._getDaysInMonth(year, month));
459
                                                break;
460
                                }
461
                                matches = pattern.exec(offset);
462
                        }
463
                        return new Date(year, month, day, hour, minute, second, 0);
464
                };
465
                var time = (setting == null ? defaultTime :
466
                        (typeof setting == 'string' ? offsetString(setting) :
467
                        (typeof setting == 'number' ? offsetNumeric(setting) : setting)));
468
                if (time) time.setMilliseconds(0);
469
                return time;
470
        },
471
 
472
        /* Determine the number of days in a month.
473
           @param  year   (number) the year
474
           @param  month  (number) the month
475
           @return  (number) the days in that month */
476
        _getDaysInMonth: function(year, month) {
477
                return 32 - new Date(year, month, 32).getDate();
478
        },
479
 
480
        /* Determine which set of labels should be used for an amount.
481
           @param  num  (number) the amount to be displayed
482
           @return  (number) the set of labels to be used for this amount */
483
        _normalLabels: function(num) {
484
                return num;
485
        },
486
 
487
        /* Generate the HTML to display the countdown widget.
488
           @param  inst  (object) the current settings for this instance
489
           @return  (string) the new HTML for the countdown display */
490
        _generateHTML: function(inst) {
491
                var self = this;
492
                // Determine what to show
493
                inst._periods = (inst._hold ? inst._periods :
494
                        this._calculatePeriods(inst, inst._show, inst.options.significant, new Date()));
495
                // Show all 'asNeeded' after first non-zero value
496
                var shownNonZero = false;
497
                var showCount = 0;
498
                var sigCount = inst.options.significant;
499
                var show = $.extend({}, inst._show);
500
                for (var period = Y; period <= S; period++) {
501
                        shownNonZero |= (inst._show[period] == '?' && inst._periods[period] > 0);
502
                        show[period] = (inst._show[period] == '?' && !shownNonZero ? null : inst._show[period]);
503
                        showCount += (show[period] ? 1 : 0);
504
                        sigCount -= (inst._periods[period] > 0 ? 1 : 0);
505
                }
506
                var showSignificant = [false, false, false, false, false, false, false];
507
                for (var period = S; period >= Y; period--) { // Determine significant periods
508
                        if (inst._show[period]) {
509
                                if (inst._periods[period]) {
510
                                        showSignificant[period] = true;
511
                                }
512
                                else {
513
                                        showSignificant[period] = sigCount > 0;
514
                                        sigCount--;
515
                                }
516
                        }
517
                }
518
                var labels = (inst.options.compact ? inst.options.compactLabels : inst.options.labels);
519
                var whichLabels = inst.options.whichLabels || this._normalLabels;
520
                var showCompact = function(period) {
521
                        var labelsNum = inst.options['compactLabels' + whichLabels(inst._periods[period])];
522
                        return (show[period] ? self._translateDigits(inst, inst._periods[period]) +
523
                                (labelsNum ? labelsNum[period] : labels[period]) + ' ' : '');
524
                };
525
                var showFull = function(period) {
526
                        var labelsNum = inst.options['labels' + whichLabels(inst._periods[period])];
527
                        return ((!inst.options.significant && show[period]) ||
528
                                (inst.options.significant && showSignificant[period]) ?
529
                                '<span class="' + plugin._sectionClass + '">' +
530
                                '<span class="' + plugin._amountClass + '">' +
531
                                self._translateDigits(inst, inst._periods[period]) + '</span><br/>' +
532
                                (labelsNum ? labelsNum[period] : labels[period]) + '</span>' : '');
533
                };
534
                return (inst.options.layout ? this._buildLayout(inst, show, inst.options.layout,
535
                        inst.options.compact, inst.options.significant, showSignificant) :
536
                        ((inst.options.compact ? // Compact version
537
                        '<span class="' + this._rowClass + ' ' + this._amountClass +
538
                        (inst._hold ? ' ' + this._holdingClass : '') + '">' +
539
                        showCompact(Y) + showCompact(O) + showCompact(W) + showCompact(D) +
540
                        (show[H] ? this._minDigits(inst, inst._periods[H], 2) : '') +
541
                        (show[M] ? (show[H] ? inst.options.timeSeparator : '') +
542
                        this._minDigits(inst, inst._periods[M], 2) : '') +
543
                        (show[S] ? (show[H] || show[M] ? inst.options.timeSeparator : '') +
544
                        this._minDigits(inst, inst._periods[S], 2) : '') :
545
                        // Full version
546
                        '<span class="' + this._rowClass + ' ' + this._showClass + (inst.options.significant || showCount) +
547
                        (inst._hold ? ' ' + this._holdingClass : '') + '">' +
548
                        showFull(Y) + showFull(O) + showFull(W) + showFull(D) +
549
                        showFull(H) + showFull(M) + showFull(S)) + '</span>' +
550
                        (inst.options.description ? '<span class="' + this._rowClass + ' ' + this._descrClass + '">' +
551
                        inst.options.description + '</span>' : '')));
552
        },
553
 
554
        /* Construct a custom layout.
555
           @param  inst             (object) the current settings for this instance
556
           @param  show             (string[7]) flags indicating which periods are requested
557
           @param  layout           (string) the customised layout
558
           @param  compact          (boolean) true if using compact labels
559
           @param  significant      (number) the number of periods with values to show, zero for all
560
           @param  showSignificant  (boolean[7]) other periods to show for significance
561
           @return  (string) the custom HTML */
562
        _buildLayout: function(inst, show, layout, compact, significant, showSignificant) {
563
                var labels = inst.options[compact ? 'compactLabels' : 'labels'];
564
                var whichLabels = inst.options.whichLabels || this._normalLabels;
565
                var labelFor = function(index) {
566
                        return (inst.options[(compact ? 'compactLabels' : 'labels') +
567
                                whichLabels(inst._periods[index])] || labels)[index];
568
                };
569
                var digit = function(value, position) {
570
                        return inst.options.digits[Math.floor(value / position) % 10];
571
                };
572
                var subs = {desc: inst.options.description, sep: inst.options.timeSeparator,
573
                        yl: labelFor(Y), yn: this._minDigits(inst, inst._periods[Y], 1),
574
                        ynn: this._minDigits(inst, inst._periods[Y], 2),
575
                        ynnn: this._minDigits(inst, inst._periods[Y], 3), y1: digit(inst._periods[Y], 1),
576
                        y10: digit(inst._periods[Y], 10), y100: digit(inst._periods[Y], 100),
577
                        y1000: digit(inst._periods[Y], 1000),
578
                        ol: labelFor(O), on: this._minDigits(inst, inst._periods[O], 1),
579
                        onn: this._minDigits(inst, inst._periods[O], 2),
580
                        onnn: this._minDigits(inst, inst._periods[O], 3), o1: digit(inst._periods[O], 1),
581
                        o10: digit(inst._periods[O], 10), o100: digit(inst._periods[O], 100),
582
                        o1000: digit(inst._periods[O], 1000),
583
                        wl: labelFor(W), wn: this._minDigits(inst, inst._periods[W], 1),
584
                        wnn: this._minDigits(inst, inst._periods[W], 2),
585
                        wnnn: this._minDigits(inst, inst._periods[W], 3), w1: digit(inst._periods[W], 1),
586
                        w10: digit(inst._periods[W], 10), w100: digit(inst._periods[W], 100),
587
                        w1000: digit(inst._periods[W], 1000),
588
                        dl: labelFor(D), dn: this._minDigits(inst, inst._periods[D], 1),
589
                        dnn: this._minDigits(inst, inst._periods[D], 2),
590
                        dnnn: this._minDigits(inst, inst._periods[D], 3), d1: digit(inst._periods[D], 1),
591
                        d10: digit(inst._periods[D], 10), d100: digit(inst._periods[D], 100),
592
                        d1000: digit(inst._periods[D], 1000),
593
                        hl: labelFor(H), hn: this._minDigits(inst, inst._periods[H], 1),
594
                        hnn: this._minDigits(inst, inst._periods[H], 2),
595
                        hnnn: this._minDigits(inst, inst._periods[H], 3), h1: digit(inst._periods[H], 1),
596
                        h10: digit(inst._periods[H], 10), h100: digit(inst._periods[H], 100),
597
                        h1000: digit(inst._periods[H], 1000),
598
                        ml: labelFor(M), mn: this._minDigits(inst, inst._periods[M], 1),
599
                        mnn: this._minDigits(inst, inst._periods[M], 2),
600
                        mnnn: this._minDigits(inst, inst._periods[M], 3), m1: digit(inst._periods[M], 1),
601
                        m10: digit(inst._periods[M], 10), m100: digit(inst._periods[M], 100),
602
                        m1000: digit(inst._periods[M], 1000),
603
                        sl: labelFor(S), sn: this._minDigits(inst, inst._periods[S], 1),
604
                        snn: this._minDigits(inst, inst._periods[S], 2),
605
                        snnn: this._minDigits(inst, inst._periods[S], 3), s1: digit(inst._periods[S], 1),
606
                        s10: digit(inst._periods[S], 10), s100: digit(inst._periods[S], 100),
607
                        s1000: digit(inst._periods[S], 1000)};
608
                var html = layout;
609
                // Replace period containers: {p<}...{p>}
610
                for (var i = Y; i <= S; i++) {
611
                        var period = 'yowdhms'.charAt(i);
612
                        var re = new RegExp('\\{' + period + '<\\}([\\s\\S]*)\\{' + period + '>\\}', 'g');
613
                        html = html.replace(re, ((!significant && show[i]) ||
614
                                (significant && showSignificant[i]) ? '$1' : ''));
615
                }
616
                // Replace period values: {pn}
617
                $.each(subs, function(n, v) {
618
                        var re = new RegExp('\\{' + n + '\\}', 'g');
619
                        html = html.replace(re, v);
620
                });
621
                return html;
622
        },
623
 
624
        /* Ensure a numeric value has at least n digits for display.
625
           @param  inst   (object) the current settings for this instance
626
           @param  value  (number) the value to display
627
           @param  len    (number) the minimum length
628
           @return  (string) the display text */
629
        _minDigits: function(inst, value, len) {
630
                value = '' + value;
631
                if (value.length >= len) {
632
                        return this._translateDigits(inst, value);
633
                }
634
                value = '0000000000' + value;
635
                return this._translateDigits(inst, value.substr(value.length - len));
636
        },
637
 
638
        /* Translate digits into other representations.
639
           @param  inst   (object) the current settings for this instance
640
           @param  value  (string) the text to translate
641
           @return  (string) the translated text */
642
        _translateDigits: function(inst, value) {
643
                return ('' + value).replace(/[0-9]/g, function(digit) {
644
                                return inst.options.digits[digit];
645
                        });
646
        },
647
 
648
        /* Translate the format into flags for each period.
649
           @param  inst  (object) the current settings for this instance
650
           @return  (string[7]) flags indicating which periods are requested (?) or
651
                    required (!) by year, month, week, day, hour, minute, second */
652
        _determineShow: function(inst) {
653
                var format = inst.options.format;
654
                var show = [];
655
                show[Y] = (format.match('y') ? '?' : (format.match('Y') ? '!' : null));
656
                show[O] = (format.match('o') ? '?' : (format.match('O') ? '!' : null));
657
                show[W] = (format.match('w') ? '?' : (format.match('W') ? '!' : null));
658
                show[D] = (format.match('d') ? '?' : (format.match('D') ? '!' : null));
659
                show[H] = (format.match('h') ? '?' : (format.match('H') ? '!' : null));
660
                show[M] = (format.match('m') ? '?' : (format.match('M') ? '!' : null));
661
                show[S] = (format.match('s') ? '?' : (format.match('S') ? '!' : null));
662
                return show;
663
        },
664
 
665
        /* Calculate the requested periods between now and the target time.
666
           @param  inst         (object) the current settings for this instance
667
           @param  show         (string[7]) flags indicating which periods are requested/required
668
           @param  significant  (number) the number of periods with values to show, zero for all
669
           @param  now          (Date) the current date and time
670
           @return  (number[7]) the current time periods (always positive)
671
                    by year, month, week, day, hour, minute, second */
672
        _calculatePeriods: function(inst, show, significant, now) {
673
                // Find endpoints
674
                inst._now = now;
675
                inst._now.setMilliseconds(0);
676
                var until = new Date(inst._now.getTime());
677
                if (inst._since) {
678
                        if (now.getTime() < inst._since.getTime()) {
679
                                inst._now = now = until;
680
                        }
681
                        else {
682
                                now = inst._since;
683
                        }
684
                }
685
                else {
686
                        until.setTime(inst._until.getTime());
687
                        if (now.getTime() > inst._until.getTime()) {
688
                                inst._now = now = until;
689
                        }
690
                }
691
                // Calculate differences by period
692
                var periods = [0, 0, 0, 0, 0, 0, 0];
693
                if (show[Y] || show[O]) {
694
                        // Treat end of months as the same
695
                        var lastNow = plugin._getDaysInMonth(now.getFullYear(), now.getMonth());
696
                        var lastUntil = plugin._getDaysInMonth(until.getFullYear(), until.getMonth());
697
                        var sameDay = (until.getDate() == now.getDate() ||
698
                                (until.getDate() >= Math.min(lastNow, lastUntil) &&
699
                                now.getDate() >= Math.min(lastNow, lastUntil)));
700
                        var getSecs = function(date) {
701
                                return (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds();
702
                        };
703
                        var months = Math.max(0,
704
                                (until.getFullYear() - now.getFullYear()) * 12 + until.getMonth() - now.getMonth() +
705
                                ((until.getDate() < now.getDate() && !sameDay) ||
706
                                (sameDay && getSecs(until) < getSecs(now)) ? -1 : 0));
707
                        periods[Y] = (show[Y] ? Math.floor(months / 12) : 0);
708
                        periods[O] = (show[O] ? months - periods[Y] * 12 : 0);
709
                        // Adjust for months difference and end of month if necessary
710
                        now = new Date(now.getTime());
711
                        var wasLastDay = (now.getDate() == lastNow);
712
                        var lastDay = plugin._getDaysInMonth(now.getFullYear() + periods[Y],
713
                                now.getMonth() + periods[O]);
714
                        if (now.getDate() > lastDay) {
715
                                now.setDate(lastDay);
716
                        }
717
                        now.setFullYear(now.getFullYear() + periods[Y]);
718
                        now.setMonth(now.getMonth() + periods[O]);
719
                        if (wasLastDay) {
720
                                now.setDate(lastDay);
721
                        }
722
                }
723
                var diff = Math.floor((until.getTime() - now.getTime()) / 1000);
724
                var extractPeriod = function(period, numSecs) {
725
                        periods[period] = (show[period] ? Math.floor(diff / numSecs) : 0);
726
                        diff -= periods[period] * numSecs;
727
                };
728
                extractPeriod(W, 604800);
729
                extractPeriod(D, 86400);
730
                extractPeriod(H, 3600);
731
                extractPeriod(M, 60);
732
                extractPeriod(S, 1);
733
                if (diff > 0 && !inst._since) { // Round up if left overs
734
                        var multiplier = [1, 12, 4.3482, 7, 24, 60, 60];
735
                        var lastShown = S;
736
                        var max = 1;
737
                        for (var period = S; period >= Y; period--) {
738
                                if (show[period]) {
739
                                        if (periods[lastShown] >= max) {
740
                                                periods[lastShown] = 0;
741
                                                diff = 1;
742
                                        }
743
                                        if (diff > 0) {
744
                                                periods[period]++;
745
                                                diff = 0;
746
                                                lastShown = period;
747
                                                max = 1;
748
                                        }
749
                                }
750
                                max *= multiplier[period];
751
                        }
752
                }
753
                if (significant) { // Zero out insignificant periods
754
                        for (var period = Y; period <= S; period++) {
755
                                if (significant && periods[period]) {
756
                                        significant--;
757
                                }
758
                                else if (!significant) {
759
                                        periods[period] = 0;
760
                                }
761
                        }
762
                }
763
                return periods;
764
        }
765
});
766
 
767
// The list of commands that return values and don't permit chaining
768
var getters = ['getTimes'];
769
 
770
/* Determine whether a command is a getter and doesn't permit chaining.
771
   @param  command    (string, optional) the command to run
772
   @param  otherArgs  ([], optional) any other arguments for the command
773
   @return  true if the command is a getter, false if not */
774
function isNotChained(command, otherArgs) {
775
        if (command == 'option' && (otherArgs.length == 0 ||
776
                        (otherArgs.length == 1 && typeof otherArgs[0] == 'string'))) {
777
                return true;
778
        }
779
        return $.inArray(command, getters) > -1;
780
}
781
 
782
/* Process the countdown functionality for a jQuery selection.
783
   @param  options  (object) the new settings to use for these instances (optional) or
784
                    (string) the command to run (optional)
785
   @return  (jQuery) for chaining further calls or
786
            (any) getter value */
787
$.fn.countdown = function(options) {
788
        var otherArgs = Array.prototype.slice.call(arguments, 1);
789
        if (isNotChained(options, otherArgs)) {
790
                return plugin['_' + options + 'Plugin'].
791
                        apply(plugin, [this[0]].concat(otherArgs));
792
        }
793
        return this.each(function() {
794
                if (typeof options == 'string') {
795
                        if (!plugin['_' + options + 'Plugin']) {
796
                                throw 'Unknown command: ' + options;
797
                        }
798
                        plugin['_' + options + 'Plugin'].
799
                                apply(plugin, [this].concat(otherArgs));
800
                }
801
                else {
802
                        plugin._attachPlugin(this, options || {});
803
                }
804
        });
805
};
806
 
807
/* Initialise the countdown functionality. */
808
var plugin = $.countdown = new Countdown(); // Singleton instance
809
 
810
})(jQuery);