jquery.countdown.js 35 KB

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