countdown.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // AMD support (Thanks to @FagnerMartinsBrack)
  2. ;(function(factory) {
  3. 'use strict';
  4. if (typeof define === 'function' && define.amd) {
  5. define(['jquery'], factory);
  6. } else {
  7. factory(jQuery);
  8. }
  9. })(function($){
  10. 'use strict';
  11. var instances = [],
  12. matchers = [],
  13. defaultOptions = {
  14. precision: 100, // 0.1 seconds, used to update the DOM
  15. elapse: false,
  16. defer: false
  17. };
  18. // Miliseconds
  19. matchers.push(/^[0-9]*$/.source);
  20. // Month/Day/Year [hours:minutes:seconds]
  21. matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/
  22. .source);
  23. // Year/Day/Month [hours:minutes:seconds] and
  24. // Year-Day-Month [hours:minutes:seconds]
  25. matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/
  26. .source);
  27. // Cast the matchers to a regular expression object
  28. matchers = new RegExp(matchers.join('|'));
  29. // Parse a Date formatted has String to a native object
  30. function parseDateString(dateString) {
  31. // Pass through when a native object is sent
  32. if(dateString instanceof Date) {
  33. return dateString;
  34. }
  35. // Caste string to date object
  36. if(String(dateString).match(matchers)) {
  37. // If looks like a milisecond value cast to number before
  38. // final casting (Thanks to @msigley)
  39. if(String(dateString).match(/^[0-9]*$/)) {
  40. dateString = Number(dateString);
  41. }
  42. // Replace dashes to slashes
  43. if(String(dateString).match(/\-/)) {
  44. dateString = String(dateString).replace(/\-/g, '/');
  45. }
  46. return new Date(dateString);
  47. } else {
  48. throw new Error('Couldn\'t cast `' + dateString +
  49. '` to a date object.');
  50. }
  51. }
  52. // Map to convert from a directive to offset object property
  53. var DIRECTIVE_KEY_MAP = {
  54. 'Y': 'years',
  55. 'm': 'months',
  56. 'n': 'daysToMonth',
  57. 'd': 'daysToWeek',
  58. 'w': 'weeks',
  59. 'W': 'weeksToMonth',
  60. 'H': 'hours',
  61. 'M': 'minutes',
  62. 'S': 'seconds',
  63. 'D': 'totalDays',
  64. 'I': 'totalHours',
  65. 'N': 'totalMinutes',
  66. 'T': 'totalSeconds'
  67. };
  68. // Returns an escaped regexp from the string
  69. function escapedRegExp(str) {
  70. var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
  71. return new RegExp(sanitize);
  72. }
  73. // Time string formatter
  74. function strftime(offsetObject) {
  75. return function(format) {
  76. var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi);
  77. if(directives) {
  78. for(var i = 0, len = directives.length; i < len; ++i) {
  79. var directive = directives[i]
  80. .match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/),
  81. regexp = escapedRegExp(directive[0]),
  82. modifier = directive[1] || '',
  83. plural = directive[3] || '',
  84. value = null;
  85. // Get the key
  86. directive = directive[2];
  87. // Swap shot-versions directives
  88. if(DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) {
  89. value = DIRECTIVE_KEY_MAP[directive];
  90. value = Number(offsetObject[value]);
  91. }
  92. if(value !== null) {
  93. // Pluralize
  94. if(modifier === '!') {
  95. value = pluralize(plural, value);
  96. }
  97. // Add zero-padding
  98. if(modifier === '') {
  99. if(value < 10) {
  100. value = '0' + value.toString();
  101. }
  102. }
  103. // Replace the directive
  104. format = format.replace(regexp, value.toString());
  105. }
  106. }
  107. }
  108. format = format.replace(/%%/, '%');
  109. return format;
  110. };
  111. }
  112. // Pluralize
  113. function pluralize(format, count) {
  114. var plural = 's', singular = '';
  115. if(format) {
  116. format = format.replace(/(:|;|\s)/gi, '').split(/\,/);
  117. if(format.length === 1) {
  118. plural = format[0];
  119. } else {
  120. singular = format[0];
  121. plural = format[1];
  122. }
  123. }
  124. // Fix #187
  125. if(Math.abs(count) > 1) {
  126. return plural;
  127. } else {
  128. return singular;
  129. }
  130. }
  131. // The Final Countdown
  132. var Countdown = function(el, finalDate, options) {
  133. this.el = el;
  134. this.$el = $(el);
  135. this.interval = null;
  136. this.offset = {};
  137. this.options = $.extend({}, defaultOptions);
  138. // console.log(this.options);
  139. // This helper variable is necessary to mimick the previous check for an
  140. // event listener on this.$el. Because of the event loop there might not
  141. // be a registered event listener during the first tick. In order to work
  142. // as expected a second tick is necessary, so that the events can be fired
  143. // and handled properly.
  144. this.firstTick = true;
  145. // Register this instance
  146. this.instanceNumber = instances.length;
  147. instances.push(this);
  148. // Save the reference
  149. this.$el.data('countdown-instance', this.instanceNumber);
  150. // Handle options or callback
  151. if (options) {
  152. // Register the callbacks when supplied
  153. if(typeof options === 'function') {
  154. this.$el.on('update.countdown', options);
  155. this.$el.on('stoped.countdown', options);
  156. this.$el.on('finish.countdown', options);
  157. } else {
  158. this.options = $.extend({}, defaultOptions, options);
  159. }
  160. }
  161. // Set the final date and start
  162. this.setFinalDate(finalDate);
  163. // Starts the countdown automatically unless it's defered,
  164. // Issue #198
  165. if (this.options.defer === false) {
  166. this.start();
  167. }
  168. };
  169. $.extend(Countdown.prototype, {
  170. start: function() {
  171. if(this.interval !== null) {
  172. clearInterval(this.interval);
  173. }
  174. var self = this;
  175. this.update();
  176. this.interval = setInterval(function() {
  177. self.update.call(self);
  178. }, this.options.precision);
  179. },
  180. stop: function() {
  181. clearInterval(this.interval);
  182. this.interval = null;
  183. this.dispatchEvent('stoped');
  184. },
  185. toggle: function() {
  186. if (this.interval) {
  187. this.stop();
  188. } else {
  189. this.start();
  190. }
  191. },
  192. pause: function() {
  193. this.stop();
  194. },
  195. resume: function() {
  196. this.start();
  197. },
  198. remove: function() {
  199. this.stop.call(this);
  200. instances[this.instanceNumber] = null;
  201. // Reset the countdown instance under data attr (Thanks to @assiotis)
  202. delete this.$el.data().countdownInstance;
  203. },
  204. setFinalDate: function(value) {
  205. this.finalDate = parseDateString(value); // Cast the given date
  206. },
  207. update: function() {
  208. // Stop if dom is not in the html (Thanks to @dleavitt)
  209. if(this.$el.closest('html').length === 0) {
  210. this.remove();
  211. return;
  212. }
  213. var now = new Date(),
  214. newTotalSecsLeft;
  215. // Create an offset date object
  216. newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs
  217. // Calculate the remaining time
  218. newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs
  219. // If is not have to elapse set the finish
  220. newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 :
  221. Math.abs(newTotalSecsLeft);
  222. // Do not proceed to calculation if the seconds have not changed or
  223. // during the first tick
  224. if (this.totalSecsLeft === newTotalSecsLeft || this.firstTick) {
  225. this.firstTick = false;
  226. return;
  227. } else {
  228. this.totalSecsLeft = newTotalSecsLeft;
  229. }
  230. // Check if the countdown has elapsed
  231. this.elapsed = (now >= this.finalDate);
  232. // Calculate the offsets
  233. this.offset = {
  234. seconds : this.totalSecsLeft % 60,
  235. minutes : Math.floor(this.totalSecsLeft / 60) % 60,
  236. hours : Math.floor(this.totalSecsLeft / 60 / 60) % 24,
  237. days : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
  238. daysToWeek : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
  239. daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368),
  240. weeks : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7),
  241. weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4,
  242. months : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368),
  243. years : Math.abs(this.finalDate.getFullYear()-now.getFullYear()),
  244. totalDays : Math.floor(this.totalSecsLeft / 60 / 60 / 24),
  245. totalHours : Math.floor(this.totalSecsLeft / 60 / 60),
  246. totalMinutes: Math.floor(this.totalSecsLeft / 60),
  247. totalSeconds: this.totalSecsLeft
  248. };
  249. // Dispatch an event
  250. if(!this.options.elapse && this.totalSecsLeft === 0) {
  251. this.stop();
  252. this.dispatchEvent('finish');
  253. } else {
  254. this.dispatchEvent('update');
  255. }
  256. },
  257. dispatchEvent: function(eventName) {
  258. var event = $.Event(eventName + '.countdown');
  259. event.finalDate = this.finalDate;
  260. event.elapsed = this.elapsed;
  261. event.offset = $.extend({}, this.offset);
  262. event.strftime = strftime(this.offset);
  263. this.$el.trigger(event);
  264. }
  265. });
  266. // Register the jQuery selector actions
  267. $.fn.countdown = function() {
  268. var argumentsArray = Array.prototype.slice.call(arguments, 0);
  269. return this.each(function() {
  270. // If no data was set, jQuery.data returns undefined
  271. var instanceNumber = $(this).data('countdown-instance');
  272. // Verify if we already have a countdown for this node ...
  273. // Fix issue #22 (Thanks to @romanbsd)
  274. if (instanceNumber !== undefined) {
  275. var instance = instances[instanceNumber],
  276. method = argumentsArray[0];
  277. // If method exists in the prototype execute
  278. if(Countdown.prototype.hasOwnProperty(method)) {
  279. instance[method].apply(instance, argumentsArray.slice(1));
  280. // If method look like a date try to set a new final date
  281. } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) {
  282. instance.setFinalDate.call(instance, method);
  283. // Allow plugin to restart after finished
  284. // Fix issue #38 (thanks to @yaoazhen)
  285. instance.start();
  286. } else {
  287. $.error('Method %s does not exist on jQuery.countdown'
  288. .replace(/\%s/gi, method));
  289. }
  290. } else {
  291. // ... if not we create an instance
  292. new Countdown(this, argumentsArray[0], argumentsArray[1]);
  293. }
  294. });
  295. };
  296. });