1 /**
  2  * @fileOverview The jQuery Chrono plugin
  3  * Copyright (c) 2011 Arthur Klepchukov
  4  * Licensed under the BSD license (BSD_LICENSE.txt)
  5  *
  6  * @author <a href="mailto:first-name.last-name@gmail.com">Arthur Klepchukov</a>
  7  * @version 1.2
  8  */
  9 
 10 /*global jQuery, $ */ 
 11 var jQueryChrono;
 12 
 13 /**
 14  * @namespace Main namespace
 15  */
 16 jQueryChrono = (function() {
 17   /**
 18    * Syntactic sugar for setTimeout.
 19    * <pre>
 20    *    setTimeout(function() { ... }, 300000); // becomes:
 21    *    $.after(5, "minutes", function() { ... });
 22    *    
 23    *    // other valid calls:
 24    *    $.after(100, function() { ... }); // 100 milliseconds
 25    *    $.after("9.7", function() { ... }); // 9.7 milliseconds
 26    *    $.after("50sec", function() { ... }); // 50 seconds
 27    *    $.after("33", "hours", function() { ... }); // 33 hours
 28    *    $.after("minute", function() { ... }); // 1 minute
 29    *    $.after("1 hour, 2 minutes, 15 seconds", function() { ... }); // 1:02:15 hours
 30    *    $.after("1min, 15 s", function() { ... }); // 1:15 minutes
 31    * </pre>
 32    * Valid time units include: 
 33    * <strong>millisecond, second, minute, hour, & day</strong><br />
 34    * along with all their common abbreviations and pluralizations.<br />
 35    * (See full list of valid time units: {@link jQueryChrono-valid_units})
 36    * @name jQuery.after
 37    */
 38   function after() {
 39     var timer = jQueryChrono.create_timer.apply(this, arguments);
 40     return setTimeout(timer.callback, timer.when);
 41   }
 42   
 43   /**
 44    * Syntactic sugar for setTimeout.
 45    * <pre>
 46    *    setInterval(function() { ... }, 300000); // becomes:
 47    *    $.every(5, "minutes", function() { ... });
 48    * </pre>
 49    * Supports the same syntax and arguments as {@link jQuery.after}
 50    * @name jQuery.every
 51    */
 52   function every() {
 53     var timer = jQueryChrono.create_timer.apply(this, arguments);
 54     return setInterval(timer.callback, timer.when);
 55   }
 56   
 57   /**
 58    * Reasonable defaults (delay: 4, units: ms), based on how Mozilla works with timers:
 59    * https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
 60    * @constant
 61    */
 62   var defaults = {
 63         delay: 4,
 64         units: "milliseconds"
 65       },
 66       // each supported unit, in milliseconds
 67       ms  = 1,
 68       sec = ms  * 1000,
 69       min = sec * 60,
 70       hr  = min * 60,
 71       day = hr  * 24;
 72       
 73   /**
 74    * The supported units of time:<br />
 75    *  millisecond, milliseconds, ms,<br />
 76    *  second, seconds, sec, secs, s,<br />
 77    *  minute, minutes, min, mins, m,<br />
 78    *  hour, hours, hr, hrs, h,<br />
 79    *  day, days, d
 80    * @constant
 81    */
 82   var valid_units = {
 83         "millisecond" : ms,
 84         "milliseconds": ms,
 85         "ms"          : ms,
 86         
 87         "second"      : sec,
 88         "seconds"     : sec,
 89         "sec"         : sec,
 90         "secs"        : sec,
 91         "s"           : sec,
 92         
 93         "minute"      : min,
 94         "minutes"     : min,
 95         "min"         : min,
 96         "mins"        : min,
 97         "m"           : min,
 98         
 99         "hour"        : hr,
100         "hours"       : hr,
101         "hr"          : hr,
102         "hrs"         : hr,
103         "h"           : hr,
104         
105         "day"         : day,
106         "days"        : day,
107         "d"           : day
108       };
109   
110   /**
111    * Trim string. Copied from jQuery.
112    * @param {String} text to be trimmed
113    * @returns {String} string without leading or trailing spaces
114    */
115   var trim;
116   if (typeof(jQuery) !== 'undefined') {
117     trim = jQuery.trim;
118   } else {
119     trim = String.prototype.trim2 ?
120         function(text) {
121                 return text == null ? "" :
122                 String.prototype.trim.call( text );
123         } : function(text) {
124             return text == null ? "" :
125                 text.toString().replace(/^[\s\xA0]+/, "").replace(/[\s\xA0]+$/, "");
126         }
127   }
128   
129   /**
130    * Parses a numerical delay from the given arguments.
131    * 
132    * @param {Object} parsed The arguments parsed so far
133    * @param {arguments} args The original arguments from the caller
134    *  (e.g. {@link jQueryChrono.create_timer})
135    * @throws Exception if the delay is not a number
136    * @returns {Object} The parsed parameter updated with the parsed delay
137    */
138   function parse_delay(parsed, args) {
139     if (typeof args[0] === "string") {
140       parsed.delay = parseFloat(args[0], 10);
141       if (isNaN(parsed.delay)) {
142         parsed.delay = (valid_units[args[0]] > ms) ? 1 : defaults.delay;
143       }
144     } else {
145       parsed.delay = args[0];
146     }
147     
148     if (typeof parsed.delay !== "number" || isNaN(parsed.delay)) {
149       throw "$.after and $.every - Require a numerical delay as the 1st argument";
150     }
151     
152     return parsed;
153   }
154   
155   /**
156    * Parses a units string from the given arguments.
157    * 
158    * @param {Object} parsed The arguments parsed so far
159    * @param {arguments} args The original arguments from the caller
160    *  (e.g. {@link jQueryChrono.create_timer})
161    * @throws Exception if the units are not a key of {@link jQueryChrono-valid_units}
162    * @returns {Object} The parsed parameter updated with the parsed units
163    */
164   function parse_units(parsed, args) {
165     if (typeof args[0] === "string" && parsed.delay !== null) {
166       parsed.units = trim(args[0].replace(parsed.delay, "")) || null; // "9.7sec" || "9.7"
167     }
168     if (typeof args[1] === "string") {
169       parsed.units = args[1];
170     }
171     if (parsed.units === null && args.length === 2) { // no units specified
172       parsed.units = defaults.units;
173     }
174     
175     if (typeof valid_units[parsed.units] !== "number") {
176       throw "$.after and $.every - Require a valid unit of time as the 2nd argument";
177     }
178     
179     return parsed;
180   }
181   
182   /**
183    * Parses a callback function from the given arguments.
184    * 
185    * @param {Object} parsed The arguments parsed so far
186    * @param {arguments} args The original arguments from the caller
187    *  (e.g. {@link jQueryChrono.create_timer})
188    * @throws Exception if the callback is not a function
189    * @returns {Object} The parsed parameter updated with the parsed callback
190    */
191   function parse_callback(parsed, args) {
192     parsed.callback = args[args.length - 1];
193     
194     if (typeof(parsed.callback) != 'function') {
195       throw "$.after and $.every - Require a callback as the last argument";
196     }
197     
198     return parsed;
199   }
200   
201   /**
202    * Parses a string sequence of delay with unit arguments.
203    * 
204    * @param {Object} parsed The arguments parsed so far
205    * @param {arguments} args The original arguments from the caller
206    *  (e.g. {@link jQueryChrono.create_timer})
207    * @throws Exception if the sequence contains blanks, invalid delays, or invalid units
208    * @returns {Object} The parsed parameter updated with the parsed delay 
209    *  and units, each set to the minimum unit in the sequence
210    * @example "1 minute, 15 seconds" // parsed = { delay: 75, units: "seconds" }
211    */
212   function parse_sequence(parsed, args) {
213     var commaArgs, name, minInterval = '', timer, timers = [];
214     
215     // if the first arg is a string, try splitting it on commas
216     commaArgs = (typeof args[0] === 'string') ? args[0].split(',') : [];
217     
218     // create a timer for each sequence element
219     for (name in commaArgs) {
220       if (! /\d\s?\w+/.test(commaArgs[name])) {
221         throw "$.after and $.every - Invalid delays with units sequence: " + 
222           commaArgs.join(',');
223       }
224       timer = create_timer.call(this, commaArgs[name], parsed.callback);
225       
226       // keep track of the minimum interval so we can convert the whole set to this in the next loop
227       if (minInterval === '' || valid_units[timer.units] <= valid_units[minInterval]) {
228         minInterval = timer.units;
229       }
230       timers[name] = timer;
231     }
232     parsed.units = minInterval;
233     
234     // convert each timer to the lowest interval, then add those units to parsed.delay
235     for (name in timers) {
236       parsed.delay += timers[name].delay * (valid_units[timers[name].units] / valid_units[minInterval]);
237     }
238     return parsed;
239   }
240   
241   /**
242    * Accepts more human-readable arguments for creating JavaScript timers and 
243    * converts them to values that can be inspected and passed along to 
244    * setTimeout or setInterval.<br />
245    * If the time when the timer should run is negative or faster than 
246    * the default ({@link jQueryChrono-defaults}), 
247    * it uses the default delay and default units.
248    *
249    * @param {Number|String} delay|delay+units 
250    *  Combined with units, represents when a timer should run.<br />
251    *  Units can be specified as part of this argument as a suffix of the string and 
252    *  must represent a valid unit of time ({@link jQueryChrono-valid_units}).
253    * @param {String} [units] 
254    *  Combined with the delay, represents when a timer should run.
255    *  If present, must be a valid unit of time ({@link jQueryChrono-valid_units}).
256    * @param {Function} callback 
257    *  Represents the code to be executed when the timer is ready.
258    * 
259    * @returns {Object} An object with a valid "delay", a valid "units" string, 
260    *  a time, in milliseconds, of "when" the timer should run, and 
261    *  a "callback" that the timer should execute when it's ready.
262    * @static
263    */
264   function create_timer() {
265     var parsed = {
266       delay : null,
267       units : null,
268       when : null,
269       callback : null
270     };
271     
272     if (arguments.length < 2 || arguments.length > 3) {
273       throw "$.after and $.every - Accept only 2 or 3 arguments";
274     }
275     
276     parsed = parse_callback(parsed, arguments);
277     if (typeof arguments[0] === 'string' && arguments[0].search(',') > -1) {
278       parsed = parse_sequence(parsed, arguments);
279     } else {
280       parsed = parse_delay(parsed, arguments);
281       parsed = parse_units(parsed, arguments);
282     }
283     
284     // Reset to defaults, if necessary
285     if (parsed.delay < defaults.delay && parsed.units === defaults.units) {
286       parsed.delay = defaults.delay;
287     }
288     if (parsed.delay < 0) {
289       parsed.delay = defaults.delay;
290       parsed.units = defaults.units;
291     }
292     
293     parsed.when = parsed.delay * valid_units[parsed.units];
294     
295     return parsed;
296   }
297   
298   /** @scope jQueryChrono */
299   return {
300     every : every,
301     after : after,
302     defaults : defaults,
303     valid_units : valid_units,
304     create_timer : function() {
305       return create_timer.apply(this, arguments);
306     }
307   };
308 }());
309 
310 /**
311  * The extended jQuery library
312  * @name jQuery
313  * @class the extended jQuery library
314  * @exports $ as jQuery
315  */
316 if (typeof(jQuery) !== 'undefined') {
317   jQuery.extend({
318     after : jQueryChrono.after,
319     every : jQueryChrono.every
320   });
321 }
322