4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
11 /*jshint loopfunc:true*/
13 define("tinymce/dom/EventUtils", [], function() {
16 var eventExpandoPrefix = "mce-data-";
17 var mouseEventRe = /^(?:mouse|contextmenu)|click/;
18 var deprecated = {keyLocation: 1, layerX: 1, layerY: 1};
21 * Binds a native event to a callback on the speified target.
23 function addEvent(target, name, callback, capture) {
24 if (target.addEventListener) {
25 target.addEventListener(name, callback, capture || false);
26 } else if (target.attachEvent) {
27 target.attachEvent('on' + name, callback);
32 * Unbinds a native event callback on the specified target.
34 function removeEvent(target, name, callback, capture) {
35 if (target.removeEventListener) {
36 target.removeEventListener(name, callback, capture || false);
37 } else if (target.detachEvent) {
38 target.detachEvent('on' + name, callback);
43 * Normalizes a native event object or just adds the event specific methods on a custom event.
45 function fix(originalEvent, data) {
46 var name, event = data || {}, undef;
48 // Dummy function that gets replaced on the delegation state functions
49 function returnFalse() {
53 // Dummy function that gets replaced on the delegation state functions
54 function returnTrue() {
58 // Copy all properties from the original event
59 for (name in originalEvent) {
60 // layerX/layerY is deprecated in Chrome and produces a warning
61 if (!deprecated[name]) {
62 event[name] = originalEvent[name];
66 // Normalize target IE uses srcElement
68 event.target = event.srcElement || document;
71 // Calculate pageX/Y if missing and clientX/Y available
72 if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
73 var eventDoc = event.target.ownerDocument || document;
74 var doc = eventDoc.documentElement;
75 var body = eventDoc.body;
77 event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
78 ( doc && doc.clientLeft || body && body.clientLeft || 0);
80 event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0 ) -
81 ( doc && doc.clientTop || body && body.clientTop || 0);
84 // Add preventDefault method
85 event.preventDefault = function() {
86 event.isDefaultPrevented = returnTrue;
88 // Execute preventDefault on the original event object
90 if (originalEvent.preventDefault) {
91 originalEvent.preventDefault();
93 originalEvent.returnValue = false; // IE
98 // Add stopPropagation
99 event.stopPropagation = function() {
100 event.isPropagationStopped = returnTrue;
102 // Execute stopPropagation on the original event object
104 if (originalEvent.stopPropagation) {
105 originalEvent.stopPropagation();
107 originalEvent.cancelBubble = true; // IE
112 // Add stopImmediatePropagation
113 event.stopImmediatePropagation = function() {
114 event.isImmediatePropagationStopped = returnTrue;
115 event.stopPropagation();
118 // Add event delegation states
119 if (!event.isDefaultPrevented) {
120 event.isDefaultPrevented = returnFalse;
121 event.isPropagationStopped = returnFalse;
122 event.isImmediatePropagationStopped = returnFalse;
129 * Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
130 * It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
132 function bindOnReady(win, callback, eventUtils) {
133 var doc = win.document, event = {type: 'ready'};
135 if (eventUtils.domLoaded) {
140 // Gets called when the DOM is ready
141 function readyHandler() {
142 if (!eventUtils.domLoaded) {
143 eventUtils.domLoaded = true;
148 function waitForDomLoaded() {
149 if (doc.readyState === "complete") {
150 removeEvent(doc, "readystatechange", waitForDomLoaded);
155 function tryScroll() {
157 // If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
158 // http://javascript.nwbox.com/IEContentLoaded/
159 doc.documentElement.doScroll("left");
161 setTimeout(tryScroll, 0);
169 if (doc.addEventListener) {
170 addEvent(win, 'DOMContentLoaded', readyHandler);
173 addEvent(doc, "readystatechange", waitForDomLoaded);
175 // Wait until we can scroll, when we can the DOM is initialized
176 if (doc.documentElement.doScroll && win === win.top) {
181 // Fallback if any of the above methods should fail for some odd reason
182 addEvent(win, 'load', readyHandler);
186 * This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
188 function EventUtils() {
189 var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
191 expando = eventExpandoPrefix + (+new Date()).toString(32);
192 hasMouseEnterLeave = "onmouseenter" in document.documentElement;
193 hasFocusIn = "onfocusin" in document.documentElement;
194 mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'};
197 // State if the DOMContentLoaded was executed or not
198 self.domLoaded = false;
199 self.events = events;
202 * Executes all event handler callbacks for a specific event.
205 * @param {Event} evt Event object.
206 * @param {String} id Expando id value to look for.
208 function executeHandlers(evt, id) {
209 var callbackList, i, l, callback;
211 callbackList = events[id][evt.type];
213 for (i = 0, l = callbackList.length; i < l; i++) {
214 callback = callbackList[i];
216 // Check if callback exists might be removed if a unbind is called inside the callback
217 if (callback && callback.func.call(callback.scope, evt) === false) {
218 evt.preventDefault();
221 // Should we stop propagation to immediate listeners
222 if (evt.isImmediatePropagationStopped()) {
230 * Binds a callback to an event on the specified target.
233 * @param {Object} target Target node/window or custom object.
234 * @param {String} names Name of the event to bind.
235 * @param {function} callback Callback function to execute when the event occurs.
236 * @param {Object} scope Scope to call the callback function on, defaults to target.
237 * @return {function} Callback function that got bound.
239 self.bind = function(target, names, callback, scope) {
240 var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
242 // Native event handler function patches the event and executes the callbacks for the expando
243 function defaultNativeHandler(evt) {
244 executeHandlers(fix(evt || win.event), id);
247 // Don't bind to text nodes or comments
248 if (!target || target.nodeType === 3 || target.nodeType === 8) {
252 // Create or get events id for the target
253 if (!target[expando]) {
255 target[expando] = id;
258 id = target[expando];
261 // Setup the specified scope or use the target as a default
262 scope = scope || target;
264 // Split names and bind each event, enables you to bind multiple events with one call
265 names = names.split(' ');
269 nativeHandler = defaultNativeHandler;
270 fakeName = capture = false;
272 // Use ready instead of DOMContentLoaded
273 if (name === "DOMContentLoaded") {
277 // DOM is already ready
278 if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
279 callback.call(scope, fix({type: name}));
283 // Handle mouseenter/mouseleaver
284 if (!hasMouseEnterLeave) {
285 fakeName = mouseEnterLeave[name];
288 nativeHandler = function(evt) {
289 var current, related;
291 current = evt.currentTarget;
292 related = evt.relatedTarget;
294 // Check if related is inside the current target if it's not then the event should
295 // be ignored since it's a mouseover/mouseout inside the element
296 if (related && current.contains) {
297 // Use contains for performance
298 related = current.contains(related);
300 while (related && related !== current) {
301 related = related.parentNode;
307 evt = fix(evt || win.event);
308 evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
309 evt.target = current;
310 executeHandlers(evt, id);
316 // Fake bubbeling of focusin/focusout
317 if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
319 fakeName = name === "focusin" ? "focus" : "blur";
320 nativeHandler = function(evt) {
321 evt = fix(evt || win.event);
322 evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
323 executeHandlers(evt, id);
327 // Setup callback list and bind native event
328 callbackList = events[id][name];
330 events[id][name] = callbackList = [{func: callback, scope: scope}];
331 callbackList.fakeName = fakeName;
332 callbackList.capture = capture;
334 // Add the nativeHandler to the callback list so that we can later unbind it
335 callbackList.nativeHandler = nativeHandler;
337 // Check if the target has native events support
339 if (name === "ready") {
340 bindOnReady(target, nativeHandler, self);
342 addEvent(target, fakeName || name, nativeHandler, capture);
345 if (name === "ready" && self.domLoaded) {
346 callback({type: name});
348 // If it already has an native handler then just push the callback
349 callbackList.push({func: callback, scope: scope});
354 target = callbackList = 0; // Clean memory for IE
360 * Unbinds the specified event by name, name and callback or all events on the target.
363 * @param {Object} target Target node/window or custom object.
364 * @param {String} names Optional event name to unbind.
365 * @param {function} callback Optional callback function to unbind.
366 * @return {EventUtils} Event utils instance.
368 self.unbind = function(target, names, callback) {
369 var id, callbackList, i, ci, name, eventMap;
371 // Don't bind to text nodes or comments
372 if (!target || target.nodeType === 3 || target.nodeType === 8) {
376 // Unbind event or events if the target has the expando
377 id = target[expando];
379 eventMap = events[id];
383 names = names.split(' ');
387 callbackList = eventMap[name];
389 // Unbind the event if it exists in the map
391 // Remove specified callback
393 ci = callbackList.length;
395 if (callbackList[ci].func === callback) {
396 callbackList.splice(ci, 1);
401 // Remove all callbacks if there isn't a specified callback or there is no callbacks left
402 if (!callback || callbackList.length === 0) {
403 delete eventMap[name];
404 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
409 // All events for a specific element
410 for (name in eventMap) {
411 callbackList = eventMap[name];
412 removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
418 // Check if object is empty, if it isn't then we won't remove the expando map
419 for (name in eventMap) {
423 // Delete event object
426 // Remove expando from target
428 // IE will fail here since it can't delete properties from window
429 delete target[expando];
431 // IE will set it to null
432 target[expando] = null;
440 * Fires the specified event on the specified target.
443 * @param {Object} target Target node/window or custom object.
444 * @param {String} name Event name to fire.
445 * @param {Object} args Optional arguments to send to the observers.
446 * @return {EventUtils} Event utils instance.
448 self.fire = function(target, name, args) {
451 // Don't bind to text nodes or comments
452 if (!target || target.nodeType === 3 || target.nodeType === 8) {
456 // Build event object by patching the args
457 args = fix(null, args);
459 args.target = target;
462 // Found an expando that means there is listeners to execute
463 id = target[expando];
465 executeHandlers(args, id);
469 target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
470 } while (target && !args.isPropagationStopped());
476 * Removes all bound event listeners for the specified target. This will also remove any bound
477 * listeners to child nodes within that target.
480 * @param {Object} target Target node/window object.
481 * @return {EventUtils} Event utils instance.
483 self.clean = function(target) {
484 var i, children, unbind = self.unbind;
486 // Don't bind to text nodes or comments
487 if (!target || target.nodeType === 3 || target.nodeType === 8) {
491 // Unbind any element on the specificed target
492 if (target[expando]) {
496 // Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
497 if (!target.getElementsByTagName) {
498 target = target.document;
501 // Remove events from each child element
502 if (target && target.getElementsByTagName) {
505 children = target.getElementsByTagName('*');
508 target = children[i];
510 if (target[expando]) {
520 * Destroys the event object. Call this on IE to remove memory leaks.
522 self.destroy = function() {
526 // Legacy function for canceling events
527 self.cancel = function(e) {
530 e.stopImmediatePropagation();
537 EventUtils.Event = new EventUtils();
538 EventUtils.Event.bind(window, 'ready', function() {});