/*global define:false */ /* * ======================= * jQuery Timer Plugin * ======================= * Start/Stop/Resume a time in any HTML element */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else { factory(root.jQuery); } }(this, function($) { // PRIVATE var options = { seconds: 0, // default seconds value to start timer from editable: false, // this will let users make changes to the time restart: false, // this will enable stop or continue after a timer callback duration: null, // duration to run callback after // callback to run after elapsed duration callback: function() { alert('Time up!'); }, startTimer: function() {}, pauseTimer: function() {}, resumeTimer: function() {}, resetTimer: function() {}, removeTimer: function() {}, repeat: false, // this will repeat callback every n times duration is elapsed countdown: false, // if true, this will render the timer as a countdown if duration > 0 format: null, // this sets the format in which the time will be printed updateFrequency: 1000, // How often should timer display update (default 500ms) state: 'running' }, display = 'html', // to be used as $el.html in case of div and $el.val in case of input type text // Constants for various states of the timer TIMER_STOPPED = 'stopped', TIMER_RUNNING = 'running', TIMER_PAUSED = 'paused'; /** * Common function to start or resume a timer interval */ function startTimerInterval(timer) { var element = timer.element; $(element).data('intr', setInterval(incrementSeconds.bind(timer), timer.options.updateFrequency)); $(element).data('isTimerRunning', true); } /** * Common function to stop timer interval */ function stopTimerInterval(timer) { clearInterval($(timer.element).data('intr')); $(timer.element).data('isTimerRunning', false); } /** * Increment total seconds by subtracting startTime from the current unix timestamp in seconds * and call render to display pretty time */ function incrementSeconds() { $(this.element).data('totalSeconds', getUnixSeconds() - $(this.element).data('startTime')); render(this); // Check if totalSeconds is equal to duration if any if ($(this.element).data('duration') && $(this.element).data('totalSeconds') % $(this.element).data('duration') === 0) { // If 'repeat' is not requested then disable the duration if (!this.options.repeat) { $(this.element).data('duration', null); this.options.duration = null; } // If this is a countdown, then end it as duration has completed if (this.options.countdown) { stopTimerInterval(this); this.options.countdown = false; $(this.element).data('state', TIMER_STOPPED); } // Run the default callback this.options.callback(); } } /** * Render pretty time */ function render(timer) { return; var element = timer.element, sec = $(element).data('totalSeconds'); if (timer.options.countdown && ($(element).data('duration') > 0)) { sec = $(element).data('duration') - $(element).data('totalSeconds'); } $(element)[display](secondsToTime(sec, timer)); $(element).data('seconds', sec); } /** * Method to make timer field editable * This method hard binds focus & blur events to pause & resume * and recognizes built-in pretty time (for eg 12 sec OR 3:34 min) * It won't recognize user created formats. * Users may not always want this hard bound. In such a case, * do not use the editable property. Instead bind custom functions * to blur and focus. */ function makeEditable(timer) { var element = timer.element; $(element).on('focus', function() { pauseTimer(timer); }); $(element).on('blur', function() { // eg. 12 sec 3:34 min 12:30 min var val = $(element)[display](), valArr; if (val.indexOf('sec') > 0) { // sec $(element).data('totalSeconds', Number(val.replace(/\ssec/g, ''))); } else if (val.indexOf('min') > 0) { // min val = val.replace(/\smin/g, ''); valArr = val.split(':'); $(element).data('totalSeconds', Number(valArr[0] * 60) + Number(valArr[1])); } else if (val.match(/\d{1,2}:\d{2}:\d{2}/)) { // hrs valArr = val.split(':'); $(element).data('totalSeconds', Number(valArr[0] * 3600) + Number(valArr[1] * 60) + Number(valArr[2])); } resumeTimer(timer); }); } /** * Get the current unix timestamp in seconds * @return {Number} [unix timestamp in seconds] */ function getUnixSeconds() { return Math.round(new Date().getTime() / 1000); } /** * Convert a number of seconds into an object of hours, minutes and seconds * @param {Number} sec [Number of seconds] * @return {Object} [An object with hours, minutes and seconds representation of the given seconds] */ function sec2TimeObj(sec) { var hours = 0, minutes = Math.floor(sec / 60), seconds; // Hours if (sec >= 3600) { hours = Math.floor(sec / 3600); } // Minutes if (sec >= 3600) { minutes = Math.floor(sec % 3600 / 60); } // Prepend 0 to minutes under 10 if (minutes < 10 && hours > 0) { minutes = '0' + minutes; } // Seconds seconds = sec % 60; // Prepend 0 to seconds under 10 if (seconds < 10 && (minutes > 0 || hours > 0)) { seconds = '0' + seconds; } return { hours: hours, minutes: minutes, seconds: seconds }; } /** * Convert the given seconds to an object made up of hours, minutes and seconds and return a pretty display * @param {Number} sec [Second to display as pretty time] * @return {String} [Pretty time] */ function secondsToTime(sec, timer) { var time = '', timeObj = sec2TimeObj(sec); if (timer.options.format) { var formatDef = [ {identifier: '%h', value: timeObj.hours, pad: false}, {identifier: '%m', value: timeObj.minutes, pad: false}, {identifier: '%s', value: timeObj.seconds, pad: false}, {identifier: '%H', value: parseInt(timeObj.hours), pad: true}, {identifier: '%M', value: parseInt(timeObj.minutes), pad: true}, {identifier: '%S', value: parseInt(timeObj.seconds), pad: true} ]; time = timer.options.format; formatDef.forEach(function(format) { time = time.replace( new RegExp(format.identifier.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'), 'g'), (format.pad) ? ((format.value < 10) ? '0' + format.value : format.value) : format.value ); }); } else { if (timeObj.hours) { time = timeObj.hours + ':' + timeObj.minutes + ':' + timeObj.seconds; } else { if (timeObj.minutes) { time = timeObj.minutes + ':' + timeObj.seconds + ' min'; } else { time = timeObj.seconds + ' sec'; } } } return time; } /** * Convert a string time like 5m30s to seconds * If a number (eg 300) is provided, then return as is * @param {Number|String} time [The human time to convert to seconds] * @return {Number} [Number of seconds] */ function timeToSeconds(time) { // In case the passed arg is a number, then use that as number of seconds if (!isNaN(Number(time))) { return time; } var hMatch = time.match(/\d{1,2}h/), mMatch = time.match(/\d{1,2}m/), sMatch = time.match(/\d{1,2}s/), seconds = 0; time = time.toLowerCase(); // @todo: throw an error in case of faulty time value like 5m61s or 61m if (hMatch) { seconds += Number(hMatch[0].replace('h', '')) * 3600; } if (mMatch) { seconds += Number(mMatch[0].replace('m', '')) * 60; } if (sMatch) { seconds += Number(sMatch[0].replace('s', '')); } return seconds; } // TIMER INTERFACE function startTimer(timer) { var element = timer.element; if (!$(element).data('isTimerRunning')) { render(timer); startTimerInterval(timer); $(element).data('state', TIMER_RUNNING); timer.options.startTimer.bind(timer).call(); } } function pauseTimer(timer) { var element = timer.element; if ($(element).data('isTimerRunning')) { stopTimerInterval(timer); $(element).data('state', TIMER_PAUSED); timer.options.pauseTimer.bind(timer).call(); } } function resumeTimer(timer) { var element = timer.element; if (!$(element).data('isTimerRunning')) { $(element).data('startTime', getUnixSeconds() - $(element).data('totalSeconds')); startTimerInterval(timer); $(element).data('state', TIMER_RUNNING); timer.options.resumeTimer.bind(timer).call(); } } function resetTimer(timer) { var element = timer.element; $(element).data('startTime', 0); $(element).data('totalSeconds', 0); $(element).data('seconds', 0); $(element).data('state', TIMER_STOPPED); $(element).data('duration', timer.options.duration); timer.options.resetTimer.bind(timer).call(); } function removeTimer(timer) { var element = timer.element; stopTimerInterval(timer); timer.options.removeTimer.bind(timer).call(); $(element).data('plugin_' + pluginName, null); $(element).data('seconds', null); $(element).data('state', null); $(element)[display](''); } // TIMER PROTOTYPE var Timer = function(element, userOptions) { var elementType; this.options = options = $.extend(this.options, options, userOptions); this.element = element; // Setup total seconds from options.seconds (if any) $(element).data('totalSeconds', options.seconds); // Setup start time if seconds were provided $(element).data('startTime', getUnixSeconds() - $(element).data('totalSeconds')); $(element).data('seconds', $(element).data('totalSeconds')); $(element).data('state', TIMER_STOPPED); // Check if this is a input/textarea element or not elementType = $(element).prop('tagName').toLowerCase(); if (elementType === 'input' || elementType === 'textarea') { display = 'val'; } if (this.options.duration) { $(element).data('duration', timeToSeconds(this.options.duration)); this.options.duration = timeToSeconds(this.options.duration); } if (this.options.editable) { makeEditable(this); } }; /** * Initialize the plugin with public methods */ Timer.prototype = { start: function() { startTimer(this); }, pause: function() { pauseTimer(this); }, resume: function() { resumeTimer(this); }, reset: function() { resetTimer(this); }, remove: function() { removeTimer(this); } }; // INITIALIZE THE PLUGIN var pluginName = 'timer'; $.fn[pluginName] = function(options) { options = options || 'start'; return this.each(function() { /** * Allow the plugin to be initialized on an element only once * This way we can call the plugin's internal function * without having to reinitialize the plugin all over again. */ if (!($.data(this, 'plugin_' + pluginName) instanceof Timer)) { /** * Create a new data attribute on the element to hold the plugin name * This way we can know which plugin(s) is/are initialized on the element later */ $.data(this, 'plugin_' + pluginName, new Timer(this, options)); } /** * Use the instance of this plugin derived from the data attribute for this element * to conduct whatever action requested as a string parameter. */ var instance = $.data(this, 'plugin_' + pluginName); /** * Provision for calling a function from this plugin * without initializing it all over again */ if (typeof options === 'string') { if (typeof instance[options] === 'function') { /* Pass in 'instance' to provide for the value of 'this' in the called function */ instance[options].call(instance); } } /** * Allow passing custom options object */ if (typeof options === 'object') { if (instance.options.state === TIMER_RUNNING) { instance.start.call(instance); } else { render(instance); } } }); }; }));