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