countdown.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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. // Register this instance
  140. this.instanceNumber = instances.length;
  141. instances.push(this);
  142. // Save the reference
  143. this.$el.data('countdown-instance', this.instanceNumber);
  144. // Handle options or callback
  145. if (options) {
  146. // Register the callbacks when supplied
  147. if(typeof options === 'function') {
  148. this.$el.on('update.countdown', options);
  149. this.$el.on('stoped.countdown', options);
  150. this.$el.on('finish.countdown', options);
  151. } else {
  152. this.options = $.extend({}, defaultOptions, options);
  153. }
  154. }
  155. // Set the final date and start
  156. this.setFinalDate(finalDate);
  157. // Starts the countdown automatically unless it's defered,
  158. // Issue #198
  159. if (this.options.defer === false) {
  160. this.start();
  161. }
  162. };
  163. $.extend(Countdown.prototype, {
  164. start: function() {
  165. if(this.interval !== null) {
  166. clearInterval(this.interval);
  167. }
  168. var self = this;
  169. this.update();
  170. this.interval = setInterval(function() {
  171. self.update.call(self);
  172. }, this.options.precision);
  173. },
  174. stop: function() {
  175. clearInterval(this.interval);
  176. this.interval = null;
  177. this.dispatchEvent('stoped');
  178. },
  179. toggle: function() {
  180. if (this.interval) {
  181. this.stop();
  182. } else {
  183. this.start();
  184. }
  185. },
  186. pause: function() {
  187. this.stop();
  188. },
  189. resume: function() {
  190. this.start();
  191. },
  192. remove: function() {
  193. this.stop.call(this);
  194. instances[this.instanceNumber] = null;
  195. // Reset the countdown instance under data attr (Thanks to @assiotis)
  196. delete this.$el.data().countdownInstance;
  197. },
  198. setFinalDate: function(value) {
  199. this.finalDate = parseDateString(value); // Cast the given date
  200. },
  201. update: function() {
  202. // Stop if dom is not in the html (Thanks to @dleavitt)
  203. if(this.$el.closest('html').length === 0) {
  204. this.remove();
  205. return;
  206. }
  207. var hasEventsAttached = $._data(this.el, 'events') !== undefined,
  208. now = new Date(),
  209. newTotalSecsLeft;
  210. // Create an offset date object
  211. newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs
  212. // Calculate the remaining time
  213. newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs
  214. // If is not have to elapse set the finish
  215. newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 :
  216. Math.abs(newTotalSecsLeft);
  217. // Do not proceed to calculation if the seconds have not changed or
  218. // does not any event attached
  219. if (this.totalSecsLeft === newTotalSecsLeft || !hasEventsAttached) {
  220. return;
  221. } else {
  222. this.totalSecsLeft = newTotalSecsLeft;
  223. }
  224. // Check if the countdown has elapsed
  225. this.elapsed = (now >= this.finalDate);
  226. // Calculate the offsets
  227. this.offset = {
  228. seconds : this.totalSecsLeft % 60,
  229. minutes : Math.floor(this.totalSecsLeft / 60) % 60,
  230. hours : Math.floor(this.totalSecsLeft / 60 / 60) % 24,
  231. days : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
  232. daysToWeek : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7,
  233. daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368),
  234. weeks : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7),
  235. weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4,
  236. months : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368),
  237. years : Math.abs(this.finalDate.getFullYear()-now.getFullYear()),
  238. totalDays : Math.floor(this.totalSecsLeft / 60 / 60 / 24),
  239. totalHours : Math.floor(this.totalSecsLeft / 60 / 60),
  240. totalMinutes: Math.floor(this.totalSecsLeft / 60),
  241. totalSeconds: this.totalSecsLeft
  242. };
  243. // Dispatch an event
  244. if(!this.options.elapse && this.totalSecsLeft === 0) {
  245. this.stop();
  246. this.dispatchEvent('finish');
  247. } else {
  248. this.dispatchEvent('update');
  249. }
  250. },
  251. dispatchEvent: function(eventName) {
  252. var event = $.Event(eventName + '.countdown');
  253. event.finalDate = this.finalDate;
  254. event.elapsed = this.elapsed;
  255. event.offset = $.extend({}, this.offset);
  256. event.strftime = strftime(this.offset);
  257. this.$el.trigger(event);
  258. }
  259. });
  260. // Register the jQuery selector actions
  261. $.fn.countdown = function() {
  262. var argumentsArray = Array.prototype.slice.call(arguments, 0);
  263. return this.each(function() {
  264. // If no data was set, jQuery.data returns undefined
  265. var instanceNumber = $(this).data('countdown-instance');
  266. // Verify if we already have a countdown for this node ...
  267. // Fix issue #22 (Thanks to @romanbsd)
  268. if (instanceNumber !== undefined) {
  269. var instance = instances[instanceNumber],
  270. method = argumentsArray[0];
  271. // If method exists in the prototype execute
  272. if(Countdown.prototype.hasOwnProperty(method)) {
  273. instance[method].apply(instance, argumentsArray.slice(1));
  274. // If method look like a date try to set a new final date
  275. } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) {
  276. instance.setFinalDate.call(instance, method);
  277. // Allow plugin to restart after finished
  278. // Fix issue #38 (thanks to @yaoazhen)
  279. instance.start();
  280. } else {
  281. $.error('Method %s does not exist on jQuery.countdown'
  282. .replace(/\%s/gi, method));
  283. }
  284. } else {
  285. // ... if not we create an instance
  286. new Countdown(this, argumentsArray[0], argumentsArray[1]);
  287. }
  288. });
  289. };
  290. });