1 /**
  2  * @namespace Cross-browser registration of event handlers, automatically normalizes the event object to provide web
  3  *            standard features such as preventDefault() and stopPropagation().
  4  */
  5 Jelo.Event = function() {
  6     
  7     /** @private convenience */
  8     var D = document;
  9     
 10     /** @private */
 11     var isDomReady = false;
 12     
 13     /** @private */
 14     var domFunctions = [];
 15     
 16     /** @private */
 17     var handlers = [];
 18     
 19     /** @private */
 20     var fireDomReady = function() {
 21         isDomReady = true;
 22         for (var i = 0; i < domFunctions.length; i++) {
 23             var fn = domFunctions[i];
 24             try {
 25                 fn();
 26             } catch (e) {
 27                 // TODO: log these internally so they can be shown if the developer desires
 28             }
 29         }
 30         domFunctions = [];
 31     };
 32     
 33     /** @private */
 34     var init = function() {
 35         if (D.addEventListener) {
 36             D.addEventListener("DOMContentLoaded", fireDomReady, false);
 37         } else if (Jelo.Env.isIE) {
 38             D.write("<script id='ieDomReady' defer " + "src=//:><\/script>");
 39             var ieReady = D.getElementById("ieDomReady");
 40             ieReady.onreadystatechange = function() {
 41                 if (ieReady.readyState === "complete") {
 42                     fireDomReady();
 43                 }
 44             };
 45         } else {
 46             var oldOnload = (typeof window.onload == "function")
 47                 ? window.onload
 48                 : function() {};
 49             window.onload = function() {
 50                 oldOnload();
 51                 fireDomReady();
 52             };
 53         }
 54         if (Jelo.Env.isWebkit) {
 55             var timerDomReady = setInterval(function() {
 56                 if (/loaded|complete/i.test(D.readyState)) {
 57                     fireDomReady.call(this);
 58                     clearInterval(timerDomReady);
 59                 }
 60             }, 10);
 61         }
 62     }();
 63     
 64     function find(el, ev, fn) {
 65         var handlers = el._handlers;
 66         if (!handlers) {
 67             return -1;
 68         }
 69         var d = el.document || el;
 70         var w = d.parentWindow;
 71         for (var i = handlers.length - 1; i >= 0; i--) {
 72             var a = w._allHandlers[handlers[i]];
 73             if (a.eventType == ev && a.handler == fn) {
 74                 return i;
 75             }
 76         }
 77         return -1;
 78     }
 79     
 80     function removeAllHandlers() {
 81         var w = this;
 82         var wa = w._allHandlers;
 83         for (var id in wa) {
 84             if (wa.hasOwnProperty(id)) {
 85                 var h = wa[id];
 86                 h.element.detachEvent('on' + h.eventType, h.wrappedHandler);
 87                 delete wa[id];
 88             }
 89         }
 90     }
 91     
 92     /** @scope Jelo.Event */
 93     return {
 94         /**
 95          * Fires after all elements in the document is available. In most cases, this function may be executed before
 96          * window.onload and before images fully load, which can result in a faster page response time.
 97          * 
 98          * @see Jelo#onReady
 99          */
100         onReady   : function(fn) {
101             if (typeof fn === "function") {
102                 if (isDomReady) {
103                     fn();
104                 } else {
105                     domFunctions.push(fn);
106                 }
107             }
108         },
109         /**
110          * Start listening for an event. Multiple listeners can be registered to a single element. Can be accessed via
111          * {@link Jelo.on}.
112          * 
113          * @param {HTMLElement} element HTML element to which Jelo should listen.
114          * @param {String} eventName The type of event for which Jelo should listen, such as "click" or "mouseup".
115          *        Should be all lowercase, and WITHOUT the IE prefix "on".
116          * @param {Function} handler Method to be invoked when the event occurs. The execution scope ("this") will be
117          *        the actual element that caught the event which, due to the DOM hierarchy, may be a child of the
118          *        element registered via this function. The function is passed the following arguments:
119          *        <ul>
120          *        <li>target: The element that caught the event.</li>
121          *        <li>event: The event object, normalized to conform to W3C standards.</li>
122          *        </ul>
123          */
124         add       : function(el, ev, fn) {
125             if (!el || !ev || !fn) {
126                 return;
127             }
128             if (Jelo.Valid.isArray(el)) {
129                 Jelo.each(el, function() {
130                     Jelo.Event.add(this, ev, fn);
131                 });
132                 return;
133             }
134             var handler = function(e) {
135                 e = e || window.event;
136                 var event = {};
137                 var properties = ['type', 'shiftKey', 'ctrlKey', 'altKey', 'keyCode', 'charCode', 'button', 'which',
138                     'clientX', 'clientY', 'mouseX', 'mouseY', 'metaKey', 'pageX', 'pageY', 'screenX', 'screenY',
139                     'relatedTarget'];
140                 var len = properties.length;
141                 for (var i = 0; i < len; i++) {
142                     event[properties[i]] = e[properties[i]];
143                 }
144                 target = e.target
145                     ? e.target
146                     : e.srcElement;
147                 if (target.nodeType === 3) {
148                     target = target.parentNode;
149                 }
150                 event.preventDefault = function() {
151                     if (e.preventDefault) {
152                         e.preventDefault();
153                     } else {
154                         e.returnValue = false;
155                     }
156                 };
157                 event.stopPropagation = function() {
158                     if (e.stopPropagation) {
159                         e.stopPropagation();
160                     } else {
161                         e.cancelBubble = true;
162                     }
163                 };
164                 fn.call(target, target, event);
165             };
166             if (el.addEventListener) {
167                 el.addEventListener(ev, handler, false);
168             } else if (el.attachEvent) {
169                 el.attachEvent("on" + ev, handler);
170             } else {
171                 throw ("Jelo.Event.add: Could not observe " + ev + " on " + el);
172             }
173             var newEvent = {
174                 target  : el,
175                 event   : ev,
176                 handler : handler,
177                 fn      : fn
178             };
179             handlers.push(newEvent);
180         },
181         /**
182          * Stop listening for an event. Safe to call even if no such listener has been registered. Can be accessed via
183          * {@link Jelo.un}.
184          * 
185          * @param {HTMLElement} element HTML element to which Jelo should listen.
186          * @param {String} eventName The type of event for which Jelo should listen, such as "click" or "mouseup".
187          *        Should be all lowercase, and WITHOUT the IE prefix "on".
188          * @param {Function} handler Method to be invoked when the event occurs. Anonymous handlers cannot be
189          *        unregistered at this time.
190          */
191         remove    : function(el, ev, fn) {
192             if (Jelo.Valid.isArray(el)) {
193                 Jelo.each(el, function() {
194                     Jelo.Event.remove(this, ev, fn);
195                 });
196                 return;
197             }
198             if (!!el && (typeof ev == 'string') && (typeof fn == 'function')) {
199                 for (var i = 0; i < handlers.length; i++) {
200                     var jeh = handlers[i];
201                     var ml = el === jeh.target;
202                     var mv = ev === jeh.event;
203                     var mf = fn === jeh.fn;
204                     if (ml && mv && mf) {
205                         if (el.removeEventListener) {
206                             el.removeEventListener(ev, jeh.handler, false);
207                         } else if (el.detachEvent) {
208                             el.detachEvent("on" + ev, jeh.handler);
209                         } else {
210                             throw ("Jelo.Event.remove: Could not remove " + ev + " from " + el);
211                         }
212                         handlers.splice(i, 1);
213                         break;
214                     }
215                 }
216             } else {
217                 throw ("Syntax Error. Jelo.Event.remove(DOMElement, Event:String, Function");
218             }
219         },
220         
221         /**
222          * @deprecated Bootstrap the next version of Jelo.Event.add and Jelo.Event.remove. The only difference you
223          *             should notice as a developer is that the arguments passed to the OLD event handler were (target,
224          *             event). The NEW arguments are (event) to fit in line with most implementations out there. See
225          *             also: {@link Jelo.Event.isFixed}.
226          */
227         normalize : function() {
228             Jelo.Event.add = (D.addEventListener)
229                 ? function(el, ev, fn) {
230                     if (Jelo.Valid.isArray(el)) {
231                         Jelo.each(el, function() {
232                             Jelo.Event.add(this, ev, fn);
233                         });
234                         return;
235                     }
236                     el.addEventListener(ev, fn, false);
237                 }
238                 : function(el, ev, fn) {
239                     if (Jelo.Valid.isArray(el)) {
240                         Jelo.each(el, function() {
241                             Jelo.Event.add(this, ev, fn);
242                         });
243                         return;
244                     }
245                     if (find(el, ev, fn) != -1) {
246                         return;
247                     }
248                     var wh = function(e) {
249                         e = e || window.event;
250                         var event = {
251                             _event          : e,
252                             type            : e.type,
253                             target          : e.srcElement,
254                             currentTarget   : el,
255                             relatedTarget   : e.fromElement
256                                 ? e.fromElement
257                                 : e.toElement,
258                             eventPhase      : (e.srcElement == el)
259                                 ? 2
260                                 : 3,
261                             clientX         : e.clientX,
262                             clientY         : e.clientY,
263                             screenX         : e.screenX,
264                             screenY         : e.screenY,
265                             altKey          : e.altKey,
266                             ctrlKey         : e.ctrlKey,
267                             shiftKey        : e.shiftKey,
268                             charCode        : e.charCode || e.keyCode,
269                             keyCode         : e.keyCode || e.charCode,
270                             button          : e.button
271                                 ? {
272                                     1 : 0,
273                                     4 : 1,
274                                     2 : 2
275                                 }[e.button]
276                                 : (e.which
277                                     ? e.which - 1
278                                     : -1),
279                             which           : e.which || e.button,
280                             stopPropagation : function() {
281                                 this._event.cancelBubble = true;
282                             },
283                             preventDefault  : function() {
284                                 this._event.returnValue = false;
285                             }
286                         };
287                         fn.call(el, event);
288                     };
289                     el.attachEvent('on' + ev, wh);
290                     var h = {
291                         element        : el,
292                         eventType      : ev,
293                         handler        : fn,
294                         wrappedHandler : wh
295                     };
296                     var d = el.document || el;
297                     var w = d.parentWindow;
298                     var id = 'h' + Jelo.uID();
299                     if (!w._allHandlers) {
300                         w._allHandlers = {};
301                     }
302                     w._allHandlers[id] = h;
303                     if (!el._handlers) {
304                         el._handlers = [];
305                     }
306                     el._handlers.push(id);
307                     if (!w._onunloadRegistered) {
308                         w.attachEvent('onunload', removeAllHandlers);
309                         w._onunloadRegistered = true;
310                     }
311                 };
312             Jelo.Event.remove = (D.removeEventListener)
313                 ? function(el, ev, fn) {
314                     if (Jelo.Valid.isArray(el)) {
315                         Jelo.each(el, function() {
316                             Jelo.Event.remove(this, ev, fn);
317                         });
318                         return;
319                     }
320                     el.removeEventListener(ev, fn, false);
321                 }
322                 : function(el, ev, fn) {
323                     if (Jelo.Valid.isArray(el)) {
324                         Jelo.each(el, function() {
325                             Jelo.Event.remove(this, ev, fn);
326                         });
327                         return;
328                     }
329                     var i = find(el, ev, fn);
330                     if (i == -1) {
331                         return;
332                     }
333                     var d = el.document || el;
334                     var w = d.parentWindow;
335                     var hid = el._handlers[i];
336                     var h = w._allHandlers[hid];
337                     el.detachEvent('on' + ev, h.wrappedHandler);
338                     el._handlers.splice(i, 1);
339                     delete w._allHandlers[hid];
340                 };
341             Jelo.on = Jelo.Event.add;
342             Jelo.un = Jelo.Event.remove;
343             Jelo.Event.isFixed = function() {
344                 return true;
345             }
346         },
347         /**
348          * @deprecated True when {@link Jelo.Event.normalize} has been called.
349          */
350         isFixed   : function() {
351             return false;
352         }
353         
354     };
355 }();
356 
357 Jelo.Event.fix = Jelo.Event.normalize;
358 
359 /**
360  * Alias for {@link Jelo.Event.add}.
361  * 
362  * @function
363  * @memberOf Jelo
364  */
365 Jelo.on = Jelo.Event.add;
366 
367 /**
368  * Alias for {@link Jelo.Event.remove}.
369  * 
370  * @function
371  * @memberOf Jelo
372  */
373 Jelo.un = Jelo.Event.remove;
374 
375 /**
376  * Alias for {@link Jelo.Event.remove}.
377  * 
378  * @function
379  * @memberOf Jelo.Event
380  */
381 Jelo.Event.rem = Jelo.Event.remove;
382 
383 /**
384  * Alias for {@link Jelo.Event.onReady}.
385  * 
386  * @function
387  * @memberOf Jelo
388  */
389 Jelo.onReady = Jelo.Event.onReady;
390