X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/util/Observable.js diff --git a/src/util/Observable.js b/src/util/Observable.js new file mode 100644 index 00000000..532d6418 --- /dev/null +++ b/src/util/Observable.js @@ -0,0 +1,737 @@ +/** + * @class Ext.util.Observable + * Base class that provides a common interface for publishing events. Subclasses are expected to + * to have a property "events" with all the events defined, and, optionally, a property "listeners" + * with configured listeners defined.
+ * For example: + *

+Employee = Ext.extend(Ext.util.Observable, {
+    constructor: function(config){
+        this.name = config.name;
+        this.addEvents({
+            "fired" : true,
+            "quit" : true
+        });
+
+        // Copy configured listeners into *this* object so that the base class's
+        // constructor will add them.
+        this.listeners = config.listeners;
+
+        // Call our superclass constructor to complete construction process.
+        Employee.superclass.constructor.call(this, config)
+    }
+});
+
+ * This could then be used like this:

+var newEmployee = new Employee({
+    name: employeeName,
+    listeners: {
+        quit: function() {
+            // By default, "this" will be the object that fired the event.
+            alert(this.name + " has quit!");
+        }
+    }
+});
+
+ */ + +Ext.define('Ext.util.Observable', { + + /* Begin Definitions */ + + requires: ['Ext.util.Event'], + + statics: { + /** + * Removes all added captures from the Observable. + * @param {Observable} o The Observable to release + * @static + */ + releaseCapture: function(o) { + o.fireEvent = this.prototype.fireEvent; + }, + + /** + * Starts capture on the specified Observable. All events will be passed + * to the supplied function with the event name + standard signature of the event + * before the event is fired. If the supplied function returns false, + * the event will not fire. + * @param {Observable} o The Observable to capture events from. + * @param {Function} fn The function to call when an event is fired. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Observable firing the event. + * @static + */ + capture: function(o, fn, scope) { + o.fireEvent = Ext.Function.createInterceptor(o.fireEvent, fn, scope); + }, + + /** +Sets observability on the passed class constructor. + +This makes any event fired on any instance of the passed class also fire a single event through +the __class__ allowing for central handling of events on many instances at once. + +Usage: + + Ext.util.Observable.observe(Ext.data.Connection); + Ext.data.Connection.on('beforerequest', function(con, options) { + console.log('Ajax request made to ' + options.url); + }); + + * @param {Function} c The class constructor to make observable. + * @param {Object} listeners An object containing a series of listeners to add. See {@link #addListener}. + * @static + * @markdown + */ + observe: function(cls, listeners) { + if (cls) { + if (!cls.isObservable) { + Ext.applyIf(cls, new this()); + this.capture(cls.prototype, cls.fireEvent, cls); + } + if (Ext.isObject(listeners)) { + cls.on(listeners); + } + return cls; + } + } + }, + + /* End Definitions */ + + /** + * @cfg {Object} listeners (optional)

A config object containing one or more event handlers to be added to this + * object during initialization. This should be a valid listeners config object as specified in the + * {@link #addListener} example for attaching multiple handlers at once.

+ *

DOM events from ExtJs {@link Ext.Component Components}

+ *

While some ExtJs Component classes export selected DOM events (e.g. "click", "mouseover" etc), this + * is usually only done when extra value can be added. For example the {@link Ext.view.View DataView}'s + * {@link Ext.view.View#click click} event passing the node clicked on. To access DOM + * events directly from a child element of a Component, we need to specify the element option to + * identify the Component property to add a DOM listener to: + *


+new Ext.panel.Panel({
+    width: 400,
+    height: 200,
+    dockedItems: [{
+        xtype: 'toolbar'
+    }],
+    listeners: {
+        click: {
+            element: 'el', //bind to the underlying el property on the panel
+            fn: function(){ console.log('click el'); }
+        },
+        dblclick: {
+            element: 'body', //bind to the underlying body property on the panel
+            fn: function(){ console.log('dblclick body'); }
+        }
+    }
+});
+
+ *

+ */ + // @private + isObservable: true, + + constructor: function(config) { + var me = this; + + Ext.apply(me, config); + if (me.listeners) { + me.on(me.listeners); + delete me.listeners; + } + me.events = me.events || {}; + + if (me.bubbleEvents) { + me.enableBubble(me.bubbleEvents); + } + }, + + // @private + eventOptionsRe : /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate|element|vertical|horizontal)$/, + + /** + *

Adds listeners to any Observable object (or Element) which are automatically removed when this Component + * is destroyed. + * @param {Observable/Element} item The item to which to add a listener/listeners. + * @param {Object/String} ename The event name, or an object containing event name properties. + * @param {Function} fn Optional. If the ename parameter was an event name, this + * is the handler function. + * @param {Object} scope Optional. If the ename parameter was an event name, this + * is the scope (this reference) in which the handler function is executed. + * @param {Object} opt Optional. If the ename parameter was an event name, this + * is the {@link Ext.util.Observable#addListener addListener} options. + */ + addManagedListener : function(item, ename, fn, scope, options) { + var me = this, + managedListeners = me.managedListeners = me.managedListeners || [], + config; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); + } + } + } + } + else { + managedListeners.push({ + item: item, + ename: ename, + fn: fn, + scope: scope, + options: options + }); + + item.on(ename, fn, scope, options); + } + }, + + /** + * Removes listeners that were added by the {@link #mon} method. + * @param {Observable|Element} item The item from which to remove a listener/listeners. + * @param {Object|String} ename The event name, or an object containing event name properties. + * @param {Function} fn Optional. If the ename parameter was an event name, this + * is the handler function. + * @param {Object} scope Optional. If the ename parameter was an event name, this + * is the scope (this reference) in which the handler function is executed. + */ + removeManagedListener : function(item, ename, fn, scope) { + var me = this, + options, + config, + managedListeners, + managedListener, + length, + i; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope); + } + } + } + } + + managedListeners = me.managedListeners ? me.managedListeners.slice() : []; + length = managedListeners.length; + + for (i = 0; i < length; i++) { + managedListener = managedListeners[i]; + if (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope)) { + Ext.Array.remove(me.managedListeners, managedListener); + item.un(managedListener.ename, managedListener.fn, managedListener.scope); + } + } + }, + + /** + *

Fires the specified event with the passed parameters (minus the event name).

+ *

An event may be set to bubble up an Observable parent hierarchy (See {@link Ext.Component#getBubbleTarget}) + * by calling {@link #enableBubble}.

+ * @param {String} eventName The name of the event to fire. + * @param {Object...} args Variable number of parameters are passed to handlers. + * @return {Boolean} returns false if any of the handlers return false otherwise it returns true. + */ + fireEvent: function() { + var me = this, + args = Ext.Array.toArray(arguments), + ename = args[0].toLowerCase(), + ret = true, + event = me.events[ename], + queue = me.eventQueue, + parent; + + if (me.eventsSuspended === true) { + if (queue) { + queue.push(args); + } + } else if (event && Ext.isObject(event) && event.bubble) { + if (event.fire.apply(event, args.slice(1)) === false) { + return false; + } + parent = me.getBubbleTarget && me.getBubbleTarget(); + if (parent && parent.isObservable) { + if (!parent.events[ename] || !Ext.isObject(parent.events[ename]) || !parent.events[ename].bubble) { + parent.enableBubble(ename); + } + return parent.fireEvent.apply(parent, args); + } + } else if (event && Ext.isObject(event)) { + args.shift(); + ret = event.fire.apply(event, args); + } + return ret; + }, + + /** + * Appends an event handler to this object. + * @param {String} eventName The name of the event to listen for. May also be an object who's property names are event names. See + * @param {Function} handler The method the event invokes. + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * If omitted, defaults to the object which fired the event. + * @param {Object} options (optional) An object containing handler configuration. + * properties. This may contain any of the following properties:
+ *

+ * Combining Options
+ * Using the options argument, it is possible to combine different types of listeners:
+ *
+ * A delayed, one-time listener. + *


+myPanel.on('hide', this.handleClick, this, {
+single: true,
+delay: 100
+});
+ *

+ * Attaching multiple handlers in 1 call
+ * The method also allows for a single argument to be passed which is a config object containing properties + * which specify multiple events. For example:


+myGridPanel.on({
+    cellClick: this.onCellClick,
+    mouseover: this.onMouseOver,
+    mouseout: this.onMouseOut,
+    scope: this // Important. Ensure "this" is correct during handler execution
+});
+
. + *

+ */ + addListener: function(ename, fn, scope, options) { + var me = this, + config, + event; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.addListener(ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); + } + } + } + } + else { + ename = ename.toLowerCase(); + me.events[ename] = me.events[ename] || true; + event = me.events[ename] || true; + if (Ext.isBoolean(event)) { + me.events[ename] = event = new Ext.util.Event(me, ename); + } + event.addListener(fn, scope, Ext.isObject(options) ? options : {}); + } + }, + + /** + * Removes an event handler. + * @param {String} eventName The type of event the handler was associated with. + * @param {Function} handler The handler to remove. This must be a reference to the function passed into the {@link #addListener} call. + * @param {Object} scope (optional) The scope originally specified for the handler. + */ + removeListener: function(ename, fn, scope) { + var me = this, + config, + event, + options; + + if (Ext.isObject(ename)) { + options = ename; + for (ename in options) { + if (options.hasOwnProperty(ename)) { + config = options[ename]; + if (!me.eventOptionsRe.test(ename)) { + me.removeListener(ename, config.fn || config, config.scope || options.scope); + } + } + } + } else { + ename = ename.toLowerCase(); + event = me.events[ename]; + if (event.isEvent) { + event.removeListener(fn, scope); + } + } + }, + + /** + * Removes all listeners for this object including the managed listeners + */ + clearListeners: function() { + var events = this.events, + event, + key; + + for (key in events) { + if (events.hasOwnProperty(key)) { + event = events[key]; + if (event.isEvent) { + event.clearListeners(); + } + } + } + + this.clearManagedListeners(); + }, + + // + purgeListeners : function() { + console.warn('Observable: purgeListeners has been deprecated. Please use clearListeners.'); + return this.clearListeners.apply(this, arguments); + }, + // + + /** + * Removes all managed listeners for this object. + */ + clearManagedListeners : function() { + var managedListeners = this.managedListeners || [], + i = 0, + len = managedListeners.length, + managedListener; + + for (; i < len; i++) { + managedListener = managedListeners[i]; + managedListener.item.un(managedListener.ename, managedListener.fn, managedListener.scope); + } + + this.managedListeners = []; + }, + + // + purgeManagedListeners : function() { + console.warn('Observable: purgeManagedListeners has been deprecated. Please use clearManagedListeners.'); + return this.clearManagedListeners.apply(this, arguments); + }, + // + + /** + * Adds the specified events to the list of events which this Observable may fire. + * @param {Object/String} o Either an object with event names as properties with a value of true + * or the first event name string if multiple event names are being passed as separate parameters. + * @param {String} [additional] Optional additional event names if multiple event names are being passed as separate parameters. + * Usage:


+this.addEvents('storeloaded', 'storecleared');
+
+ */ + addEvents: function(o) { + var me = this, + args, + len, + i; + + me.events = me.events || {}; + if (Ext.isString(o)) { + args = arguments; + i = args.length; + + while (i--) { + me.events[args[i]] = me.events[args[i]] || true; + } + } else { + Ext.applyIf(me.events, o); + } + }, + + /** + * Checks to see if this object has any listeners for a specified event + * @param {String} eventName The name of the event to check for + * @return {Boolean} True if the event is being listened for, else false + */ + hasListener: function(ename) { + var event = this.events[ename.toLowerCase()]; + return event && event.isEvent === true && event.listeners.length > 0; + }, + + /** + * Suspend the firing of all events. (see {@link #resumeEvents}) + * @param {Boolean} queueSuspended Pass as true to queue up suspended events to be fired + * after the {@link #resumeEvents} call instead of discarding all suspended events; + */ + suspendEvents: function(queueSuspended) { + this.eventsSuspended = true; + if (queueSuspended && !this.eventQueue) { + this.eventQueue = []; + } + }, + + /** + * Resume firing events. (see {@link #suspendEvents}) + * If events were suspended using the queueSuspended parameter, then all + * events fired during event suspension will be sent to any listeners now. + */ + resumeEvents: function() { + var me = this, + queued = me.eventQueue || []; + + me.eventsSuspended = false; + delete me.eventQueue; + + Ext.each(queued, + function(e) { + me.fireEvent.apply(me, e); + }); + }, + + /** + * Relays selected events from the specified Observable as if the events were fired by this. + * @param {Object} origin The Observable whose events this object is to relay. + * @param {Array} events Array of event names to relay. + */ + relayEvents : function(origin, events, prefix) { + prefix = prefix || ''; + var me = this, + len = events.length, + i = 0, + oldName, + newName; + + for (; i < len; i++) { + oldName = events[i].substr(prefix.length); + newName = prefix + oldName; + me.events[newName] = me.events[newName] || true; + origin.on(oldName, me.createRelayer(newName)); + } + }, + + /** + * @private + * Creates an event handling function which refires the event from this object as the passed event name. + * @param newName + * @returns {Function} + */ + createRelayer: function(newName){ + var me = this; + return function(){ + return me.fireEvent.apply(me, [newName].concat(Array.prototype.slice.call(arguments, 0, -1))); + }; + }, + + /** + *

Enables events fired by this Observable to bubble up an owner hierarchy by calling + * this.getBubbleTarget() if present. There is no implementation in the Observable base class.

+ *

This is commonly used by Ext.Components to bubble events to owner Containers. See {@link Ext.Component#getBubbleTarget}. The default + * implementation in Ext.Component returns the Component's immediate owner. But if a known target is required, this can be overridden to + * access the required target more quickly.

+ *

Example:


+Ext.override(Ext.form.field.Base, {
+//  Add functionality to Field's initComponent to enable the change event to bubble
+initComponent : Ext.Function.createSequence(Ext.form.field.Base.prototype.initComponent, function() {
+    this.enableBubble('change');
+}),
+
+//  We know that we want Field's events to bubble directly to the FormPanel.
+getBubbleTarget : function() {
+    if (!this.formPanel) {
+        this.formPanel = this.findParentByType('form');
+    }
+    return this.formPanel;
+}
+});
+
+var myForm = new Ext.formPanel({
+title: 'User Details',
+items: [{
+    ...
+}],
+listeners: {
+    change: function() {
+        // Title goes red if form has been modified.
+        myForm.header.setStyle('color', 'red');
+    }
+}
+});
+
+ * @param {String/Array} events The event name to bubble, or an Array of event names. + */ + enableBubble: function(events) { + var me = this; + if (!Ext.isEmpty(events)) { + events = Ext.isArray(events) ? events: Ext.Array.toArray(arguments); + Ext.each(events, + function(ename) { + ename = ename.toLowerCase(); + var ce = me.events[ename] || true; + if (Ext.isBoolean(ce)) { + ce = new Ext.util.Event(me, ename); + me.events[ename] = ce; + } + ce.bubble = true; + }); + } + } +}, function() { + /** + * Removes an event handler (shorthand for {@link #removeListener}.) + * @param {String} eventName The type of event the handler was associated with. + * @param {Function} handler The handler to remove. This must be a reference to the function passed into the {@link #addListener} call. + * @param {Object} scope (optional) The scope originally specified for the handler. + * @method un + */ + + /** + * Appends an event handler to this object (shorthand for {@link #addListener}.) + * @param {String} eventName The type of event to listen for + * @param {Function} handler The method the event invokes + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * If omitted, defaults to the object which fired the event. + * @param {Object} options (optional) An object containing handler configuration. + * @method on + */ + + this.createAlias({ + on: 'addListener', + un: 'removeListener', + mon: 'addManagedListener', + mun: 'removeManagedListener' + }); + + //deprecated, will be removed in 5.0 + this.observeClass = this.observe; + + Ext.apply(Ext.util.Observable.prototype, function(){ + // this is considered experimental (along with beforeMethod, afterMethod, removeMethodListener?) + // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call + // private + function getMethodEvent(method){ + var e = (this.methodEvents = this.methodEvents || {})[method], + returnValue, + v, + cancel, + obj = this; + + if (!e) { + this.methodEvents[method] = e = {}; + e.originalFn = this[method]; + e.methodName = method; + e.before = []; + e.after = []; + + var makeCall = function(fn, scope, args){ + if((v = fn.apply(scope || obj, args)) !== undefined){ + if (typeof v == 'object') { + if(v.returnValue !== undefined){ + returnValue = v.returnValue; + }else{ + returnValue = v; + } + cancel = !!v.cancel; + } + else + if (v === false) { + cancel = true; + } + else { + returnValue = v; + } + } + }; + + this[method] = function(){ + var args = Array.prototype.slice.call(arguments, 0), + b, i, len; + returnValue = v = undefined; + cancel = false; + + for(i = 0, len = e.before.length; i < len; i++){ + b = e.before[i]; + makeCall(b.fn, b.scope, args); + if (cancel) { + return returnValue; + } + } + + if((v = e.originalFn.apply(obj, args)) !== undefined){ + returnValue = v; + } + + for(i = 0, len = e.after.length; i < len; i++){ + b = e.after[i]; + makeCall(b.fn, b.scope, args); + if (cancel) { + return returnValue; + } + } + return returnValue; + }; + } + return e; + } + + return { + // these are considered experimental + // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call + // adds an 'interceptor' called before the original method + beforeMethod : function(method, fn, scope){ + getMethodEvent.call(this, method).before.push({ + fn: fn, + scope: scope + }); + }, + + // adds a 'sequence' called after the original method + afterMethod : function(method, fn, scope){ + getMethodEvent.call(this, method).after.push({ + fn: fn, + scope: scope + }); + }, + + removeMethodListener: function(method, fn, scope){ + var e = this.getMethodEvent(method), + i, len; + for(i = 0, len = e.before.length; i < len; i++){ + if(e.before[i].fn == fn && e.before[i].scope == scope){ + e.before.splice(i, 1); + return; + } + } + for(i = 0, len = e.after.length; i < len; i++){ + if(e.after[i].fn == fn && e.after[i].scope == scope){ + e.after.splice(i, 1); + return; + } + } + }, + + toggleEventLogging: function(toggle) { + Ext.util.Observable[toggle ? 'capture' : 'releaseCapture'](this, function(en) { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.log(en, arguments); + } + }); + } + }; + }()); +});