1 /**
  2  * @namespace Core utilities and functions.
  3  */
  4 var Jelo = window.Jelo || {
  5     version : 1.21
  6 };
  7 
  8 /**
  9  * Can be used to create unique IDs or global counters. Every time this function is called, a number will be returned
 10  * which is 1 greater than the number previously returned.
 11  * 
 12  * @function
 13  * @return {Number} A unique (to this function), autoincrementing positive integer. The first number returned is 1.
 14  */
 15 Jelo.uID = function() {
 16     var id = 1; // initial value
 17     return function() {
 18         return id++;
 19     };
 20 }();
 21 
 22 /**
 23  * A reusable no-operation function. Useful for placeholders.
 24  */
 25 Jelo.emptyFn = function() {};
 26 
 27 /**
 28  * Performs a function on each item in a collection. If the first argument is not an array or object, the function is
 29  * called once with it. If the function ever returns false, execution halts immediately and the "failed" index is
 30  * returned. Normally, Jelo.each will return null.
 31  * 
 32  * @param {Array|NodeList|Object} collection The object over which to iterate.
 33  * @param {Function} fn The function to execute for each item in the collection. Arguments passed to the function will
 34  *        be the item, its index, and the complete array. For example, myFunc(collection[index], index, collection)
 35  * @param {Object} [scope] The scope ("this") in which to execute the function. By default, the scope will be the
 36  *        current item being iterated across.
 37  */
 38 Jelo.each = function(a, f, s) {
 39     var i, ai;
 40     if (typeof a.length == "number") {
 41         for (i = 0, l = a.length; i < l; i++) {
 42             ai = a[i];
 43             if (typeof a[i] != 'undefined') {
 44                 if (f.call(s || ai, ai, i, a) === false) {
 45                     return i;
 46                 }
 47             }
 48         }
 49     } else if (typeof a == "object") {
 50         for (i in a) {
 51             ai = a[i];
 52             if (a.hasOwnProperty(i) && typeof ai != 'undefined') {
 53                 if (f.call(s || ai, ai, i, a) === false) {
 54                     return i;
 55                 }
 56             }
 57         }
 58     }
 59 };
 60 
 61 /**
 62  * Binds arguments to a function and optional scope.
 63  * 
 64  * @param {Function} fn The function to execute when the delegate is called.
 65  * @param {Object} [scope=window] The scope in which to execute the chosen function.
 66  * @param {Mixed} [...] Additional parameters to pass to the function
 67  * @return {Function} The new function reference.
 68  */
 69 Jelo.delegate = function(fn, scope) {
 70     var s = scope || window;
 71     var a = [].slice.call(arguments, 2);
 72     return function() {
 73         fn.apply(s, a);
 74     };
 75 };
 76 
 77 // Normalizes window.console to prevent debug errors when there is no console.
 78 (function() {
 79     if (!('console' in window) || !('firebug' in console)) {
 80         var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time",
 81             "timeEnd", "count", "trace", "profile", "profileEnd"];
 82         window.console = {};
 83         for (var i = 0; i < names.length; ++i) {
 84             // TODO: write to a hidden div, provide a method to toggle
 85             // visibility
 86             window.console[names[i]] = Jelo.emptyFn;
 87         }
 88     }
 89     /**
 90      * @namespace
 91      * @name Console
 92      * @memberOf Jelo
 93      */
 94     Jelo.Console = {
 95         firebug : window.console
 96     }; // Google likes to eat the console :(
 97     /**
 98      * @function
 99      * @param {Mixed} Information to log in the debug console.
100      */
101     Jelo.Console.log = window.console.log;
102     /**
103      * @function
104      * @returns {Array} The current call stack.
105      */
106     Jelo.Console.getStackTrace = function() {
107         var /* counter */i, /* length */len,
108             /* stack */s = [],
109             /* isStackPopulated */isPop = false, /* lines */l;
110         try {
111             hb.stone.javascript += 0; // raise an error
112         } catch (e) {
113             if (e.stack) {
114                 // Firefox
115                 l = e.stack.split("\n");
116                 for (i = 0, len = l.length; i < len; i++) {
117                     if (l[i].match(/^\s*[a-z0-9\-_\$]+\(/i)) {
118                         s.push(l[i]);
119                     }
120                 }
121                 s.shift(); // remove call to getStackTrace
122                 isPop = true;
123             } else if (window.opera && e.message) { // Opera
124                 l = e.message.split("\n");
125                 for (i = 0, len = l.length; i < len; i++) {
126                     if (l[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
127                         var entry = l[i];
128                         if (l[i + 1]) {
129                             entry += " at " + l[i + 1]; // file info
130                             i++;
131                         }
132                         s.push(entry);
133                     }
134                 }
135                 s.shift(); // remove call to getStackTrace
136                 isPop = true;
137             }
138         }
139         if (!isPop) {
140             // IE and Safari
141             var curr = arguments.callee;
142             while ((curr = curr.caller)) {
143                 var fn = curr.toString();
144                 var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
145                 s.push(fname);
146             }
147         }
148         return s;
149         
150     };
151     
152 })();
153 
154 /*
155  * Normalizes setTimeout and setInterval behavior across all browsers. Typically, IE does not treat additional arguments
156  * correctly, and Firefox adds a "lateness" argument to the supplied function call.
157  */
158 (function(f) {
159     /**
160      * @function
161      * @name setTimeout
162      * @param {Function} fn Method to invoke.
163      * @param {Number} ms Milliseconds to delay before invoking fn.
164      * @param {Mixed} [...] Additional arguments to be passed to fn when it is called.
165      * @returns {Number} Resource id, can be cancelled using clearTimeout(id)
166      */
167     window.setTimeout = f(window.setTimeout);
168     /**
169      * @function
170      * @name setInterval
171      * @param {Function} fn Method to invoke.
172      * @param {Number} ms Milliseconds to delay between intervals
173      * @param {Mixed} [...] Additional arguments to be passed to fn when it is called.
174      * @returns {Number} Resource id, can be cancelled using clearInterval(id)
175      */
176     window.setInterval = f(window.setInterval);
177 })(function(f) {
178     return function(c, t) {
179         var a = [].slice.call(arguments, 2);
180         return ((typeof c == 'function')
181             ? f(function() {
182                 c.apply(this, a);
183             }, t)
184             : f.call(this, c, t));
185     };
186 });
187 
188 // Normalizes Internet Explorer's behavior to match modern browsers.
189 (function() {
190     if (typeof Array.prototype.indexOf == 'undefined') {
191         /**
192          * @memberOf Array
193          * @param {Mixed} x The item to attempt to find.
194          * @returns {Number} The item's index if found, -1 otherwise.
195          */
196         Array.prototype.indexOf = function(k) {
197             var len = this.length;
198             for (var i = 0; i < len; i++) {
199                 if (this[i] == k) {
200                     return i;
201                 }
202             }
203             return -1;
204         };
205         Array.indexOf = Array.prototype.indexOf;
206     }
207     if (typeof Array.prototype.lastIndexOf == 'undefined') {
208         /**
209          * @memberOf Array
210          * @param {Mixed} x The item to attempt to find.
211          * @returns {Number} The index of the item's last occurrence if found, -1 otherwise.
212          */
213         Array.prototype.lastIndexOf = function(k) {
214             var len = this.length;
215             for (var i = len - 1; i > -1; i--) {
216                 if (this[i] == k) {
217                     return i;
218                 }
219             }
220             return -1;
221         };
222         Array.lastIndexOf = Array.prototype.lastIndexOf;
223     }
224     if (typeof Array.prototype.find == 'undefined') {
225         /**
226          * @memberOf Array
227          * @param {Mixed} x The item to attempt to find, or a RegExp to match.
228          * @returns {Array|Boolean} An array of indeces at which the item was found, or at which the RegExp tested
229          *          positive. Boolean false if no element matched.
230          */
231         Array.prototype.find = function(k) {
232             var res = [];
233             var len = this.length;
234             for (var i = 0; i < len; i++) {
235                 if ((k.test && k.test(this[i])) || k === this[i]) {
236                     res.push(i);
237                 }
238             }
239             return !!res.length && res;
240         };
241         Array.find = Array.prototype.find;
242     }
243     if (typeof Array.prototype.shuffle == 'undefined') {
244         /**
245          * @memberOf Array
246          * @returns {Array} The array, randomized.
247          */
248         Array.prototype.shuffle = function() {
249             for (var j, x, i = this.length; i; j = parseInt(Math.random() * i, 10), x = this[--i], this[i] = this[j], this[j] = x) {}
250             return this;
251         };
252         Array.shuffle = Array.prototype.shuffle;
253     }
254 })();
255 
256 /**
257  * Allows you to check or uncheck a group of checkboxes by clicking and dragging across them (or their labels).
258  * 
259  * @function
260  * @name dragCheckbox
261  * @param {HTMLElement} [root=document] The element within which to apply "drag toggle" functionality.
262  * @memberOf Jelo
263  */
264 Jelo.dragCheckbox = function(root) {
265     root = root || document;
266     var dragging = false;
267     var current = false;
268     var undrag = function() {
269         dragging = false;
270     };
271     Jelo.un(document, "mouseup", undrag);
272     Jelo.on(document, "mouseup", undrag);
273     var getTarget = function(element) {
274         switch (element.tagName.toLowerCase()) {
275             case 'input' :
276                 return element;
277             case 'label' :
278                 Jelo.css(element, '-moz-user-select', 'none');
279                 Jelo.css(element, '-webkit-user-select', 'ignore');
280                 element.onselectstart = function() {
281                     return false;
282                 };
283                 var el = element.getAttribute('for') || element.getAttribute('htmlFor');
284                 return $('input#' + el);
285             default :
286                 return null; // invalid element
287         }
288     };
289     Jelo.each($$('[type=checkbox]', root), function() {
290         var down = function() {
291             var box = getTarget(this);
292             if (box) {
293                 dragging = true;
294                 box.checked = !box.checked;
295                 current = box.checked;
296             }
297         };
298         var over = function() {
299             var box = getTarget(this);
300             if (box && dragging) {
301                 box.checked = current;
302             }
303         };
304         var click = function(target, event) {
305             var box = getTarget(this);
306             if (box) {
307                 box.checked = current;
308             }
309         };
310         Jelo.un(this, "mousedown", down);
311         Jelo.un(this, "mouseover", over);
312         Jelo.un(this, "click", click);
313         Jelo.on(this, "mousedown", down);
314         Jelo.on(this, "mouseover", over);
315         Jelo.on(this, "click", click);
316         var label = $('label[for=' + this.id + ']', root);
317         if (label) {
318             Jelo.un(label, "mousedown", down);
319             Jelo.un(label, "mouseover", over);
320             Jelo.un(label, "click", click);
321             Jelo.on(label, "mousedown", down);
322             Jelo.on(label, "mouseover", over);
323             Jelo.on(label, "click", click);
324         }
325     });
326 };
327