source: Dev/trunk/src/client/dojo/on.js @ 487

Last change on this file since 487 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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