// AMD support (Thanks to @FagnerMartinsBrack) ;(function(factory) { 'use strict'; if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else { factory(jQuery); } })(function($){ 'use strict'; var instances = [], matchers = [], defaultOptions = { precision: 100, // 0.1 seconds, used to update the DOM elapse: false, defer: false }; // Miliseconds matchers.push(/^[0-9]*$/.source); // Month/Day/Year [hours:minutes:seconds] matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/ .source); // Year/Day/Month [hours:minutes:seconds] and // Year-Day-Month [hours:minutes:seconds] matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/ .source); // Cast the matchers to a regular expression object matchers = new RegExp(matchers.join('|')); // Parse a Date formatted has String to a native object function parseDateString(dateString) { // Pass through when a native object is sent if(dateString instanceof Date) { return dateString; } // Caste string to date object if(String(dateString).match(matchers)) { // If looks like a milisecond value cast to number before // final casting (Thanks to @msigley) if(String(dateString).match(/^[0-9]*$/)) { dateString = Number(dateString); } // Replace dashes to slashes if(String(dateString).match(/\-/)) { dateString = String(dateString).replace(/\-/g, '/'); } return new Date(dateString); } else { throw new Error('Couldn\'t cast `' + dateString + '` to a date object.'); } } // Map to convert from a directive to offset object property var DIRECTIVE_KEY_MAP = { 'Y': 'years', 'm': 'months', 'n': 'daysToMonth', 'd': 'daysToWeek', 'w': 'weeks', 'W': 'weeksToMonth', 'H': 'hours', 'M': 'minutes', 'S': 'seconds', 'D': 'totalDays', 'I': 'totalHours', 'N': 'totalMinutes', 'T': 'totalSeconds' }; // Returns an escaped regexp from the string function escapedRegExp(str) { var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); return new RegExp(sanitize); } // Time string formatter function strftime(offsetObject) { return function(format) { var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi); if(directives) { for(var i = 0, len = directives.length; i < len; ++i) { var directive = directives[i] .match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/), regexp = escapedRegExp(directive[0]), modifier = directive[1] || '', plural = directive[3] || '', value = null; // Get the key directive = directive[2]; // Swap shot-versions directives if(DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) { value = DIRECTIVE_KEY_MAP[directive]; value = Number(offsetObject[value]); } if(value !== null) { // Pluralize if(modifier === '!') { value = pluralize(plural, value); } // Add zero-padding if(modifier === '') { if(value < 10) { value = '0' + value.toString(); } } // Replace the directive format = format.replace(regexp, value.toString()); } } } format = format.replace(/%%/, '%'); return format; }; } // Pluralize function pluralize(format, count) { var plural = 's', singular = ''; if(format) { format = format.replace(/(:|;|\s)/gi, '').split(/\,/); if(format.length === 1) { plural = format[0]; } else { singular = format[0]; plural = format[1]; } } // Fix #187 if(Math.abs(count) > 1) { return plural; } else { return singular; } } // The Final Countdown var Countdown = function(el, finalDate, options) { this.el = el; this.$el = $(el); this.interval = null; this.offset = {}; this.options = $.extend({}, defaultOptions); // console.log(this.options); // This helper variable is necessary to mimick the previous check for an // event listener on this.$el. Because of the event loop there might not // be a registered event listener during the first tick. In order to work // as expected a second tick is necessary, so that the events can be fired // and handled properly. this.firstTick = true; // Register this instance this.instanceNumber = instances.length; instances.push(this); // Save the reference this.$el.data('countdown-instance', this.instanceNumber); // Handle options or callback if (options) { // Register the callbacks when supplied if(typeof options === 'function') { this.$el.on('update.countdown', options); this.$el.on('stoped.countdown', options); this.$el.on('finish.countdown', options); } else { this.options = $.extend({}, defaultOptions, options); } } // Set the final date and start this.setFinalDate(finalDate); // Starts the countdown automatically unless it's defered, // Issue #198 if (this.options.defer === false) { this.start(); } }; $.extend(Countdown.prototype, { start: function() { if(this.interval !== null) { clearInterval(this.interval); } var self = this; this.update(); this.interval = setInterval(function() { self.update.call(self); }, this.options.precision); }, stop: function() { clearInterval(this.interval); this.interval = null; this.dispatchEvent('stoped'); }, toggle: function() { if (this.interval) { this.stop(); } else { this.start(); } }, pause: function() { this.stop(); }, resume: function() { this.start(); }, remove: function() { this.stop.call(this); instances[this.instanceNumber] = null; // Reset the countdown instance under data attr (Thanks to @assiotis) delete this.$el.data().countdownInstance; }, setFinalDate: function(value) { this.finalDate = parseDateString(value); // Cast the given date }, update: function() { // Stop if dom is not in the html (Thanks to @dleavitt) if(this.$el.closest('html').length === 0) { this.remove(); return; } var now = new Date(), newTotalSecsLeft; // Create an offset date object newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs // Calculate the remaining time newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs // If is not have to elapse set the finish newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 : Math.abs(newTotalSecsLeft); // Do not proceed to calculation if the seconds have not changed or // during the first tick if (this.totalSecsLeft === newTotalSecsLeft || this.firstTick) { this.firstTick = false; return; } else { this.totalSecsLeft = newTotalSecsLeft; } // Check if the countdown has elapsed this.elapsed = (now >= this.finalDate); // Calculate the offsets this.offset = { seconds : this.totalSecsLeft % 60, minutes : Math.floor(this.totalSecsLeft / 60) % 60, hours : Math.floor(this.totalSecsLeft / 60 / 60) % 24, days : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, daysToWeek : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368), weeks : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7), weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4, months : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368), years : Math.abs(this.finalDate.getFullYear()-now.getFullYear()), totalDays : Math.floor(this.totalSecsLeft / 60 / 60 / 24), totalHours : Math.floor(this.totalSecsLeft / 60 / 60), totalMinutes: Math.floor(this.totalSecsLeft / 60), totalSeconds: this.totalSecsLeft }; // Dispatch an event if(!this.options.elapse && this.totalSecsLeft === 0) { this.stop(); this.dispatchEvent('finish'); } else { this.dispatchEvent('update'); } }, dispatchEvent: function(eventName) { var event = $.Event(eventName + '.countdown'); event.finalDate = this.finalDate; event.elapsed = this.elapsed; event.offset = $.extend({}, this.offset); event.strftime = strftime(this.offset); this.$el.trigger(event); } }); // Register the jQuery selector actions $.fn.countdown = function() { var argumentsArray = Array.prototype.slice.call(arguments, 0); return this.each(function() { // If no data was set, jQuery.data returns undefined var instanceNumber = $(this).data('countdown-instance'); // Verify if we already have a countdown for this node ... // Fix issue #22 (Thanks to @romanbsd) if (instanceNumber !== undefined) { var instance = instances[instanceNumber], method = argumentsArray[0]; // If method exists in the prototype execute if(Countdown.prototype.hasOwnProperty(method)) { instance[method].apply(instance, argumentsArray.slice(1)); // If method look like a date try to set a new final date } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) { instance.setFinalDate.call(instance, method); // Allow plugin to restart after finished // Fix issue #38 (thanks to @yaoazhen) instance.start(); } else { $.error('Method %s does not exist on jQuery.countdown' .replace(/\%s/gi, method)); } } else { // ... if not we create an instance new Countdown(this, argumentsArray[0], argumentsArray[1]); } }); }; });