(function() {
/**
* @class Ext.mixin.Observable
* @alternateClassName Ext.util.Observable
* @extend Ext.mixin.Mixin
* @mixins Ext.mixin.Identifiable
*
* Mixin that provides a common interface for publishing events.
*/
var Observable = Ext.define('Ext.mixin.Observable', {
requires: ['Ext.event.Dispatcher'],
extend: 'Ext.mixin.Mixin',
mixins: ['Ext.mixin.Identifiable'],
mixinConfig: {
id: 'observable',
beforeHooks: {
constructor: 'constructor'
},
hooks: {
destroy: 'destroy'
}
},
alternateClassName: 'Ext.util.Observable',
statics: {
releaseCapture: function(o) {
console.log('TODO: static releaseCapture');
},
capture: function(o, fn, scope) {
console.log('TODO: static capture');
},
observe: function(cls, listeners) {
console.log('TODO: static observe');
}
},
// @private
isObservable: true,
observableType: 'observable',
validIdRegex: /^([\w\-]+)$/,
observableIdPrefix: '#',
ADD_LISTENER_ACTION: 'doAddListener',
REMOVE_LISTENER_ACTION: 'doRemoveListener',
listenerOptionsRegex: /^(?:delay|buffer|single|args|delegate)$/,
config: {
/**
* @cfg {Object} listeners
*
* 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.
*/
listeners: null,
/**
* @cfg {String/String[]} bubbleEvents The event name to bubble, or an Array of event names.
*/
bubbleEvents: null
},
constructor: function(config) {
if (config) {
if ('listeners' in config) {
this.setListeners(config.listeners);
delete config.listeners;
}
if ('bubbleEvents' in config) {
this.setBubbleEvents(config.bubbleEvents);
delete config.bubbleEvents;
}
}
return this;
},
applyListeners: function(listeners) {
if (listeners) {
this.addListener(listeners);
}
},
applyBubbleEvents: function(bubbleEvents) {
if (bubbleEvents) {
this.enableBubble(bubbleEvents);
}
},
getOptimizedObservableId: function() {
return this.observableId;
},
getObservableId: function() {
if (!this.observableId) {
var id = this.getUniqueId();
//<debug error>
if (!id.match(this.validIdRegex)) {
Ext.Logger.error("Invalid unique id of '" + id + "' for this object", this);
}
//</debug>
this.observableId = this.observableIdPrefix + id;
this.getObservableId = this.getOptimizedObservableId;
}
return this.observableId;
},
getOptimizedEventDispatcher: function() {
return this.eventDispatcher;
},
getEventDispatcher: function() {
if (!this.eventDispatcher) {
this.eventDispatcher = Ext.event.Dispatcher.getInstance();
this.getEventDispatcher = this.getOptimizedEventDispatcher;
}
return this.eventDispatcher;
},
getManagedListeners: function(object, eventName) {
var id = object.getUniqueId(),
managedListeners = this.managedListeners;
if (!managedListeners) {
this.managedListeners = managedListeners = {};
}
if (!managedListeners[id]) {
managedListeners[id] = {};
object.doAddListener('destroy', 'clearManagedListeners', this, {
single: true,
args: [object]
});
}
if (!managedListeners[id][eventName]) {
managedListeners[id][eventName] = [];
}
return managedListeners[id][eventName];
},
getUsedSelectors: function() {
var selectors = this.usedSelectors;
if (!selectors) {
selectors = this.usedSelectors = [];
selectors.$map = {};
}
return selectors;
},
/**
* Fires the specified event with the passed parameters (minus the event name, plus the `options` object passed
* to {@link #addListener}).
*
* An event may be set to bubble up an Observable parent hierarchy 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(eventName) {
var args = Array.prototype.slice.call(arguments, 1);
return this.doFireEvent(eventName, args);
},
fireAction: function(eventName, args, fn, scope, options, order) {
var actions = [];
if (args === undefined) {
args = [];
}
if (fn !== undefined) {
actions.push({
fn: fn,
scope: scope || this,
options: options,
order: order
});
}
return this.doFireEvent(eventName, args, actions);
},
doFireEvent: function(eventName, args, actions) {
if (this.eventFiringSuspended) {
return;
}
var id = this.getObservableId(),
dispatcher = this.getEventDispatcher();
return dispatcher.dispatchEvent(this.observableType, id, eventName, args, actions);
},
/**
* @private
* @param name
* @param fn
* @param scope
* @param options
*/
doAddListener: function(name, fn, scope, options, order) {
if (typeof fn !== 'string' && typeof fn !== 'function') {
scope = fn.scope || scope;
if (fn.before) {
this.doAddListener(name, fn.before, scope, options, 'before');
}
if (fn.after) {
this.doAddListener(name, fn.after, scope, options, 'current');
}
return;
}
var isManaged = (scope && scope !== this && scope.isIdentifiable),
dispatcher = this.getEventDispatcher(),
usedSelectors = this.getUsedSelectors(),
usedSelectorsMap = usedSelectors.$map,
selector = this.getObservableId(),
isAdded, managedListeners, delegate;
if (!options) {
options = {};
}
if (!scope) {
scope = this;
}
if (options.delegate) {
delegate = options.delegate;
// TODO: Compress the selector here
selector += ' ' + delegate;
}
if (!(selector in usedSelectorsMap)) {
usedSelectorsMap[selector] = true;
usedSelectors.push(selector);
}
isAdded = dispatcher.addListener(this.observableType, selector, name, fn, scope, options, order);
if (isAdded && isManaged) {
managedListeners = this.getManagedListeners(scope, name);
managedListeners.push({
delegate: delegate,
scope: scope,
fn: fn,
order: order
});
}
return isAdded;
},
doRemoveListener: function(name, fn, scope, options, order) {
if (typeof fn !== 'string' && typeof fn !== 'function') {
scope = fn.scope || scope;
if (fn.before) {
this.doRemoveListener(name, fn.before, scope, options, 'before');
}
if (fn.after) {
this.doRemoveListener(name, fn.after, scope, options, 'current');
}
return;
}
var isManaged = (scope && scope !== this && scope.isIdentifiable),
selector = this.getObservableId(),
isRemoved,
managedListeners, i, ln, listener, delegate;
if (options && options.delegate) {
delegate = options.delegate;
// TODO: Compress the selector here
selector += ' ' + delegate;
}
if (!scope) {
scope = this;
}
isRemoved = this.getEventDispatcher().removeListener(this.observableType, selector, name, fn, scope, order);
if (isRemoved && isManaged) {
managedListeners = this.getManagedListeners(scope, name);
for (i = 0,ln = managedListeners.length; i < ln; i++) {
listener = managedListeners[i];
if (listener.fn === fn && listener.scope === scope && listener.delegate === delegate && listener.order === order) {
managedListeners.splice(i, 1);
break;
}
}
}
return isRemoved;
},
clearManagedListeners: function(object) {
var managedListeners = this.managedListeners,
id, namedListeners, listeners, eventName, i, ln, listener, options;
if (!managedListeners) {
return this;
}
if (object) {
if (typeof object != 'string') {
id = object.getUniqueId();
}
else {
id = object;
}
namedListeners = managedListeners[id];
for (eventName in namedListeners) {
if (namedListeners.hasOwnProperty(eventName)) {
listeners = namedListeners[eventName];
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
options = {};
if (listener.delegate) {
options.delegate = listener.delegate;
}
if (this.doRemoveListener(eventName, listener.fn, listener.scope, options, listener.order)) {
i--;
ln--;
}
}
}
}
delete managedListeners[id];
return this;
}
for (id in managedListeners) {
if (managedListeners.hasOwnProperty(id)) {
this.clearManagedListeners(id);
}
}
},
/**
* @private
* @param operation
* @param eventName
* @param fn
* @param scope
* @param options
* @param order
*/
changeListener: function(action, eventName, fn, scope, options, order) {
var eventNames,
listeners,
listenerOptionsRegex,
actualOptions,
name, value, i, ln, listener;
if (typeof fn != 'undefined') {
// Support for array format to add multiple listeners
if (typeof eventName != 'string') {
for (i = 0,ln = eventName.length; i < ln; i++) {
name = eventName[i];
this[action](name, fn, scope, options, order);
}
return this;
}
this[action](eventName, fn, scope, options, order);
}
else if (Ext.isArray(eventName)) {
listeners = eventName;
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
this[action](listener.event, listener.fn, listener.scope, listener, listener.order);
}
}
else {
listenerOptionsRegex = this.listenerOptionsRegex;
options = eventName;
eventNames = [];
listeners = [];
actualOptions = {};
for (name in options) {
if (options.hasOwnProperty(name)) {
value = options[name];
if (name === 'scope') {
scope = value;
continue;
}
else if (name === 'order') {
order = value;
continue;
}
if (!listenerOptionsRegex.test(name)) {
eventNames.push(name);
listeners.push(value);
}
else {
actualOptions[name] = value;
}
}
}
for (i = 0,ln = eventNames.length; i < ln; i++) {
this[action](eventNames[i], listeners[i], scope, actualOptions, order);
}
}
},
/**
* 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.
* @param {Function} fn The method the event invokes. Will be called with arguments given to
* {@link #fireEvent} plus the `options` parameter described below.
* @param {Object} [scope] The scope (`this` reference) in which the handler function is executed. **If
* omitted, defaults to the object which fired the event.**
* @param {Object} [options] An object containing handler configuration.
*
* This object may contain any of the following properties:
*
* - **scope** : Object
*
* The scope (`this` reference) in which the handler function is executed. **If omitted, defaults to the object
* which fired the event.**
*
* - **delay** : Number
*
* The number of milliseconds to delay the invocation of the handler after the event fires.
*
* - **single** : Boolean
*
* True to add a handler to handle just the next firing of the event, and then remove itself.
*
* - **buffer** : Number
*
* Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed by the specified number of
* milliseconds. If the event fires again within that time, the original handler is _not_ invoked, but the new
* handler is scheduled in its place.
*
* - **delegate** : String
*
* Uses {@link Ext.ComponentQuery} to delegate events to a specified query selector within this item.
*
* // Create a container with a two children; a button and a toolbar
* var container = Ext.create('Ext.Container', {
* items: [
* {
* xtype: 'toolbar',
* dock: 'top',
* title: 'My Toolbar'
* },
* {
* xtype: 'button',
* text: 'My Button'
* }
* ]
* });
*
* container.on({
* // Ext.Buttons have an xtype of 'button', so we use that are a selector for our delegate
* delegate: 'button',
*
* tap: function() {
* alert('Button tapped!');
* }
* });
*
* **Combining Options**
*
* Using the options argument, it is possible to combine different types of listeners:
*
* A delayed, one-time listener.
*
* container.on('tap', this.handleTap, 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:
*
* container.on({
* tap : this.onTap,
* swipe: this.onSwipe,
*
* scope: this // Important. Ensure "this" is correct during handler execution
* });
*
* One can also specify options for each event handler separately:
*
* container.on({
* tap : { fn: this.onTap, scope: this, single: true },
* swipe: { fn: button.onSwipe, scope: button }
* });
*
*/
addListener: function(eventName, fn, scope, options, order) {
return this.changeListener(this.ADD_LISTENER_ACTION, eventName, fn, scope, options, order);
},
addBeforeListener: function(eventName, fn, scope, options) {
return this.addListener(eventName, fn, scope, options, 'before');
},
addAfterListener: function(eventName, fn, scope, options) {
return this.addListener(eventName, fn, scope, options, 'after');
},
/**
* Removes an event handler.
*
* @param {String} eventName The type of event the handler was associated with.
* @param {Function} fn 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. It must be the same as the
* scope argument specified in the original call to {@link #addListener} or the listener will not be removed.
*/
removeListener: function(eventName, fn, scope, options, order) {
return this.changeListener(this.REMOVE_LISTENER_ACTION, eventName, fn, scope, options, order);
},
removeBeforeListener: function(eventName, fn, scope, options) {
return this.removeListener(eventName, fn, scope, options, 'before');
},
removeAfterListener: function(eventName, fn, scope, options) {
return this.removeListener(eventName, fn, scope, options, 'after');
},
/**
* Removes all listeners for this object.
*/
clearListeners: function() {
var usedSelectors = this.getUsedSelectors(),
dispatcher = this.getEventDispatcher(),
i, ln, selector;
for (i = 0,ln = usedSelectors.length; i < ln; i++) {
selector = usedSelectors[i];
dispatcher.clearListeners(this.observableType, selector);
}
},
/**
* 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(eventName) {
return this.getEventDispatcher().hasListener(this.observableType, this.getObservableId(), eventName);
},
/**
* Suspends 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.eventFiringSuspended = true;
},
/**
* Resumes 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() {
this.eventFiringSuspended = false;
},
/**
* Relays selected events from the specified Observable as if the events were fired by <code><b>this</b></code>.
* @param {Object} object The Observable whose events this object is to relay.
* @param {String/Array/Object} events Array of event names to relay.
*/
relayEvents: function(object, events, prefix) {
var i, ln, oldName, newName;
if (typeof prefix == 'undefined') {
prefix = '';
}
if (typeof events == 'string') {
events = [events];
}
if (Ext.isArray(events)) {
for (i = 0,ln = events.length; i < ln; i++) {
oldName = events[i];
newName = prefix + oldName;
object.addListener(oldName, this.createEventRelayer(newName), this);
}
}
else {
for (oldName in events) {
if (events.hasOwnProperty(oldName)) {
newName = prefix + events[oldName];
object.addListener(oldName, this.createEventRelayer(newName), this);
}
}
}
return this;
},
/**
* @private
* Creates an event handling function which refires the event from this object as the passed event name.
* @param newName
* @returns {Function}
*/
createEventRelayer: function(newName){
return function() {
return this.doFireEvent(newName, Array.prototype.slice.call(arguments, 0, -2));
}
},
/**
* 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.
*
* @param {String/String[]} events The event name to bubble, or an Array of event names.
*/
enableBubble: function(events) {
var isBubblingEnabled = this.isBubblingEnabled,
i, ln, name;
if (!isBubblingEnabled) {
isBubblingEnabled = this.isBubblingEnabled = {};
}
if (typeof events == 'string') {
events = Ext.Array.clone(arguments);
}
for (i = 0,ln = events.length; i < ln; i++) {
name = events[i];
if (!isBubblingEnabled[name]) {
isBubblingEnabled[name] = true;
this.addListener(name, this.createEventBubbler(name), this);
}
}
},
createEventBubbler: function(name) {
return function doBubbleEvent() {
var bubbleTarget = ('getBubbleTarget' in this) ? this.getBubbleTarget() : null;
if (bubbleTarget && bubbleTarget !== this && bubbleTarget.isObservable) {
bubbleTarget.fireAction(name, Array.prototype.slice.call(arguments, 0, -2), doBubbleEvent, bubbleTarget, null, 'after');
}
}
},
getBubbleTarget: function() {
return false;
},
destroy: function() {
if (this.observableId) {
this.fireEvent('destroy');
this.clearListeners();
this.clearManagedListeners();
}
},
addEvents: Ext.emptyFn
}, function() {
this.createAlias({
on: 'addListener',
un: 'removeListener',
onBefore: 'addBeforeListener',
onAfter: 'addAfterListener',
unBefore: 'addBeforeListener',
unAfter: 'addAfterListener'
});
//<deprecated product=touch since=2.0>
Ext.deprecateClassMethod(this, 'addEvents', function(){}, "addEvents() is deprecated. It's no longer needed to add events before firing");
Ext.deprecateClassMethod(this, 'addManagedListener', function(object, eventName, fn, scope, options) {
return object.addListener(eventName, fn, scope, options);
}, "addManagedListener() / mon() is deprecated, simply use addListener() / on(). All listeners are now automatically managed where necessary.");
Ext.deprecateClassMethod(this, 'removeManagedListener', function(object, eventName, fn, scope) {
return object.removeListener(eventName, fn, scope);
}, "removeManagedListener() / mun() is deprecated, simply use removeListener() / un(). All listeners are now automatically managed where necessary.");
this.createAlias({
mon: 'addManagedListener',
mun: 'removeManagedListener'
});
//</deprecated>
});
})();