source: Dev/branches/rest-dojo-ui/client/dojo/on.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 18.6 KB
Line 
1define(["./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(has("dom")){ // 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});
Note: See TracBrowser for help on using the repository browser.