]>
Commit | Line | Data |
---|---|---|
1354d172 AD |
1 | define("dojo/on", ["./has!dom-addeventlistener?:./aspect", "./_base/kernel", "./has"], function(aspect, dojo, has){ |
2 | // summary: | |
3 | // The export of this module is a function that provides core event listening functionality. With this function | |
4 | // you can provide a target, event type, and listener to be notified of | |
5 | // future matching events that are fired. | |
6 | // target: Element|Object | |
7 | // This is the target object or DOM element that to receive events from | |
8 | // type: String|Function | |
9 | // This is the name of the event to listen for or an extension event type. | |
10 | // listener: Function | |
11 | // This is the function that should be called when the event fires. | |
12 | // returns: Object | |
13 | // An object with a remove() method that can be used to stop listening for this | |
14 | // event. | |
15 | // description: | |
16 | // To listen for "click" events on a button node, we can do: | |
17 | // | define(["dojo/on"], function(listen){ | |
18 | // | on(button, "click", clickHandler); | |
19 | // | ... | |
20 | // Evented JavaScript objects can also have their own events. | |
21 | // | var obj = new Evented; | |
22 | // | on(obj, "foo", fooHandler); | |
23 | // And then we could publish a "foo" event: | |
24 | // | on.emit(obj, "foo", {key: "value"}); | |
25 | // We can use extension events as well. For example, you could listen for a tap gesture: | |
26 | // | define(["dojo/on", "dojo/gesture/tap", function(listen, tap){ | |
27 | // | on(button, tap, tapHandler); | |
28 | // | ... | |
29 | // which would trigger fooHandler. Note that for a simple object this is equivalent to calling: | |
30 | // | obj.onfoo({key:"value"}); | |
31 | // If you use on.emit on a DOM node, it will use native event dispatching when possible. | |
32 | ||
33 | "use strict"; | |
34 | if(1){ // check to make sure we are in a browser, this module should work anywhere | |
35 | var major = window.ScriptEngineMajorVersion; | |
36 | has.add("jscript", major && (major() + ScriptEngineMinorVersion() / 10)); | |
37 | has.add("event-orientationchange", has("touch") && !has("android")); // TODO: how do we detect this? | |
38 | } | |
39 | var on = function(target, type, listener, dontFix){ | |
40 | if(target.on){ | |
41 | // delegate to the target's on() method, so it can handle it's own listening if it wants | |
42 | return target.on(type, listener); | |
43 | } | |
44 | // delegate to main listener code | |
45 | return on.parse(target, type, listener, addListener, dontFix, this); | |
46 | }; | |
47 | on.pausable = function(target, type, listener, dontFix){ | |
48 | // summary: | |
49 | // This function acts the same as on(), but with pausable functionality. The | |
50 | // returned signal object has pause() and resume() functions. Calling the | |
51 | // pause() method will cause the listener to not be called for future events. Calling the | |
52 | // resume() method will cause the listener to again be called for future events. | |
53 | var paused; | |
54 | var signal = on(target, type, function(){ | |
55 | if(!paused){ | |
56 | return listener.apply(this, arguments); | |
57 | } | |
58 | }, dontFix); | |
59 | signal.pause = function(){ | |
60 | paused = true; | |
61 | }; | |
62 | signal.resume = function(){ | |
63 | paused = false; | |
64 | }; | |
65 | return signal; | |
66 | }; | |
67 | on.once = function(target, type, listener, dontFix){ | |
68 | // summary: | |
69 | // This function acts the same as on(), but will only call the listener once. The | |
70 | // listener will be called for the first | |
71 | // event that takes place and then listener will automatically be removed. | |
72 | var signal = on(target, type, function(){ | |
73 | // remove this listener | |
74 | signal.remove(); | |
75 | // proceed to call the listener | |
76 | return listener.apply(this, arguments); | |
77 | }); | |
78 | return signal; | |
79 | }; | |
80 | on.parse = function(target, type, listener, addListener, dontFix, matchesTarget){ | |
81 | if(type.call){ | |
82 | // event handler function | |
83 | // on(node, dojo.touch.press, touchListener); | |
84 | return type.call(matchesTarget, target, listener); | |
85 | } | |
86 | ||
87 | if(type.indexOf(",") > -1){ | |
88 | // we allow comma delimited event names, so you can register for multiple events at once | |
89 | var events = type.split(/\s*,\s*/); | |
90 | var handles = []; | |
91 | var i = 0; | |
92 | var eventName; | |
93 | while(eventName = events[i++]){ | |
94 | handles.push(addListener(target, eventName, listener, dontFix, matchesTarget)); | |
95 | } | |
96 | handles.remove = function(){ | |
97 | for(var i = 0; i < handles.length; i++){ | |
98 | handles[i].remove(); | |
99 | } | |
100 | }; | |
101 | return handles; | |
102 | } | |
103 | return addListener(target, type, listener, dontFix, matchesTarget) | |
104 | }; | |
105 | var touchEvents = /^touch/; | |
106 | function addListener(target, type, listener, dontFix, matchesTarget){ | |
107 | // event delegation: | |
108 | var selector = type.match(/(.*):(.*)/); | |
109 | // if we have a selector:event, the last one is interpreted as an event, and we use event delegation | |
110 | if(selector){ | |
111 | type = selector[2]; | |
112 | selector = selector[1]; | |
113 | // create the extension event for selectors and directly call it | |
114 | return on.selector(selector, type).call(matchesTarget, target, listener); | |
115 | } | |
116 | // test to see if it a touch event right now, so we don't have to do it every time it fires | |
117 | if(has("touch")){ | |
118 | if(touchEvents.test(type)){ | |
119 | // touch event, fix it | |
120 | listener = fixTouchListener(listener); | |
121 | } | |
122 | if(!has("event-orientationchange") && (type == "orientationchange")){ | |
123 | //"orientationchange" not supported <= Android 2.1, | |
124 | //but works through "resize" on window | |
125 | type = "resize"; | |
126 | target = window; | |
127 | listener = fixTouchListener(listener); | |
128 | } | |
129 | } | |
130 | // normal path, the target is |this| | |
131 | if(target.addEventListener){ | |
132 | // the target has addEventListener, which should be used if available (might or might not be a node, non-nodes can implement this method as well) | |
133 | // check for capture conversions | |
134 | var capture = type in captures; | |
135 | target.addEventListener(capture ? captures[type] : type, listener, capture); | |
136 | // create and return the signal | |
137 | return { | |
138 | remove: function(){ | |
139 | target.removeEventListener(type, listener, capture); | |
140 | } | |
141 | }; | |
142 | } | |
143 | type = "on" + type; | |
144 | if(fixAttach && target.attachEvent){ | |
145 | return fixAttach(target, type, listener); | |
146 | } | |
147 | throw new Error("Target must be an event emitter"); | |
148 | } | |
149 | ||
150 | on.selector = function(selector, eventType, children){ | |
151 | // summary: | |
152 | // Creates a new extension event with event delegation. This is based on | |
153 | // the provided event type (can be extension event) that | |
154 | // only calls the listener when the CSS selector matches the target of the event. | |
155 | // selector: | |
156 | // The CSS selector to use for filter events and determine the |this| of the event listener. | |
157 | // eventType: | |
158 | // The event to listen for | |
159 | // children: | |
160 | // Indicates if children elements of the selector should be allowed. This defaults to | |
161 | // true (except in the case of normally non-bubbling events like mouse.enter, in which case it defaults to false). | |
162 | // example: | |
163 | // define(["dojo/on", "dojo/mouse"], function(listen, mouse){ | |
164 | // on(node, on.selector(".my-class", mouse.enter), handlerForMyHover); | |
165 | return function(target, listener){ | |
166 | var matchesTarget = this; | |
167 | var bubble = eventType.bubble; | |
168 | if(bubble){ | |
169 | // the event type doesn't naturally bubble, but has a bubbling form, use that | |
170 | eventType = bubble; | |
171 | }else if(children !== false){ | |
172 | // for normal bubbling events we default to allowing children of the selector | |
173 | children = true; | |
174 | } | |
175 | return on(target, eventType, function(event){ | |
176 | var eventTarget = event.target; | |
177 | // see if we have a valid matchesTarget or default to dojo.query | |
178 | matchesTarget = matchesTarget && matchesTarget.matches ? matchesTarget : dojo.query; | |
179 | // there is a selector, so make sure it matches | |
180 | while(!matchesTarget.matches(eventTarget, selector, target)){ | |
181 | if(eventTarget == target || !children || !(eventTarget = eventTarget.parentNode)){ // intentional assignment | |
182 | return; | |
183 | } | |
184 | } | |
185 | return listener.call(eventTarget, event); | |
186 | }); | |
187 | }; | |
188 | }; | |
189 | ||
190 | function syntheticPreventDefault(){ | |
191 | this.cancelable = false; | |
192 | } | |
193 | function syntheticStopPropagation(){ | |
194 | this.bubbles = false; | |
195 | } | |
196 | var slice = [].slice, | |
197 | syntheticDispatch = on.emit = function(target, type, event){ | |
198 | // summary: | |
199 | // Fires an event on the target object. | |
200 | // target: | |
201 | // The target object to fire the event on. This can be a DOM element or a plain | |
202 | // JS object. If the target is a DOM element, native event emiting mechanisms | |
203 | // are used when possible. | |
204 | // type: | |
205 | // The event type name. You can emulate standard native events like "click" and | |
206 | // "mouseover" or create custom events like "open" or "finish". | |
207 | // event: | |
208 | // An object that provides the properties for the event. See https://developer.mozilla.org/en/DOM/event.initEvent | |
209 | // for some of the properties. These properties are copied to the event object. | |
210 | // Of particular importance are the cancelable and bubbles properties. The | |
211 | // cancelable property indicates whether or not the event has a default action | |
212 | // that can be cancelled. The event is cancelled by calling preventDefault() on | |
213 | // the event object. The bubbles property indicates whether or not the | |
214 | // event will bubble up the DOM tree. If bubbles is true, the event will be called | |
215 | // on the target and then each parent successively until the top of the tree | |
216 | // is reached or stopPropagation() is called. Both bubbles and cancelable | |
217 | // default to false. | |
218 | // returns: | |
219 | // If the event is cancelable and the event is not cancelled, | |
220 | // emit will return true. If the event is cancelable and the event is cancelled, | |
221 | // emit will return false. | |
222 | // details: | |
223 | // Note that this is designed to emit events for listeners registered through | |
224 | // dojo/on. It should actually work with any event listener except those | |
225 | // added through IE's attachEvent (IE8 and below's non-W3C event emiting | |
226 | // doesn't support custom event types). It should work with all events registered | |
227 | // through dojo/on. Also note that the emit method does do any default | |
228 | // action, it only returns a value to indicate if the default action should take | |
229 | // place. For example, emiting a keypress event would not cause a character | |
230 | // to appear in a textbox. | |
231 | // example: | |
232 | // To fire our own click event | |
233 | // | on.emit(dojo.byId("button"), "click", { | |
234 | // | cancelable: true, | |
235 | // | bubbles: true, | |
236 | // | screenX: 33, | |
237 | // | screenY: 44 | |
238 | // | }); | |
239 | // We can also fire our own custom events: | |
240 | // | on.emit(dojo.byId("slider"), "slide", { | |
241 | // | cancelable: true, | |
242 | // | bubbles: true, | |
243 | // | direction: "left-to-right" | |
244 | // | }); | |
245 | var args = slice.call(arguments, 2); | |
246 | var method = "on" + type; | |
247 | if("parentNode" in target){ | |
248 | // node (or node-like), create event controller methods | |
249 | var newEvent = args[0] = {}; | |
250 | for(var i in event){ | |
251 | newEvent[i] = event[i]; | |
252 | } | |
253 | newEvent.preventDefault = syntheticPreventDefault; | |
254 | newEvent.stopPropagation = syntheticStopPropagation; | |
255 | newEvent.target = target; | |
256 | newEvent.type = type; | |
257 | event = newEvent; | |
258 | } | |
259 | do{ | |
260 | // call any node which has a handler (note that ideally we would try/catch to simulate normal event propagation but that causes too much pain for debugging) | |
261 | target[method] && target[method].apply(target, args); | |
262 | // and then continue up the parent node chain if it is still bubbling (if started as bubbles and stopPropagation hasn't been called) | |
263 | }while(event && event.bubbles && (target = target.parentNode)); | |
264 | return event && event.cancelable && event; // if it is still true (was cancelable and was cancelled), return the event to indicate default action should happen | |
265 | }; | |
266 | var captures = {}; | |
267 | if(has("dom-addeventlistener")){ | |
268 | // normalize focusin and focusout | |
269 | captures = { | |
270 | focusin: "focus", | |
271 | focusout: "blur" | |
272 | }; | |
273 | if(has("opera")){ | |
274 | captures.keydown = "keypress"; // this one needs to be transformed because Opera doesn't support repeating keys on keydown (and keypress works because it incorrectly fires on all keydown events) | |
275 | } | |
276 | ||
277 | // emiter that works with native event handling | |
278 | on.emit = function(target, type, event){ | |
279 | if(target.dispatchEvent && document.createEvent){ | |
280 | // use the native event emiting mechanism if it is available on the target object | |
281 | // create a generic event | |
282 | // we could create branch into the different types of event constructors, but | |
283 | // that would be a lot of extra code, with little benefit that I can see, seems | |
284 | // best to use the generic constructor and copy properties over, making it | |
285 | // easy to have events look like the ones created with specific initializers | |
286 | var nativeEvent = document.createEvent("HTMLEvents"); | |
287 | nativeEvent.initEvent(type, !!event.bubbles, !!event.cancelable); | |
288 | // and copy all our properties over | |
289 | for(var i in event){ | |
290 | var value = event[i]; | |
291 | if(!(i in nativeEvent)){ | |
292 | nativeEvent[i] = event[i]; | |
293 | } | |
294 | } | |
295 | return target.dispatchEvent(nativeEvent) && nativeEvent; | |
296 | } | |
297 | return syntheticDispatch.apply(on, arguments); // emit for a non-node | |
298 | }; | |
299 | }else{ | |
300 | // no addEventListener, basically old IE event normalization | |
301 | on._fixEvent = function(evt, sender){ | |
302 | // summary: | |
303 | // normalizes properties on the event object including event | |
304 | // bubbling methods, keystroke normalization, and x/y positions | |
305 | // evt: | |
306 | // native event object | |
307 | // sender: | |
308 | // node to treat as "currentTarget" | |
309 | if(!evt){ | |
310 | var w = sender && (sender.ownerDocument || sender.document || sender).parentWindow || window; | |
311 | evt = w.event; | |
312 | } | |
313 | if(!evt){return(evt);} | |
314 | if(!evt.target){ // check to see if it has been fixed yet | |
315 | evt.target = evt.srcElement; | |
316 | evt.currentTarget = (sender || evt.srcElement); | |
317 | if(evt.type == "mouseover"){ | |
318 | evt.relatedTarget = evt.fromElement; | |
319 | } | |
320 | if(evt.type == "mouseout"){ | |
321 | evt.relatedTarget = evt.toElement; | |
322 | } | |
323 | if(!evt.stopPropagation){ | |
324 | evt.stopPropagation = stopPropagation; | |
325 | evt.preventDefault = preventDefault; | |
326 | } | |
327 | switch(evt.type){ | |
328 | case "keypress": | |
329 | var c = ("charCode" in evt ? evt.charCode : evt.keyCode); | |
330 | if (c==10){ | |
331 | // CTRL-ENTER is CTRL-ASCII(10) on IE, but CTRL-ENTER on Mozilla | |
332 | c=0; | |
333 | evt.keyCode = 13; | |
334 | }else if(c==13||c==27){ | |
335 | c=0; // Mozilla considers ENTER and ESC non-printable | |
336 | }else if(c==3){ | |
337 | c=99; // Mozilla maps CTRL-BREAK to CTRL-c | |
338 | } | |
339 | // Mozilla sets keyCode to 0 when there is a charCode | |
340 | // but that stops the event on IE. | |
341 | evt.charCode = c; | |
342 | _setKeyChar(evt); | |
343 | break; | |
344 | } | |
345 | } | |
346 | return evt; | |
347 | }; | |
348 | var IESignal = function(handle){ | |
349 | this.handle = handle; | |
350 | }; | |
351 | IESignal.prototype.remove = function(){ | |
352 | delete _dojoIEListeners_[this.handle]; | |
353 | }; | |
354 | var fixListener = function(listener){ | |
355 | // this is a minimal function for closing on the previous listener with as few as variables as possible | |
356 | return function(evt){ | |
357 | evt = on._fixEvent(evt, this); | |
358 | return listener.call(this, evt); | |
359 | } | |
360 | } | |
361 | var fixAttach = function(target, type, listener){ | |
362 | listener = fixListener(listener); | |
363 | if(((target.ownerDocument ? target.ownerDocument.parentWindow : target.parentWindow || target.window || window) != top || | |
364 | has("jscript") < 5.8) && | |
365 | !has("config-_allow_leaks")){ | |
366 | // IE will leak memory on certain handlers in frames (IE8 and earlier) and in unattached DOM nodes for JScript 5.7 and below. | |
367 | // Here we use global redirection to solve the memory leaks | |
368 | if(typeof _dojoIEListeners_ == "undefined"){ | |
369 | _dojoIEListeners_ = []; | |
370 | } | |
371 | var emiter = target[type]; | |
372 | if(!emiter || !emiter.listeners){ | |
373 | var oldListener = emiter; | |
374 | target[type] = emiter = Function('event', 'var callee = arguments.callee; for(var i = 0; i<callee.listeners.length; i++){var listener = _dojoIEListeners_[callee.listeners[i]]; if(listener){listener.call(this,event);}}'); | |
375 | emiter.listeners = []; | |
376 | emiter.global = this; | |
377 | if(oldListener){ | |
378 | emiter.listeners.push(_dojoIEListeners_.push(oldListener) - 1); | |
379 | } | |
380 | } | |
381 | var handle; | |
382 | emiter.listeners.push(handle = (emiter.global._dojoIEListeners_.push(listener) - 1)); | |
383 | return new IESignal(handle); | |
384 | } | |
385 | return aspect.after(target, type, listener, true); | |
386 | }; | |
387 | ||
388 | var _setKeyChar = function(evt){ | |
389 | evt.keyChar = evt.charCode ? String.fromCharCode(evt.charCode) : ''; | |
390 | evt.charOrCode = evt.keyChar || evt.keyCode; | |
391 | }; | |
392 | // Called in Event scope | |
393 | var stopPropagation = function(){ | |
394 | this.cancelBubble = true; | |
395 | }; | |
396 | var preventDefault = on._preventDefault = function(){ | |
397 | // Setting keyCode to 0 is the only way to prevent certain keypresses (namely | |
398 | // ctrl-combinations that correspond to menu accelerator keys). | |
399 | // Otoh, it prevents upstream listeners from getting this information | |
400 | // Try to split the difference here by clobbering keyCode only for ctrl | |
401 | // combinations. If you still need to access the key upstream, bubbledKeyCode is | |
402 | // provided as a workaround. | |
403 | this.bubbledKeyCode = this.keyCode; | |
404 | if(this.ctrlKey){ | |
405 | try{ | |
406 | // squelch errors when keyCode is read-only | |
407 | // (e.g. if keyCode is ctrl or shift) | |
408 | this.keyCode = 0; | |
409 | }catch(e){ | |
410 | } | |
411 | } | |
412 | this.returnValue = false; | |
413 | }; | |
414 | } | |
415 | if(has("touch")){ | |
416 | var Event = function (){}; | |
417 | var windowOrientation = window.orientation; | |
418 | var fixTouchListener = function(listener){ | |
419 | return function(originalEvent){ | |
420 | //Event normalization(for ontouchxxx and resize): | |
421 | //1.incorrect e.pageX|pageY in iOS | |
422 | //2.there are no "e.rotation", "e.scale" and "onorientationchange" in Andriod | |
423 | //3.More TBD e.g. force | screenX | screenX | clientX | clientY | radiusX | radiusY | |
424 | ||
425 | // see if it has already been corrected | |
426 | var event = originalEvent.corrected; | |
427 | if(!event){ | |
428 | var type = originalEvent.type; | |
429 | try{ | |
430 | delete originalEvent.type; // on some JS engines (android), deleting properties make them mutable | |
431 | }catch(e){} | |
432 | if(originalEvent.type){ | |
433 | // deleting properties doesn't work (older iOS), have to use delegation | |
434 | Event.prototype = originalEvent; | |
435 | var event = new Event; | |
436 | // have to delegate methods to make them work | |
437 | event.preventDefault = function(){ | |
438 | originalEvent.preventDefault(); | |
439 | }; | |
440 | event.stopPropagation = function(){ | |
441 | originalEvent.stopPropagation(); | |
442 | }; | |
443 | }else{ | |
444 | // deletion worked, use property as is | |
445 | event = originalEvent; | |
446 | event.type = type; | |
447 | } | |
448 | originalEvent.corrected = event; | |
449 | if(type == 'resize'){ | |
450 | if(windowOrientation == window.orientation){ | |
451 | return null;//double tap causes an unexpected 'resize' in Andriod | |
452 | } | |
453 | windowOrientation = window.orientation; | |
454 | event.type = "orientationchange"; | |
455 | return listener.call(this, event); | |
456 | } | |
457 | // We use the original event and augment, rather than doing an expensive mixin operation | |
458 | if(!("rotation" in event)){ // test to see if it has rotation | |
459 | event.rotation = 0; | |
460 | event.scale = 1; | |
461 | } | |
462 | //use event.changedTouches[0].pageX|pageY|screenX|screenY|clientX|clientY|target | |
463 | var firstChangeTouch = event.changedTouches[0]; | |
464 | for(var i in firstChangeTouch){ // use for-in, we don't need to have dependency on dojo/_base/lang here | |
465 | delete event[i]; // delete it first to make it mutable | |
466 | event[i] = firstChangeTouch[i]; | |
467 | } | |
468 | } | |
469 | return listener.call(this, event); | |
470 | }; | |
471 | }; | |
472 | } | |
473 | return on; | |
474 | }); |