provide installation instructions
[extjs.git] / source / util / Observable.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10  * @class Ext.util.Observable\r
11  * Abstract base class that provides a common interface for publishing events. Subclasses are expected to\r
12  * to have a property "events" with all the events defined.<br>\r
13  * For example:\r
14  * <pre><code>\r
15  Employee = function(name){\r
16     this.name = name;\r
17     this.addEvents({\r
18         "fired" : true,\r
19         "quit" : true\r
20     });\r
21  }\r
22  Ext.extend(Employee, Ext.util.Observable);\r
23 </code></pre>\r
24  */\r
25 Ext.util.Observable = function(){\r
26     /**\r
27      * @cfg {Object} listeners (optional) A config object containing one or more event handlers to be added to this\r
28      * object during initialization.  This should be a valid listeners config object as specified in the\r
29      * {@link #addListener} example for attaching multiple handlers at once.\r
30      */\r
31     if(this.listeners){\r
32         this.on(this.listeners);\r
33         delete this.listeners;\r
34     }\r
35 };\r
36 Ext.util.Observable.prototype = {\r
37     /**\r
38      * Fires the specified event with the passed parameters (minus the event name).\r
39      * @param {String} eventName\r
40      * @param {Object...} args Variable number of parameters are passed to handlers\r
41      * @return {Boolean} returns false if any of the handlers return false otherwise it returns true\r
42      */\r
43     fireEvent : function(){\r
44         if(this.eventsSuspended !== true){\r
45             var ce = this.events[arguments[0].toLowerCase()];\r
46             if(typeof ce == "object"){\r
47                 return ce.fire.apply(ce, Array.prototype.slice.call(arguments, 1));\r
48             }\r
49         }\r
50         return true;\r
51     },\r
52 \r
53     // private\r
54     filterOptRe : /^(?:scope|delay|buffer|single)$/,\r
55 \r
56     /**\r
57      * Appends an event handler to this component\r
58      * @param {String}   eventName The type of event to listen for\r
59      * @param {Function} handler The method the event invokes\r
60      * @param {Object}   scope (optional) The scope in which to execute the handler\r
61      * function. The handler function's "this" context.\r
62      * @param {Object}   options (optional) An object containing handler configuration\r
63      * properties. This may contain any of the following properties:<ul>\r
64      * <li><b>scope</b> : Object<p class="sub-desc">The scope in which to execute the handler function. The handler function's "this" context.</p></li>\r
65      * <li><b>delay</b> : Number<p class="sub-desc">The number of milliseconds to delay the invocation of the handler after the event fires.</p></li>\r
66      * <li><b>single</b> : Boolean<p class="sub-desc">True to add a handler to handle just the next firing of the event, and then remove itself.</p></li>\r
67      * <li><b>buffer</b> : Number<p class="sub-desc">Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed\r
68      * by the specified number of milliseconds. If the event fires again within that time, the original\r
69      * handler is <em>not</em> invoked, but the new handler is scheduled in its place.</p></li>\r
70      * </ul><br>\r
71      * <p>\r
72      * <b>Combining Options</b><br>\r
73      * Using the options argument, it is possible to combine different types of listeners:<br>\r
74      * <br>\r
75      * A normalized, delayed, one-time listener that auto stops the event and passes a custom argument (forumId)\r
76      * <pre><code>\r
77 el.on('click', this.onClick, this, {\r
78     single: true,\r
79     delay: 100,\r
80     forumId: 4\r
81 });</code></pre>\r
82      * <p>\r
83      * <b>Attaching multiple handlers in 1 call</b><br>\r
84       * The method also allows for a single argument to be passed which is a config object containing properties\r
85      * which specify multiple handlers.\r
86      * <p>\r
87      * <pre><code>\r
88 foo.on({\r
89     'click' : {\r
90         fn: this.onClick,\r
91         scope: this,\r
92         delay: 100\r
93     },\r
94     'mouseover' : {\r
95         fn: this.onMouseOver,\r
96         scope: this\r
97     },\r
98     'mouseout' : {\r
99         fn: this.onMouseOut,\r
100         scope: this\r
101     }\r
102 });</code></pre>\r
103      * <p>\r
104      * Or a shorthand syntax:<br>\r
105      * <pre><code>\r
106 foo.on({\r
107     'click' : this.onClick,\r
108     'mouseover' : this.onMouseOver,\r
109     'mouseout' : this.onMouseOut,\r
110      scope: this\r
111 });</code></pre>\r
112      */\r
113     addListener : function(eventName, fn, scope, o){\r
114         if(typeof eventName == "object"){\r
115             o = eventName;\r
116             for(var e in o){\r
117                 if(this.filterOptRe.test(e)){\r
118                     continue;\r
119                 }\r
120                 if(typeof o[e] == "function"){\r
121                     // shared options\r
122                     this.addListener(e, o[e], o.scope,  o);\r
123                 }else{\r
124                     // individual options\r
125                     this.addListener(e, o[e].fn, o[e].scope, o[e]);\r
126                 }\r
127             }\r
128             return;\r
129         }\r
130         o = (!o || typeof o == "boolean") ? {} : o;\r
131         eventName = eventName.toLowerCase();\r
132         var ce = this.events[eventName] || true;\r
133         if(typeof ce == "boolean"){\r
134             ce = new Ext.util.Event(this, eventName);\r
135             this.events[eventName] = ce;\r
136         }\r
137         ce.addListener(fn, scope, o);\r
138     },\r
139 \r
140     /**\r
141      * Removes a listener\r
142      * @param {String}   eventName     The type of event to listen for\r
143      * @param {Function} handler        The handler to remove\r
144      * @param {Object}   scope  (optional) The scope (this object) for the handler\r
145      */\r
146     removeListener : function(eventName, fn, scope){\r
147         var ce = this.events[eventName.toLowerCase()];\r
148         if(typeof ce == "object"){\r
149             ce.removeListener(fn, scope);\r
150         }\r
151     },\r
152 \r
153     /**\r
154      * Removes all listeners for this object\r
155      */\r
156     purgeListeners : function(){\r
157         for(var evt in this.events){\r
158             if(typeof this.events[evt] == "object"){\r
159                  this.events[evt].clearListeners();\r
160             }\r
161         }\r
162     },\r
163 \r
164     /**\r
165      * Relays selected events from the specified Observable as if the events were fired by <tt><b>this</b></tt>.\r
166      * @param {Object} o The Observable whose events this object is to relay.\r
167      * @param {Array} events Array of event names to relay.\r
168      */\r
169     relayEvents : function(o, events){\r
170         var createHandler = function(ename){\r
171             return function(){\r
172                 return this.fireEvent.apply(this, Ext.combine(ename, Array.prototype.slice.call(arguments, 0)));\r
173             };\r
174         };\r
175         for(var i = 0, len = events.length; i < len; i++){\r
176             var ename = events[i];\r
177             if(!this.events[ename]){ this.events[ename] = true; };\r
178             o.on(ename, createHandler(ename), this);\r
179         }\r
180     },\r
181 \r
182     /**\r
183      * Used to define events on this Observable\r
184      * @param {Object} object The object with the events defined\r
185      */\r
186     addEvents : function(o){\r
187         if(!this.events){\r
188             this.events = {};\r
189         }\r
190         if(typeof o == 'string'){\r
191             for(var i = 0, a = arguments, v; v = a[i]; i++){\r
192                 if(!this.events[a[i]]){\r
193                     this.events[a[i]] = true;\r
194                 }\r
195             }\r
196         }else{\r
197             Ext.applyIf(this.events, o);\r
198         }\r
199     },\r
200 \r
201     /**\r
202      * Checks to see if this object has any listeners for a specified event\r
203      * @param {String} eventName The name of the event to check for\r
204      * @return {Boolean} True if the event is being listened for, else false\r
205      */\r
206     hasListener : function(eventName){\r
207         var e = this.events[eventName];\r
208         return typeof e == "object" && e.listeners.length > 0;\r
209     },\r
210 \r
211     /**\r
212      * Suspend the firing of all events. (see {@link #resumeEvents})\r
213      */\r
214     suspendEvents : function(){\r
215         this.eventsSuspended = true;\r
216     },\r
217 \r
218     /**\r
219      * Resume firing events. (see {@link #suspendEvents})\r
220      */\r
221     resumeEvents : function(){\r
222         this.eventsSuspended = false;\r
223     },\r
224 \r
225     // these are considered experimental\r
226     // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call\r
227     // private\r
228     getMethodEvent : function(method){\r
229         if(!this.methodEvents){\r
230             this.methodEvents = {};\r
231         }\r
232         var e = this.methodEvents[method];\r
233         if(!e){\r
234             e = {};\r
235             this.methodEvents[method] = e;\r
236 \r
237             e.originalFn = this[method];\r
238             e.methodName = method;\r
239             e.before = [];\r
240             e.after = [];\r
241 \r
242 \r
243             var returnValue, v, cancel;\r
244             var obj = this;\r
245 \r
246             var makeCall = function(fn, scope, args){\r
247                 if((v = fn.apply(scope || obj, args)) !== undefined){\r
248                     if(typeof v === 'object'){\r
249                         if(v.returnValue !== undefined){\r
250                             returnValue = v.returnValue;\r
251                         }else{\r
252                             returnValue = v;\r
253                         }\r
254                         if(v.cancel === true){\r
255                             cancel = true;\r
256                         }\r
257                     }else if(v === false){\r
258                         cancel = true;\r
259                     }else {\r
260                         returnValue = v;\r
261                     }\r
262                 }\r
263             }\r
264 \r
265             this[method] = function(){\r
266                 returnValue = v = undefined; cancel = false;\r
267                 var args = Array.prototype.slice.call(arguments, 0);\r
268                 for(var i = 0, len = e.before.length; i < len; i++){\r
269                     makeCall(e.before[i].fn, e.before[i].scope, args);\r
270                     if(cancel){\r
271                         return returnValue;\r
272                     }\r
273                 }\r
274 \r
275                 if((v = e.originalFn.apply(obj, args)) !== undefined){\r
276                     returnValue = v;\r
277                 }\r
278 \r
279                 for(var i = 0, len = e.after.length; i < len; i++){\r
280                     makeCall(e.after[i].fn, e.after[i].scope, args);\r
281                     if(cancel){\r
282                         return returnValue;\r
283                     }\r
284                 }\r
285                 return returnValue;\r
286             };\r
287         }\r
288         return e;\r
289     },\r
290 \r
291     // adds an "interceptor" called before the original method\r
292     beforeMethod : function(method, fn, scope){\r
293         var e = this.getMethodEvent(method);\r
294         e.before.push({fn: fn, scope: scope});\r
295     },\r
296 \r
297     // adds a "sequence" called after the original method\r
298     afterMethod : function(method, fn, scope){\r
299         var e = this.getMethodEvent(method);\r
300         e.after.push({fn: fn, scope: scope});\r
301     },\r
302 \r
303     removeMethodListener : function(method, fn, scope){\r
304         var e = this.getMethodEvent(method);\r
305         for(var i = 0, len = e.before.length; i < len; i++){\r
306             if(e.before[i].fn == fn && e.before[i].scope == scope){\r
307                 e.before.splice(i, 1);\r
308                 return;\r
309             }\r
310         }\r
311         for(var i = 0, len = e.after.length; i < len; i++){\r
312             if(e.after[i].fn == fn && e.after[i].scope == scope){\r
313                 e.after.splice(i, 1);\r
314                 return;\r
315             }\r
316         }\r
317     }\r
318 };\r
319 /**\r
320  * Appends an event handler to this element (shorthand for addListener)\r
321  * @param {String}   eventName     The type of event to listen for\r
322  * @param {Function} handler        The method the event invokes\r
323  * @param {Object}   scope (optional) The scope in which to execute the handler\r
324  * function. The handler function's "this" context.\r
325  * @param {Object}   options  (optional)\r
326  * @method\r
327  */\r
328 Ext.util.Observable.prototype.on = Ext.util.Observable.prototype.addListener;\r
329 /**\r
330  * Removes a listener (shorthand for removeListener)\r
331  * @param {String}   eventName     The type of event to listen for\r
332  * @param {Function} handler        The handler to remove\r
333  * @param {Object}   scope  (optional) The scope (this object) for the handler\r
334  * @method\r
335  */\r
336 Ext.util.Observable.prototype.un = Ext.util.Observable.prototype.removeListener;\r
337 \r
338 /**\r
339  * Starts capture on the specified Observable. All events will be passed\r
340  * to the supplied function with the event name + standard signature of the event\r
341  * <b>before</b> the event is fired. If the supplied function returns false,\r
342  * the event will not fire.\r
343  * @param {Observable} o The Observable to capture\r
344  * @param {Function} fn The function to call\r
345  * @param {Object} scope (optional) The scope (this object) for the fn\r
346  * @static\r
347  */\r
348 Ext.util.Observable.capture = function(o, fn, scope){\r
349     o.fireEvent = o.fireEvent.createInterceptor(fn, scope);\r
350 };\r
351 \r
352 /**\r
353  * Removes <b>all</b> added captures from the Observable.\r
354  * @param {Observable} o The Observable to release\r
355  * @static\r
356  */\r
357 Ext.util.Observable.releaseCapture = function(o){\r
358     o.fireEvent = Ext.util.Observable.prototype.fireEvent;\r
359 };\r
360 \r
361 (function(){\r
362 \r
363     var createBuffered = function(h, o, scope){\r
364         var task = new Ext.util.DelayedTask();\r
365         return function(){\r
366             task.delay(o.buffer, h, scope, Array.prototype.slice.call(arguments, 0));\r
367         };\r
368     };\r
369 \r
370     var createSingle = function(h, e, fn, scope){\r
371         return function(){\r
372             e.removeListener(fn, scope);\r
373             return h.apply(scope, arguments);\r
374         };\r
375     };\r
376 \r
377     var createDelayed = function(h, o, scope){\r
378         return function(){\r
379             var args = Array.prototype.slice.call(arguments, 0);\r
380             setTimeout(function(){\r
381                 h.apply(scope, args);\r
382             }, o.delay || 10);\r
383         };\r
384     };\r
385 \r
386     Ext.util.Event = function(obj, name){\r
387         this.name = name;\r
388         this.obj = obj;\r
389         this.listeners = [];\r
390     };\r
391 \r
392     Ext.util.Event.prototype = {\r
393         addListener : function(fn, scope, options){\r
394             scope = scope || this.obj;\r
395             if(!this.isListening(fn, scope)){\r
396                 var l = this.createListener(fn, scope, options);\r
397                 if(!this.firing){\r
398                     this.listeners.push(l);\r
399                 }else{ // if we are currently firing this event, don't disturb the listener loop\r
400                     this.listeners = this.listeners.slice(0);\r
401                     this.listeners.push(l);\r
402                 }\r
403             }\r
404         },\r
405 \r
406         createListener : function(fn, scope, o){\r
407             o = o || {};\r
408             scope = scope || this.obj;\r
409             var l = {fn: fn, scope: scope, options: o};\r
410             var h = fn;\r
411             if(o.delay){\r
412                 h = createDelayed(h, o, scope);\r
413             }\r
414             if(o.single){\r
415                 h = createSingle(h, this, fn, scope);\r
416             }\r
417             if(o.buffer){\r
418                 h = createBuffered(h, o, scope);\r
419             }\r
420             l.fireFn = h;\r
421             return l;\r
422         },\r
423 \r
424         findListener : function(fn, scope){\r
425             scope = scope || this.obj;\r
426             var ls = this.listeners;\r
427             for(var i = 0, len = ls.length; i < len; i++){\r
428                 var l = ls[i];\r
429                 if(l.fn == fn && l.scope == scope){\r
430                     return i;\r
431                 }\r
432             }\r
433             return -1;\r
434         },\r
435 \r
436         isListening : function(fn, scope){\r
437             return this.findListener(fn, scope) != -1;\r
438         },\r
439 \r
440         removeListener : function(fn, scope){\r
441             var index;\r
442             if((index = this.findListener(fn, scope)) != -1){\r
443                 if(!this.firing){\r
444                     this.listeners.splice(index, 1);\r
445                 }else{\r
446                     this.listeners = this.listeners.slice(0);\r
447                     this.listeners.splice(index, 1);\r
448                 }\r
449                 return true;\r
450             }\r
451             return false;\r
452         },\r
453 \r
454         clearListeners : function(){\r
455             this.listeners = [];\r
456         },\r
457 \r
458         fire : function(){\r
459             var ls = this.listeners, scope, len = ls.length;\r
460             if(len > 0){\r
461                 this.firing = true;\r
462                 var args = Array.prototype.slice.call(arguments, 0);\r
463                 for(var i = 0; i < len; i++){\r
464                     var l = ls[i];\r
465                     if(l.fireFn.apply(l.scope||this.obj||window, arguments) === false){\r
466                         this.firing = false;\r
467                         return false;\r
468                     }\r
469                 }\r
470                 this.firing = false;\r
471             }\r
472             return true;\r
473         }\r
474     };\r
475 })();