Base.js revision d3966fd264ce4975cf548ce7ed36b723596d4227
/**
* Base class support for objects requiring
* managed attributes and acting as event targets
*
* @module base
*/
var L = Y.Lang,
O = Y.Object,
SEP = ":",
DESTROY = "destroy",
INIT = "init",
INITIALIZED = "initialized",
DESTROYED = "destroyed",
INITIALIZER = "initializer",
DESTRUCTOR = "destructor";
var ETP = Y.Event.Target.prototype;
/**
* <p>
* Provides a base class for managed attribute based
* objects, which handles the chaining of initializer and destructor methods
* across the hierarchy during init and destroy lifecycle methods and
* handles automatic configuration of registered Attributes, through
* the static <a href="#property_ATTRS">ATTRS</a> property.
* </p>
*
* <p>The Base class also handles prefixing of event types with the static <a href="#property_NAME">NAME</a>
* property for all events fired from instances of classes derived from Base.</p>
*
* @constructor
* @class Base
* @uses Attribute
*
* @param {Object} config Object literal of configuration property name/value pairs
*/
var Base = function() {
Y.log('constructor called', 'life', 'base');
Y.Attribute.call(this);
return this.init.apply(this, arguments);
};
/**
* <p>
* Name string to be used to identify instances of
* this class, for example in prefixing events.
* </p>
* <p>
* Classes extending Base, should define their own
* static NAME property.
* </p>
* @property NAME
* @type String
* @static
*/
Base.NAME = 'base';
/**
* Object literal defining the set of attributes which
* will be available for instances of this class, and
* how they are configured. See Attributes addAtt method
* for a description of configuration options available
* for each attribute.
*
* @property ATTRS
* @type Object
* @static
*/
Base.ATTRS = {
/**
* Flag indicating whether or not this object
* has been through the init lifecycle phase.
*
* @attribute initialized
* @readOnly
* @default false
* @type boolean
*/
initialized: {
readOnly:true,
value:false
},
/**
* Flag indicating whether or not this object
* has been through the destroy lifecycle phase.
*
* @attribute destroyed
* @readOnly
* @default false
* @type boolean
*/
destroyed: {
readOnly:true,
value:false
}
};
/**
* The build configuration for the Base class.
* Defines the static fields which need to be aggregated,
* when this class is used as the main class passed to
* the <a href="#method_build">Base.build</a> method.
*
* @property _buildCfg
* @type Object
* @static
* @final
* @private
*/
Base._buildCfg = {
aggregates : ["ATTRS"]
};
var _instances = {};
/**
* <p>
* Builds a constructor function (class) from the
* main function, and array of extension functions (classes)
* provided.
* </p>
* <p>
* The cfg object literal supports the following properties
* </p>
* <dl>
* <dt>dynamic &#60;boolean&#62;</dt>
* <dd>
* <p>If true (default), a completely new class
* is created which extends the main class, and acts as the
* host on which the extension classes are augmented.</p>
* <p>If false, the extensions classes are augmented directly to
* the main class, modifying the main classes prototype.</p>
* </dd>
* <dt>aggregates &#60;String[]&#62;</dt>
* <dd>An array of static property names, which will get aggregated
* on to the built class, in addition to the default properties build
* will always aggregate as defined by the main class' _buildCfg
* property.
* </dd>
* </dl>
*
* @method build
* @static
* @param {Function} main The main class on which to base the built class
* @param {Function[]} extensions The set of extension classes which will be
* augmented/aggregated to the built class.
* @param {Object} cfg
* @return {Function} A custom class, created from the provided main and extension classes
*/
Base.build = function(main, extensions, cfg) {
var build = Base.build,
builtClass = build._getClass(main, cfg),
aggregates = build._getAggregates(main, cfg),
dynamic = builtClass._yuibuild.dynamic,
key = main.NAME,
i, l;
// Shallow isolate aggregates
if (dynamic) {
if (aggregates) {
for (i = 0, l = aggregates.length; i < l; ++i) {
var val = aggregates[i];
if (main.hasOwnProperty(val)) {
builtClass[val] = L.isArray(main[val]) ? [] : {};
}
}
Y.aggregate(builtClass, main, true, aggregates);
}
}
// Augment/Aggregate
for (i = 0, l = extensions.length; i < l; i++) {
var extClass = extensions[i];
if (aggregates) {
Y.aggregate(builtClass, extClass, true, aggregates);
}
// Old augment
Y.mix(builtClass, extClass, true, null, 1);
builtClass._yuibuild.exts.push(extClass);
key = key + ":" + Y.stamp(extClass);
}
builtClass._yuibuild.id = key;
builtClass.prototype.hasImpl = build._hasImpl;
if (dynamic) {
builtClass.NAME = main.NAME;
builtClass.prototype.constructor = builtClass;
}
return builtClass;
};
Y.mix(Base.build, {
_template: function(main) {
function BuiltClass() {
BuiltClass.superclass.constructor.apply(this, arguments);
var f = BuiltClass._yuibuild.exts,
l = f.length;
for (var i = 0; i < l; i++) {
f[i].apply(this, arguments);
}
return this;
}
Y.extend(BuiltClass, main);
return BuiltClass;
},
_hasImpl : function(extClass) {
if (this.constructor._yuibuild) {
var f = this.constructor._yuibuild.exts,
l = f.length,
i;
for (i = 0; i < l; i++) {
if (f[i] === extClass) {
return true;
}
}
}
return false;
},
_getClass : function(main, cfg) {
// Create dynamic class or just modify main class
var dynamic = (cfg && false === cfg.dynamic) ? false : true,
builtClass = (dynamic) ? Base.build._template(main) : main;
builtClass._yuibuild = {
id: null,
exts : [],
dynamic : dynamic
};
return builtClass;
},
_getAggregates : function(main, cfg) {
var aggr = [],
cfgAggr = (cfg && cfg.aggregates),
c = main;
while (c && c.prototype) {
var classAggr = c._buildCfg && c._buildCfg.aggregates;
if (classAggr) {
aggr = aggr.concat(classAggr);
}
c = c.superclass ? c.superclass.constructor : null;
}
if (cfgAggr) {
aggr = aggr.concat(cfgAggr);
}
return aggr;
}
});
/**
* <p>
* Creates a new object instance, based on a dynamically created custom class.
* The custom class is created from the main class passed in as the first parameter
* along with the list of extension classes passed in
* as the second parameter using <a href="#method_build">Base.build</a>
* with "dynamic" set to true. See the documentation for Base.build method
* to see how the main class and extension classes are used.
* </p>
*
* <p>Any arguments following the 2nd argument are passed as arguments to the
* constructor of the newly created class used to create the instance.</p>
*
* @method create
* @static
*
* @param {Function} main The main class on which the instance it to be
* based. This class will be extended to create the class for the custom instance
* @param {Array} extensions The list of extension classes used to augment the
* main class with.
* @param {Any*} args* 0..n arguments to pass to the constructor of the
* newly created class, when creating the instance.
* @return {Object} An instance of the custom class
*/
Base.prototype = {
/**
* Init lifecycle method, invoked during construction.
* Fires the init event prior to invoking initializers on
* the class hierarchy.
*
* @method init
* @final
* @chainable
* @param {Object} config Object literal of configuration property name/value pairs
* @return {Base} A reference to this object
*/
init: function(config) {
Y.log('init called', 'life', 'base');
/**
* The name string to be used to identify
* this instance of object.
* @property name
* @type String
*/
this.name = this.constructor.NAME;
/**
* <p>
* Lifecycle event for the init phase, fired prior to initialization.
* Invoking the preventDefault method on the event object provided
* to subscribers will prevent initialization from occuring.
* </p>
* <p>
* Subscribers to the "after" momemt of this event, will be notified
* after initialization of the object is complete (and therefore
* cannot prevent initialization).
* </p>
*
* @event init
* @preventable _defInitFn
* @param {Event.Facade} e Event object
* @param config Object literal of configuration name/value pairs
*/
this.publish(INIT, {
queuable:false,
defaultFn:this._defInitFn
});
this.fire(INIT, null, config);
return this;
},
/**
* <p>
* Destroy lifecycle method. Fires the destroy
* event, prior to invoking destructors for the
* class hierarchy.
* </p>
* <p>
* Subscribers to the destroy
* event can invoke preventDefault on the event object, to prevent destruction
* from proceeding.
* </p>
* @method destroy
* @return {Base} A reference to this object
* @final
* @chainable
*/
destroy: function() {
Y.log('destroy called', 'life', 'base');
/**
* <p>
* Lifecycle event for the destroy phase,
* fired prior to destruction. Invoking the preventDefault
* method on the event object provided to subscribers will
* prevent destruction from proceeding.
* </p>
* <p>
* Subscribers to the "after" moment of this event, will be notified
* after destruction is complete (and as a result cannot prevent
* destruction).
* </p>
* @event destroy
* @preventable _defDestroyFn
* @param {Event.Facade} e Event object
*/
this.publish(DESTROY, {
queuable:false,
defaultFn: this._defDestroyFn
});
this.fire(DESTROY);
return this;
},
/**
* Default init event handler
*
* @method _defInitFn
* @param {Event.Facade} e Event object
* @param {Object} config Object literal of configuration property name/value pairs
* @protected
*/
_defInitFn : function(e, config) {
_instances[Y.stamp(this)] = this;
this._initHierarchy(config);
this._set(INITIALIZED, true);
},
/**
* Default destroy event handler
*
* @method _defDestroyFn
* @param {Event.Facade} e Event object
* @protected
*/
_defDestroyFn : function(e) {
this._destroyHierarchy();
delete _instances[this._yuid];
this._set(DESTROYED, true);
},
/**
* Returns the top down class hierarchy for this object,
* with Base being the first class in the array.
*
* @method _getClasses
* @protected
* @return {Function[]} An Array of classes (constructor functions), making up the class hierarchy for this object
*/
_getClasses : function() {
if (!this._classes) {
this._initHierarchyData();
}
return this._classes.concat();
},
/**
* Returns an aggregated set of attribute configurations, by traversing the class hierarchy.
*
* @method _getAttrCfgs
* @protected
* @return {Object} The hash of attribute configurations, aggregated across classes in the hierarchy
*/
_getAttrCfgs : function() {
if (!this._attrs) {
this._initHierarchyData();
}
return Y.merge(this._attrs);
},
_filterAttrCfgs : function(clazz, allCfgs) {
var cfgs = {};
if (clazz.ATTRS) {
Y.each(clazz.ATTRS, function(v, k) {
if (allCfgs[k]) {
cfgs[k] = allCfgs[k];
delete allCfgs[k];
}
});
}
return cfgs;
},
_initHierarchyData : function() {
var c = this.constructor,
classes = [],
attrs = {};
while (c && c.prototype) {
// Add to classes
classes.unshift(c);
// Add to attributes
if (c.ATTRS) {
this._aggregateAttrs(attrs, c.ATTRS, false);
}
c = c.superclass ? c.superclass.constructor : null;
}
this._classes = classes;
this._attrs = attrs;
},
_aggregateAttrs : function(r, s, ov) {
Y.each(s, function(v, k) {
if (!r[k]) {
r[k] = v;
} else {
r[k] = Y.mix(r[k], v, ov);
}
});
},
/**
* Initializes the class hierarchy rooted at this base class,
* which includes initializing attributes for each class defined
* in the class's static <a href="#property_ATTRS">ATTRS</a> property and invoking the initializer
* method on the prototype of each class in the hierarchy.
*
* @method _initHierarchy
* @param {Object} userConf Object literal containing attribute name/value pairs
* @private
*/
_initHierarchy : function(userConf) {
var constr,
classes = this._getClasses(),
mergedCfgs = this._getAttrCfgs();
for (var ci = 0, cl = classes.length; ci < cl; ci++) {
constr = classes[ci];
this._initAttrs(this._filterAttrCfgs(constr, mergedCfgs), userConf);
if (O.owns(constr.prototype, INITIALIZER)) {
constr.prototype[INITIALIZER].apply(this, arguments);
}
}
},
/**
* Destroys the class hierarchy rooted at this base class by invoking
* the descructor method on the prototype of each class in the hierarchy.
*
* @method _destroyHierarchy
* @private
*/
_destroyHierarchy : function() {
var constr,
classes = this._getClasses();
for (var ci = classes.length-1; ci >= 0; ci--) {
constr = classes[ci];
if (O.owns(constr.prototype, DESTRUCTOR)) {
constr.prototype[DESTRUCTOR].apply(this, arguments);
}
}
},
/**
* Default toString implementation. Provides the constructor NAME
* and the instance ID.
*
* @method toString
* @return {String} String representation for this object
*/
toString: function() {
return this.constructor.NAME + "[" + Y.stamp(this) + "]";
},
/**
* <p>
* Subscribe to a custom event hosted by this object.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_subscribe">subscribe</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
*
* @method subscribe
* @param {String} type The type of event to subscribe to. If
* the type string does not contain a prefix ("prefix:eventType"),
* the name property of the instance will be used as the default prefix.
* @param {Function} fn The subscribed callback function, invoked when the event is fired.
* @param {Object} context Optional execution context for the callback.
* @param {Any*} args* 0..n params to supply to the callback
*
* @return {Event.Handle} An event handle which can be used to unsubscribe the subscribed callback.
*/
subscribe : function() {
var a = arguments;
a[0] = this._prefixEvtType(a[0]);
return ETP.subscribe.apply(this, a);
},
/**
* <p>
* Fire a custom event by name. The callback functions will be executed
* from the context specified when the event was created, and with the
* following parameters.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_fire">fire</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
*
* @method fire
* @param {String|Object} type The type of the event, or an object that contains
* a 'type' property. If the type does not contain a prefix ("prefix:eventType"),
* the name property of the instance will be used as the default prefix.
* @param {Any*} args* 0..n Additional arguments to pass to subscribers.
* @return {boolean} The return value from Event Target's <a href="Event.Target.html#method_fire">fire</a> method.
*
*/
fire : function() {
var a = arguments;
if (L.isString(a[0])) {
a[0] = this._prefixEvtType(a[0]);
} else if (a[0].type){
a[0].type = this._prefixEvtType(a[0].type);
}
return ETP.fire.apply(this, a);
},
/**
* <p>
* Creates a new custom event of the specified type. If a custom event
* by that name already exists, it will not be re-created. In either
* case the custom event is returned.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_publish">publish</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
*
* @method publish
* @param {String} type The type, or name of the event. If the type does not
* contain a prefix ("prefix:eventType"), the name property of the instance will
* be used as the default prefix.
* @param {Object} opts Optional config params (see Event.Target <a href="Event.Target.html#method_publish">publish</a> for details)
* @return {Event.Custom} The published custom event object
*/
publish : function() {
var a = arguments;
a[0] = this._prefixEvtType(a[0]);
return ETP.publish.apply(this, a);
},
/**
* <p>
* Subscribe to a custom event hosted by this object. The
* supplied callback will execute <em>after</em> any listeners added
* via the subscribe method, and after the default function,
* if configured for the event, has executed.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_after">after</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
* @method after
* @param {String} type The type of event to subscribe to. If
* the type string does not contain a prefix ("prefix:eventType"),
* the name property of the instance will be used as the default prefix.
* @param {Function} fn The subscribed callback function
* @param {Object} context Optional execution context for the callback
* @param {Any*} args* 0..n params to supply to the callback
* @return {Event.Handle} Event handle which can be used to unsubscribe the subscribed callback.
*/
after : function() {
var a = arguments;
a[0] = this._prefixEvtType(a[0]);
return ETP.after.apply(this, a);
},
/**
* <p>
* Unsubscribes one or more listeners the from the specified event.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_unsubscribe">unsubscribe</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
* @method unsubscribe
* @param {String|Object} type Either the handle to the subscriber or the
* type of event. If the type
* is not specified, it will attempt to remove
* the listener from all hosted events. If
* the type string does not contain a prefix
* ("prefix:eventType"), the name property of the
* instance will be used as the default prefix.
* @param {Function} fn The subscribed function to unsubscribe, if not
* supplied, all subscribers will be removed.
* @param {Object} context The custom object passed to subscribe. This is
* optional, but if supplied will be used to
* disambiguate multiple listeners that are the same
* (e.g., you subscribe many object using a function
* that lives on the prototype)
* @return {boolean} true if the subscriber was found and detached.
*/
unsubscribe: function(type, fn, context) {
var a = arguments;
if (L.isString(a[0])) {
a[0] = this._prefixEvtType(a[0]);
}
return ETP.unsubscribe.apply(this, a);
},
/**
* <p>
* Removes all listeners from the specified event. If the event type
* is not specified, all listeners from all hosted custom events will
* be removed.
* </p>
* <p>
* Overrides Event.Target's <a href="Event.Target.html#method_unsubscribeAll">unsubscribeAll</a> method, to add the name prefix
* of the instance to the event type, if absent.
* </p>
* @method unsubscribeAll
* @param {String} type The type, or name of the event. If
* the type string does not contain a prefix ("prefix:eventType"),
* the name property of the instance will be used as the default prefix
* @return {int} The number of listeners unsubscribed
*/
unsubscribeAll: function(type) {
var a = arguments;
a[0] = this._prefixEvtType(a[0]);
return ETP.unsubscribeAll.apply(this, a);
},
/**
* Utility method to prefix the event name with the
* name property of the instance, if absent
*
* @method _prefixEvtType
* @private
* @param {String} type The event name
* @return {String} The prefixed event name
*/
_prefixEvtType: function(type) {
if (type.indexOf(SEP) === -1 && this.name) {
type = this.name + ":" + type;
}
return type;
}
};
Y.mix(Base, Y.Attribute, false, null, 1);
Base.prototype.constructor = Base;
Y.Base = Base;