jquery.countdown.js 39 KB

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