1 /**
  2  * @fileOverview Event handling functionality
  3  * @author <a href="http://www.bobs-bits.com">Stephen Reay</a>
  4  */
  5 
  6 /**
  7  * @namespace BTM Event handling functions
  8  */
  9 BTM.Event = {
 10 	/**
 11 	 * Flag to identify if the DOM has loaded.
 12 	 * @type Boolean
 13 	 */
 14 	DOMLoaded : false,
 15 
 16 	/**
 17 	 * Reference to all event handlers registered through {@link BTM.Event.observe}
 18 	 * @type {Object[]}
 19 	 */
 20 	eventHandlers : [],
 21 
 22 	/**
 23 	 * Hash of Event Key Codes, use with {@link BTM.Event.getKeyCode}
 24 	 * @type {Object}
 25 	 */
 26 	keyCodes : {
 27 		3 : 'KEY_CANCEL',
 28 		6 : 'KEY_HELP',
 29 		8 : 'KEY_BACK_SPACE',
 30 		9 : 'KEY_TAB',
 31 		12 : 'KEY_CLEAR',
 32 		13 : 'KEY_RETURN',
 33 		14 : 'KEY_ENTER',
 34 		16 : 'KEY_SHIFT',
 35 		17 : 'KEY_CONTROL',
 36 		18 : 'KEY_ALT',
 37 		19 : 'KEY_PAUSE',
 38 		20 : 'KEY_CAPS_LOCK',
 39 		27 : 'KEY_ESCAPE',
 40 		32 : 'KEY_SPACE',
 41 		33 : 'KEY_PAGE_UP',
 42 		34 : 'KEY_PAGE_DOWN',
 43 		35 : 'KEY_END',
 44 		36 : 'KEY_HOME',
 45 		37 : 'KEY_LEFT',
 46 		38 : 'KEY_UP',
 47 		39 : 'KEY_RIGHT',
 48 		40 : 'KEY_DOWN',
 49 		44 : 'KEY_PRINTSCREEN',
 50 		45 : 'KEY_INSERT',
 51 		46 : 'KEY_DELETE',
 52 		48 : 'KEY_0',
 53 		49 : 'KEY_1',
 54 		50 : 'KEY_2',
 55 		51 : 'KEY_3',
 56 		52 : 'KEY_4',
 57 		53 : 'KEY_5',
 58 		54 : 'KEY_6',
 59 		55 : 'KEY_7',
 60 		56 : 'KEY_8',
 61 		57 : 'KEY_9',
 62 		59 : 'KEY_SEMICOLON',
 63 		61 : 'KEY_EQUALS',
 64 		65 : 'KEY_A',
 65 		66 : 'KEY_B',
 66 		67 : 'KEY_C',
 67 		68 : 'KEY_D',
 68 		69 : 'KEY_E',
 69 		70 : 'KEY_F',
 70 		71 : 'KEY_G',
 71 		72 : 'KEY_H',
 72 		73 : 'KEY_I',
 73 		74 : 'KEY_J',
 74 		75 : 'KEY_K',
 75 		76 : 'KEY_L',
 76 		77 : 'KEY_M',
 77 		78 : 'KEY_N',
 78 		79 : 'KEY_O',
 79 		80 : 'KEY_P',
 80 		81 : 'KEY_Q',
 81 		82 : 'KEY_R',
 82 		83 : 'KEY_S',
 83 		84 : 'KEY_T',
 84 		85 : 'KEY_U',
 85 		86 : 'KEY_V',
 86 		87 : 'KEY_W',
 87 		88 : 'KEY_X',
 88 		89 : 'KEY_Y',
 89 		90 : 'KEY_Z',
 90 		93 : 'KEY_CONTEXT_MENU',
 91 		96 : 'KEY_NUMPAD0',
 92 		97 : 'KEY_NUMPAD1',
 93 		98 : 'KEY_NUMPAD2',
 94 		99 : 'KEY_NUMPAD3',
 95 		100 : 'KEY_NUMPAD4',
 96 		101 : 'KEY_NUMPAD5',
 97 		102 : 'KEY_NUMPAD6',
 98 		103 : 'KEY_NUMPAD7',
 99 		104 : 'KEY_NUMPAD8',
100 		105 : 'KEY_NUMPAD9',
101 		106 : 'KEY_MULTIPLY',
102 		107 : 'KEY_ADD',
103 		108 : 'KEY_SEPARATOR',
104 		109 : 'KEY_SUBTRACT',
105 		110 : 'KEY_DECIMAL',
106 		111 : 'KEY_DIVIDE',
107 		112 : 'KEY_F1',
108 		113 : 'KEY_F2',
109 		114 : 'KEY_F3',
110 		115 : 'KEY_F4',
111 		116 : 'KEY_F5',
112 		117 : 'KEY_F6',
113 		118 : 'KEY_F7',
114 		119 : 'KEY_F8',
115 		120 : 'KEY_F9',
116 		121 : 'KEY_F10',
117 		122 : 'KEY_F11',
118 		123 : 'KEY_F12',
119 		124 : 'KEY_F13',
120 		125 : 'KEY_F14',
121 		126 : 'KEY_F15',
122 		127 : 'KEY_F16',
123 		128 : 'KEY_F17',
124 		129 : 'KEY_F18',
125 		130 : 'KEY_F19',
126 		131 : 'KEY_F20',
127 		132 : 'KEY_F21',
128 		133 : 'KEY_F22',
129 		134 : 'KEY_F23',
130 		135 : 'KEY_F24',
131 		144 : 'KEY_NUM_LOCK',
132 		145 : 'KEY_SCROLL_LOCK',
133 		188 : 'KEY_COMMA',
134 		190 : 'KEY_PERIOD',
135 		191 : 'KEY_SLASH',
136 		192 : 'KEY_BACK_QUOTE',
137 		219 : 'KEY_OPEN_BRACKET',
138 		220 : 'KEY_BACK_SLASH',
139 		221 : 'KEY_CLOSE_BRACKET',
140 		222 : 'KEY_QUOTE',
141 		224 : 'KEY_META'
142 	}
143 };
144 
145 /**
146  * Add an event listener to an element
147  * @param {HTMLElement|String} element element ID or element reference to add event listener to. This can be any element accessibly via JavaScript, including document, window, etc.
148  * @param {String} eventType the event to listen to, e.g.: 'click', 'mouseover', etc. Also supports the custom event 'DOMContentLoaded' on the document object.
149  * @param {Function} callback the Function to be called when the event fires
150  * @param {Number} [priority=5] the priority of the event listener, lower numbers run first
151  * @returns {Number} the index in the Array that holds this callback. Used with {@link BTM.Event.stopObserving}
152  * @example
153  * var t = new MyClass();
154  * BTM.observe('mydiv', 'click', t.myMethod.bindAsEventListener(t, 'hello', 'world'));
155  * BTM.observe(document,'DOMContentLoaded', someRandomFunc, 8); // runs someRandomFunc() once the DOM has loaded, as a lower priority.
156  * @see {BTM.Event.stopObserving}
157  * @see {BTM.Event.fire}
158  * @see {Function#bind}
159  * @see {Function#bindAsEventListener}
160  */	
161 BTM.Event.observe = function observe(element, eventType, callback, priority) {
162 	element = Object.isString(element) ? BTM.$(element) : element;
163 	
164 	if (!Object.isFunction(callback)) {
165 		throw new TypeError();
166 	}
167 	
168 	priority = priority || 5;
169 	
170 	var callbackName = callback.name || 'anonymous Function';
171 	
172 	
173 	if (Object.isUndefined(element.observerID)) {
174 		element.observerID = BTM.Event.eventHandlers.push({'element':element}) - 1;
175 	}
176 	
177 	BTM.log("Adding '" + eventType + "' handler '" + callbackName + "'", element);
178 
179 	if (!BTM.Event.eventHandlers[element.observerID][eventType]) {
180 		BTM.Event.eventHandlers[element.observerID][eventType] = [];
181 		
182 		var loaderCallback = BTM.Event.runEventFuncs.bindAsEventListener(window, element, eventType);
183 		
184 		//Add native event listener for our loader
185 		if (element.addEventListener) {
186 			element.addEventListener(eventType, loaderCallback, false);
187 		}
188 		else if (element === window && document.addEventListener) {
189 			document.addEventListener(eventType, loaderCallback, false);
190 		}
191 		else if (element.attachEvent) {
192 			element.attachEvent('on'+eventType, loaderCallback);
193 		}
194 		
195 	}
196 	
197 	if (!BTM.Event.eventHandlers[element.observerID][eventType][priority]) {
198 		BTM.Event.eventHandlers[element.observerID][eventType][priority] = [];
199 	}
200 	
201 	return BTM.Event.eventHandlers[element.observerID][eventType][priority].push(callback) -1;
202 };
203 
204 /**
205  * Convenience shortcut to {@link BTM.Event.oserve}
206  */
207 BTM.observe = BTM.Event.observe;
208 
209 /**
210  * Remove an event listener from an element
211  * @param {HTMLElement|String} element element ID or element reference to add event listener to. This can be any element accessible via JavaScript, including document, window, etc.
212  * @param {String} eventType the event to listen to, e.g.: 'click', 'mouseover', etc. Also supports the event 'DOMContentLoaded' on the document object.
213  * @param {Number} callbackIndex the index in the Array that holds the callback to be removed. Generated by {@link BTM.Event.observe}
214  * @param {Number} [priority=5] the priority of the event listener, lower numbers run first
215  * @returns {Function} the callback that was removed
216  * @see {BTM.Event.observe}
217  */
218 BTM.Event.stopObserving = function stopObserving(element, eventType, callbackIndex, priority) {
219 	element = Object.isString(element) ? BTM.$(element) : element;
220 	
221 	priority = Object.isUndefined(priority) ? 5 : priority;
222 	
223 	if (element.observerID && 
224 	BTM.Event.eventHandlers[element.observerID] && 
225 	BTM.Event.eventHandlers[element.observerID][eventType] && 
226 	BTM.Event.eventHandlers[element.observerID][eventType][priority] && 
227 	BTM.Event.eventHandlers[element.observerID][eventType][priority][callbackIndex]) {
228 		var callback = BTM.Event.eventHandlers[element.observerID][eventType][priority][callbackIndex];
229 		var callbackName = callback.name || 'anonymous Function';
230 		
231 		BTM.log("Removing '" + eventType + "' handler '" + callbackName + "'", element);
232 		
233 		delete BTM.Event.eventHandlers[element.observerID][eventType][priority][callbackIndex];
234 		
235 		return callback;
236 	}
237 };
238 
239 /**
240  * Convenience shortcut to {@link BTM.Event.stopObserving}
241  */
242 BTM.stopObserving = BTM.Event.stopObserving;
243 
244 /**
245  * Create a fake Event object
246  * @param {HTMLElement|String} element element ID or element reference to generate Event object for. This can be any element accessible via JavaScript, including document, window, etc
247  * @param {String} eventType the event to fire, e.g.: 'click', 'mouseover', 'MyApp:CustomEvent', etc. Also supports the event 'DOMContentLoaded' on the document object
248  * @param {Object} [extras] key:value pairs of extra data to be included in the custom Event object generated when the Event fires
249  * @returns {Object} the generated Event object
250  */
251 BTM.Event.fakeEvent = function fakeEvent(element, type, extras) {
252 	var eventObject = Object.update({
253 		'createdBy' : 'BTM Event Handling',
254 		'target' : element,
255 		'type' : type
256 	}, extras || {});
257 	
258 	return eventObject;
259 };
260 
261 
262 /**
263  * Run the event listeners assigned to a specific element/event combination.
264  * @param {Event} event the Event object
265  * @param {HTMLElement} element the DOM Element the Event was fired on
266  * @param {String} eventType the Event type, e.g.: 'click', 'hover', etc
267  * @see BTM.Event.observe
268  * @see BTM.Event.fire
269  */
270 BTM.Event.runEventFuncs = function runEventFuncs(event, element, eventType) {
271 	var data = BTM.Event.eventHandlers[element.observerID][eventType];
272 	event = event || false;
273 			
274 	BTM.log("Running '" + eventType + "' event listeners", element);
275 	data.forEach(function (callbacks, level) {
276 		BTM.log("Running " + callbacks.length + " callbacks at level " + level);
277 		callbacks.forEach(function (callback) {
278 			callback(event);
279 		});
280 	});
281 };
282 
283 /**
284  * Fire an event on an element. This will apply to native and custom events registered via BTM.Event.observe
285  * @param {HTMLElement|String} element element ID or element reference to fire Event on. This can be any element accessible via JavaScript, including document, window, etc
286  * @param {String} eventType the event to fire, e.g.: 'click', 'mouseover', 'MyApp:CustomEvent', etc. Also supports the event 'DOMContentLoaded' on the document object
287  * @param {Object} [extras] key:value pairs of extra data to be included in the custom Event object generated when the Event fires
288  */
289 BTM.Event.fire = function fire(element, eventType, extras) {
290 	element = BTM.$(element);
291 	BTM.log("Firing '" + eventType + "' event", element);
292 	if (!Object.isUndefined(element.observerID)) {
293 		var evt = BTM.Event.fakeEvent(element, eventType, extras);
294 		BTM.Event.runEventFuncs(evt, element, eventType);
295 	}
296 };
297 
298 
299 /**
300  * Cancel an event. This stops the event from "bubbling" up the DOM tree, and prevents the default action.
301  * @param {Event} event the event to be cancelled
302  */
303 BTM.Event.cancelEvent = function cancelEvent(event) {
304 	if(event === false) {
305 		return false;
306 	}
307 	else if (Object.isUndefined(event)) {
308 		event = window.event;
309 	}
310 
311 	BTM.log("Attempting to cancel event", event);
312 
313 	event.cancelBubble = true;
314 	event.returnValue = false;
315 
316 	if (event.stopPropagation) {
317 		event.stopPropagation();
318 	}
319 
320 	if (event.preventDefault) {
321 		event.preventDefault();
322 	}
323 };
324 
325 /**
326  * Determine the original target element an Event fired on
327  * @param {Event} event the event to determine the original target of
328  * @returns {HTMLElement} the element the Event originally fired on
329  */
330 BTM.Event.getTarget = function getTarget(event) {
331 	var target;
332 	
333 	BTM.log("Finding target Element for event", event);
334 	
335 	if (event.target) {
336 		target = event.target;
337 	}
338 	else if (event.srcElement) {
339 		target = event.srcElement;
340 	}
341 	
342 	if (target.nodeType === 3) { // defeat Safari bug
343 		target = target.parentNode;
344 	}
345 	
346 	return target;
347 };
348 
349 /**
350  * Determine the Key that fired an event
351  * @param {Event} event the event to determine the original Key of
352  * @returns {String} the Key that was pressed
353  */
354 BTM.Event.getKeyCode = function getKeyCode(event) {
355 	var code;
356 	if (event.keyCode) {
357 		code = event.keyCode;
358 	}
359 	else if (event.which) {
360 		code = event.which;
361 	}
362 	
363 	return code;
364 };
365 
366 (function mimicDOMContentLoaded() {
367 	function runLoadEvents(event) {
368 		if (BTM.Event.DOMLoaded) {
369 			return false;
370 		}
371 		
372 		if (typeof ieOnLoadScript !== 'undefined') {
373 			ieOnLoadScript.parentNode.removeChild(ieOnLoadScript);
374 		}
375 
376 		BTM.Event.DOMLoaded = true;
377 		BTM.Event.runEventFuncs(false, document, 'DOMContentLoaded');
378 	}
379 	
380 	// Add DOMContentLoaded Event listener entry
381 	BTM.Event.eventHandlers[0] = {
382 			element : document,
383 			DOMContentLoaded: []
384 		};
385 	document.observerID = 0;
386 
387 	//Simulated DOMContentLoaded event for unsupporting browsers
388 	// Dean Edwards/Matthias Miller/John Resig
389 	var timer = false;
390 	
391 	/*@cc_on @*/
392 	/*@if (@_win32)
393 	document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
394 	var ieOnLoadScript = document.getElementById("__ie_onload");
395 	ieOnLoadScript.onreadystatechange = function() {
396 	  if (ieOnLoadScript.readyState === "complete") {
397 		runLoadEvents(); // call the onload handler
398 	  }
399 	};
400 	 @else @*/
401 	/* for Mozilla/Opera9 */
402 	if (document.addEventListener) {
403 		document.addEventListener("DOMContentLoaded", runLoadEvents, false);
404 	}
405 	/* for WebKit */
406 	if (/WebKit/i.test(navigator.userAgent)) {
407 		timer = setInterval(function() {
408 				if (/loaded|complete/.test(document.readyState)) {
409 				clearInterval(timer);
410 				runLoadEvents(); // call the onload handler
411 			}
412 		}, 10);
413 	}
414 	/* for other browsers */
415 	window.onload = runLoadEvents;
416 	/*@end @*/
417 
418 	
419 })();
420