jquery-countdown-rails 2.0.0 → 2.0.1

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