Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / util / core / Observable.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 (function(){
8
9 var EXTUTIL = Ext.util,
10     TOARRAY = Ext.toArray,
11     EACH = Ext.each,
12     ISOBJECT = Ext.isObject,
13     TRUE = true,
14     FALSE = false;
15 /**
16  * @class Ext.util.Observable
17  * Base class that provides a common interface for publishing events. Subclasses are expected to
18  * to have a property "events" with all the events defined, and, optionally, a property "listeners"
19  * with configured listeners defined.<br>
20  * For example:
21  * <pre><code>
22 Employee = Ext.extend(Ext.util.Observable, {
23     constructor: function(config){
24         this.name = config.name;
25         this.addEvents({
26             "fired" : true,
27             "quit" : true
28         });
29
30         // Copy configured listeners into *this* object so that the base class&#39;s
31         // constructor will add them.
32         this.listeners = config.listeners;
33
34         // Call our superclass constructor to complete construction process.
35         Employee.superclass.constructor.call(config)
36     }
37 });
38 </code></pre>
39  * This could then be used like this:<pre><code>
40 var newEmployee = new Employee({
41     name: employeeName,
42     listeners: {
43         quit: function() {
44             // By default, "this" will be the object that fired the event.
45             alert(this.name + " has quit!");
46         }
47     }
48 });
49 </code></pre>
50  */
51 EXTUTIL.Observable = function(){
52     /**
53      * @cfg {Object} listeners (optional) <p>A config object containing one or more event handlers to be added to this
54      * object during initialization.  This should be a valid listeners config object as specified in the
55      * {@link #addListener} example for attaching multiple handlers at once.</p>
56      * <br><p><b><u>DOM events from ExtJs {@link Ext.Component Components}</u></b></p>
57      * <br><p>While <i>some</i> ExtJs Component classes export selected DOM events (e.g. "click", "mouseover" etc), this
58      * is usually only done when extra value can be added. For example the {@link Ext.DataView DataView}'s
59      * <b><code>{@link Ext.DataView#click click}</code></b> event passing the node clicked on. To access DOM
60      * events directly from a Component's HTMLElement, listeners must be added to the <i>{@link Ext.Component#getEl Element}</i> after the Component
61      * has been rendered. A plugin can simplify this step:<pre><code>
62 // Plugin is configured with a listeners config object.
63 // The Component is appended to the argument list of all handler functions.
64 Ext.DomObserver = Ext.extend(Object, {
65     constructor: function(config) {
66         this.listeners = config.listeners ? config.listeners : config;
67     },
68
69     // Component passes itself into plugin&#39;s init method
70     init: function(c) {
71         var p, l = this.listeners;
72         for (p in l) {
73             if (Ext.isFunction(l[p])) {
74                 l[p] = this.createHandler(l[p], c);
75             } else {
76                 l[p].fn = this.createHandler(l[p].fn, c);
77             }
78         }
79
80         // Add the listeners to the Element immediately following the render call
81         c.render = c.render.{@link Function#createSequence createSequence}(function() {
82             var e = c.getEl();
83             if (e) {
84                 e.on(l);
85             }
86         });
87     },
88
89     createHandler: function(fn, c) {
90         return function(e) {
91             fn.call(this, e, c);
92         };
93     }
94 });
95
96 var combo = new Ext.form.ComboBox({
97
98     // Collapse combo when its element is clicked on
99     plugins: [ new Ext.DomObserver({
100         click: function(evt, comp) {
101             comp.collapse();
102         }
103     })],
104     store: myStore,
105     typeAhead: true,
106     mode: 'local',
107     triggerAction: 'all'
108 });
109      * </code></pre></p>
110      */
111     var me = this, e = me.events;
112     if(me.listeners){
113         me.on(me.listeners);
114         delete me.listeners;
115     }
116     me.events = e || {};
117 };
118
119 EXTUTIL.Observable.prototype = function(){
120     var filterOptRe = /^(?:scope|delay|buffer|single)$/, toLower = function(s){
121         return s.toLowerCase();
122     };
123
124     return {
125         /**
126          * <p>Fires the specified event with the passed parameters (minus the event name).</p>
127          * <p>An event may be set to bubble up an Observable parent hierarchy (See {@link Ext.Component#getBubbleTarget})
128          * by calling {@link #enableBubble}.</p>
129          * @param {String} eventName The name of the event to fire.
130          * @param {Object...} args Variable number of parameters are passed to handlers.
131          * @return {Boolean} returns false if any of the handlers return false otherwise it returns true.
132          */
133
134         fireEvent : function(){
135             var a = TOARRAY(arguments),
136                 ename = toLower(a[0]),
137                 me = this,
138                 ret = TRUE,
139                 ce = me.events[ename],
140                 q,
141                 c;
142             if (me.eventsSuspended === TRUE) {
143                 if (q = me.suspendedEventsQueue) {
144                     q.push(a);
145                 }
146             }
147             else if(ISOBJECT(ce) && ce.bubble){
148                 if(ce.fire.apply(ce, a.slice(1)) === FALSE) {
149                     return FALSE;
150                 }
151                 c = me.getBubbleTarget && me.getBubbleTarget();
152                 if(c && c.enableBubble) {
153                     c.enableBubble(ename);
154                     return c.fireEvent.apply(c, a);
155                 }
156             }
157             else {
158                 if (ISOBJECT(ce)) {
159                     a.shift();
160                     ret = ce.fire.apply(ce, a);
161                 }
162             }
163             return ret;
164         },
165
166         /**
167          * Appends an event handler to this object.
168          * @param {String}   eventName The name of the event to listen for.
169          * @param {Function} handler The method the event invokes.
170          * @param {Object}   scope (optional) The scope (<code><b>this</b></code> reference) in which the handler function is executed.
171          * <b>If omitted, defaults to the object which fired the event.</b>
172          * @param {Object}   options (optional) An object containing handler configuration.
173          * properties. This may contain any of the following properties:<ul>
174          * <li><b>scope</b> : Object<div class="sub-desc">The scope (<code><b>this</b></code> reference) in which the handler function is executed.
175          * <b>If omitted, defaults to the object which fired the event.</b></div></li>
176          * <li><b>delay</b> : Number<div class="sub-desc">The number of milliseconds to delay the invocation of the handler after the event fires.</div></li>
177          * <li><b>single</b> : Boolean<div class="sub-desc">True to add a handler to handle just the next firing of the event, and then remove itself.</div></li>
178          * <li><b>buffer</b> : Number<div class="sub-desc">Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed
179          * by the specified number of milliseconds. If the event fires again within that time, the original
180          * handler is <em>not</em> invoked, but the new handler is scheduled in its place.</div></li>
181          * <li><b>target</b> : Observable<div class="sub-desc">Only call the handler if the event was fired on the target Observable, <i>not</i>
182          * if the event was bubbled up from a child Observable.</div></li>
183          * </ul><br>
184          * <p>
185          * <b>Combining Options</b><br>
186          * Using the options argument, it is possible to combine different types of listeners:<br>
187          * <br>
188          * A delayed, one-time listener.
189          * <pre><code>
190 myDataView.on('click', this.onClick, this, {
191     single: true,
192     delay: 100
193 });</code></pre>
194          * <p>
195          * <b>Attaching multiple handlers in 1 call</b><br>
196          * The method also allows for a single argument to be passed which is a config object containing properties
197          * which specify multiple handlers.
198          * <p>
199          * <pre><code>
200 myGridPanel.on({
201     'click' : {
202         fn: this.onClick,
203         scope: this,
204         delay: 100
205     },
206     'mouseover' : {
207         fn: this.onMouseOver,
208         scope: this
209     },
210     'mouseout' : {
211         fn: this.onMouseOut,
212         scope: this
213     }
214 });</code></pre>
215      * <p>
216      * Or a shorthand syntax:<br>
217      * <pre><code>
218 myGridPanel.on({
219     'click' : this.onClick,
220     'mouseover' : this.onMouseOver,
221     'mouseout' : this.onMouseOut,
222      scope: this
223 });</code></pre>
224          */
225         addListener : function(eventName, fn, scope, o){
226             var me = this,
227                 e,
228                 oe,
229                 isF,
230             ce;
231             if (ISOBJECT(eventName)) {
232                 o = eventName;
233                 for (e in o){
234                     oe = o[e];
235                     if (!filterOptRe.test(e)) {
236                         me.addListener(e, oe.fn || oe, oe.scope || o.scope, oe.fn ? oe : o);
237                     }
238                 }
239             } else {
240                 eventName = toLower(eventName);
241                 ce = me.events[eventName] || TRUE;
242                 if (typeof ce == "boolean") {
243                     me.events[eventName] = ce = new EXTUTIL.Event(me, eventName);
244                 }
245                 ce.addListener(fn, scope, ISOBJECT(o) ? o : {});
246             }
247         },
248
249         /**
250          * Removes an event handler.
251          * @param {String}   eventName The type of event the handler was associated with.
252          * @param {Function} handler   The handler to remove. <b>This must be a reference to the function passed into the {@link #addListener} call.</b>
253          * @param {Object}   scope     (optional) The scope originally specified for the handler.
254          */
255         removeListener : function(eventName, fn, scope){
256             var ce = this.events[toLower(eventName)];
257             if (ISOBJECT(ce)) {
258                 ce.removeListener(fn, scope);
259             }
260         },
261
262         /**
263          * Removes all listeners for this object
264          */
265         purgeListeners : function(){
266             var events = this.events,
267                 evt,
268                 key;
269             for(key in events){
270                 evt = events[key];
271                 if(ISOBJECT(evt)){
272                     evt.clearListeners();
273                 }
274             }
275         },
276
277         /**
278          * Used to define events on this Observable
279          * @param {Object} object The object with the events defined
280          */
281         addEvents : function(o){
282             var me = this;
283             me.events = me.events || {};
284             if (typeof o == 'string') {
285                 EACH(arguments, function(a) {
286                     me.events[a] = me.events[a] || TRUE;
287                 });
288             } else {
289                 Ext.applyIf(me.events, o);
290             }
291         },
292
293         /**
294          * Checks to see if this object has any listeners for a specified event
295          * @param {String} eventName The name of the event to check for
296          * @return {Boolean} True if the event is being listened for, else false
297          */
298         hasListener : function(eventName){
299             var e = this.events[eventName];
300             return ISOBJECT(e) && e.listeners.length > 0;
301         },
302
303         /**
304          * Suspend the firing of all events. (see {@link #resumeEvents})
305          * @param {Boolean} queueSuspended Pass as true to queue up suspended events to be fired
306          * after the {@link #resumeEvents} call instead of discarding all suspended events;
307          */
308         suspendEvents : function(queueSuspended){
309             this.eventsSuspended = TRUE;
310             if (queueSuspended){
311                 this.suspendedEventsQueue = [];
312             }
313         },
314
315         /**
316          * Resume firing events. (see {@link #suspendEvents})
317          * If events were suspended using the <tt><b>queueSuspended</b></tt> parameter, then all
318          * events fired during event suspension will be sent to any listeners now.
319          */
320         resumeEvents : function(){
321             var me = this;
322             me.eventsSuspended = !delete me.suspendedEventQueue;
323             EACH(me.suspendedEventsQueue, function(e) {
324                 me.fireEvent.apply(me, e);
325             });
326         }
327     }
328 }();
329
330 var OBSERVABLE = EXTUTIL.Observable.prototype;
331 /**
332  * Appends an event handler to this object (shorthand for {@link #addListener}.)
333  * @param {String}   eventName     The type of event to listen for
334  * @param {Function} handler       The method the event invokes
335  * @param {Object}   scope         (optional) The scope (<code><b>this</b></code> reference) in which the handler function is executed.
336  * <b>If omitted, defaults to the object which fired the event.</b>
337  * @param {Object}   options       (optional) An object containing handler configuration.
338  * @method
339  */
340 OBSERVABLE.on = OBSERVABLE.addListener;
341 /**
342  * Removes an event handler (shorthand for {@link #removeListener}.)
343  * @param {String}   eventName     The type of event the handler was associated with.
344  * @param {Function} handler       The handler to remove. <b>This must be a reference to the function passed into the {@link #addListener} call.</b>
345  * @param {Object}   scope         (optional) The scope originally specified for the handler.
346  * @method
347  */
348 OBSERVABLE.un = OBSERVABLE.removeListener;
349
350 /**
351  * Removes <b>all</b> added captures from the Observable.
352  * @param {Observable} o The Observable to release
353  * @static
354  */
355 EXTUTIL.Observable.releaseCapture = function(o){
356     o.fireEvent = OBSERVABLE.fireEvent;
357 };
358
359 function createTargeted(h, o, scope){
360     return function(){
361         if(o.target == arguments[0]){
362             h.apply(scope, TOARRAY(arguments));
363         }
364     };
365 };
366
367 function createBuffered(h, o, scope){
368     var task = new EXTUTIL.DelayedTask();
369     return function(){
370         task.delay(o.buffer, h, scope, TOARRAY(arguments));
371     };
372 }
373
374 function createSingle(h, e, fn, scope){
375     return function(){
376         e.removeListener(fn, scope);
377         return h.apply(scope, arguments);
378     };
379 }
380
381 function createDelayed(h, o, scope){
382     return function(){
383         var args = TOARRAY(arguments);
384         (function(){
385             h.apply(scope, args);
386         }).defer(o.delay || 10);
387     };
388 };
389
390 EXTUTIL.Event = function(obj, name){
391     this.name = name;
392     this.obj = obj;
393     this.listeners = [];
394 };
395
396 EXTUTIL.Event.prototype = {
397     addListener : function(fn, scope, options){
398         var me = this,
399             l;
400         scope = scope || me.obj;
401         if(!me.isListening(fn, scope)){
402             l = me.createListener(fn, scope, options);
403             if(me.firing){ // if we are currently firing this event, don't disturb the listener loop
404                 me.listeners = me.listeners.slice(0);
405             }
406             me.listeners.push(l);
407         }
408     },
409
410     createListener: function(fn, scope, o){
411         o = o || {}, scope = scope || this.obj;
412         var l = {
413             fn: fn,
414             scope: scope,
415             options: o
416         }, h = fn;
417         if(o.target){
418             h = createTargeted(h, o, scope);
419         }
420         if(o.delay){
421             h = createDelayed(h, o, scope);
422         }
423         if(o.single){
424             h = createSingle(h, this, fn, scope);
425         }
426         if(o.buffer){
427             h = createBuffered(h, o, scope);
428         }
429         l.fireFn = h;
430         return l;
431     },
432
433     findListener : function(fn, scope){
434         var s, ret = -1;
435         EACH(this.listeners, function(l, i) {
436             s = l.scope;
437             if(l.fn == fn && (s == scope || s == this.obj)){
438                 ret = i;
439                 return FALSE;
440             }
441         },
442         this);
443         return ret;
444     },
445
446     isListening : function(fn, scope){
447         return this.findListener(fn, scope) != -1;
448     },
449
450     removeListener : function(fn, scope){
451         var index,
452             me = this,
453             ret = FALSE;
454         if((index = me.findListener(fn, scope)) != -1){
455             if (me.firing) {
456                 me.listeners = me.listeners.slice(0);
457             }
458             me.listeners.splice(index, 1);
459             ret = TRUE;
460         }
461         return ret;
462     },
463
464     clearListeners : function(){
465         this.listeners = [];
466     },
467
468     fire : function(){
469         var me = this,
470             args = TOARRAY(arguments),
471             ret = TRUE;
472
473         EACH(me.listeners, function(l) {
474             me.firing = TRUE;
475             if (l.fireFn.apply(l.scope || me.obj || window, args) === FALSE) {
476                 return ret = me.firing = FALSE;
477             }
478         });
479         me.firing = FALSE;
480         return ret;
481     }
482 };
483 })();