Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

697 lines
27 KiB

/**
* Ext.Widget is a light-weight Component that consists of nothing more than a template
* Element that can be cloned to quickly and efficiently replicate many instances.
* Ext.Widget is typically not instantiated directly, because the default template is
* just a single element with no listeners. Instead Ext.Widget should be extended to
* create Widgets that have a useful markup structure and event listeners.
*
* For example:
*
* Ext.define('MyWidget', {
* extend: 'Ext.Widget',
*
* // The element template passed to Ext.Element.create()
* element: {
* reference: 'element',
* listeners: {
* click: 'onClick'
* },
* children: [{
* reference: 'innerElement',
* listeners: {
* click: 'onInnerClick'
* }
* }]
* },
*
* constructor: function(config) {
* // It is important to remember to call the Widget superclass constructor
* // when overriding the constructor in a derived class. This ensures that
* // the element is initialized from the template, and that initConfig() is
* // is called.
* this.callParent([config]);
*
* // After calling the superclass constructor, the Element is available and
* // can safely be manipulated. Reference Elements are instances of
* // Ext.Element, and are cached on each Widget instance by reference name.
* Ext.getBody().appendChild(this.element);
* },
*
* onClick: function() {
* // listeners use this Widget instance as their scope
* console.log('element clicked', this);
* },
*
* onInnerClick: function() {
* // access the innerElement reference by name
* console.log('inner element clicked', this.innerElement);
* }
* });
*
* @since 5.0.0
*/
Ext.define('Ext.Widget', {
extend: 'Ext.Evented',
xtype: 'widget',
requires: [
'Ext.dom.Element'
],
mixins: [
'Ext.mixin.Inheritable',
'Ext.mixin.Bindable'
],
isWidget: true,
/**
* @property {Object} element
* A configuration object for Ext.Element.create() that is used to create the Element
* template. Supports all the standard options of a Ext.Element.create() config and
* adds 2 additional options:
*
* 1. `reference` - this option specifies a name for Element references. These
* references names become properties of the Widget instance and refer to Ext.Element
* instances that were created using the template:
*
* element: {
* reference: 'element',
* children: [{
* reference: 'innerElement'
* }]
* }
*
* After construction of a widget the reference elements are accessible as follows:
*
* var foo = new FooWidget(),
* innerEl = foo.innerEl; // an Ext.Element that wraps the innerElement
*
* The reference attribute is optional, but all Widgets must have a `'element'`
* reference on some element within the template (usually the outermost one).
*
* 2. `listeners` - a standard listeners object as specified by {@link
* Ext.mixin.Observable}.
*
* element: {
* reference: 'element',
* listeners: {
* click: 'onClick'
* },
* children: [{
* reference: 'innerElement',
* listeners: {
* click: 'onInnerClick'
* }
* }]
* }
*
* Since listeners cannot be attached without an Ext.Element reference the `reference`
* property MUST be specified in order to use `listeners`.
*
* The Widget instance is used as the scope for all listeners specified in this way,
* so it is invalid to use the `scope` option in the `listeners` config since it will
* always be overwritten using `this`.
* @protected
*/
element: {
reference: 'element'
},
observableType: 'component',
eventedConfig: {
/**
* @cfg {Number/String} width
* The width of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* By default, if this is not explicitly set, this Component's element will simply have its own natural size.
* If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
* @accessor
* @evented
*/
width: null,
/**
* @cfg {Number/String} height
* The height of this Component; must be a valid CSS length value, e.g: `300`, `100px`, `30%`, etc.
* By default, if this is not explicitly set, this Component's element will simply have its own natural size.
* If set to `auto`, it will set the width to `null` meaning it will have its own natural size.
* @accessor
* @evented
*/
height: null
},
/**
* @property {Array} template
* An array of child elements to use as the children of the main element in the {@link
* #element} template. Only used if "children" are not specified explicitly in the
* {@link #element} template.
* @protected
*/
template: [],
constructor: function(config) {
var me = this;
me.initId(config);
me.initElement();
me.mixins.observable.constructor.call(me, config);
Ext.ComponentManager.register(me);
},
afterCachedConfig: function() {
// This method runs once for the first instance of this Widget type that is
// created. It runs after the element config has been processed for the first
// instance, and after all the cachedConfigs (whose appliers/updaters may modify
// the element) have been initialized. At this point we are ready to take the
// DOM that was generated for the first Element instance, clone it, and cache it
// on the prototype, so that it can be cloned by future instance to create their
// elements (see initElement).
var me = this,
prototype = me.self.prototype,
referenceList = me.referenceList,
renderElement = me.renderElement.dom,
renderTemplate, element, i, ln, reference, elements;
// This is where we take the first instance's DOM and clone it as the template
// for future instances
prototype.renderTemplate = renderTemplate = document.createDocumentFragment();
renderTemplate.appendChild(renderElement.cloneNode(true));
elements = renderTemplate.querySelectorAll('[id]');
for (i = 0,ln = elements.length; i < ln; i++) {
element = elements[i];
element.removeAttribute('id');
}
// initElement skips removal of reference attributes for the first instance so that
// the reference attributes will be present in the cached element when it is cloned.
// Now that we're done cloning and caching the template element, it is safe to
// remove the reference attributes from this instance's elements
for (i = 0,ln = referenceList.length; i < ln; i++) {
reference = referenceList[i];
me[reference].dom.removeAttribute('reference');
}
},
applyWidth: function(width) {
return this.filterLengthValue(width);
},
applyHeight: function(height) {
return this.filterLengthValue(height);
},
destroy: function() {
var me = this,
referenceList = me.referenceList,
i, ln, reference;
// Destroy all element references
for (i = 0, ln = referenceList.length; i < ln; i++) {
reference = referenceList[i];
if (me.hasOwnProperty(reference)) {
me[reference].destroy();
me[reference] = null;
}
}
me.callParent();
Ext.ComponentManager.unregister(me);
},
/**
* @param width
* @protected
*/
doSetWidth: function(width) {
this.element.setWidth(width);
},
/**
* @param height
* @protected
*/
doSetHeight: function(height) {
this.element.setHeight(height);
},
/**
* A template method for modifying the {@link #element} config before it is processed.
* By default adds the result of `this.getTemplate()` as the `children` array of {@link
* #element} if `children` were not specified in the original {@link #element} config.
* Typically this method should not need to be implemented in subclasses. Instead the
* {@link #element} property should be use to configure the element template for a
* given Widget subclass.
*
* This method is called once when the first instance of each Widget subclass is
* created. The element config object that is returned is cached and used as the template
* for all successive instances. The scope object for this method is the class prototype,
* not the instance.
*
* @return {Object} the element config object
* @protected
*/
getElementConfig: function() {
var me = this,
el = me.element;
if (!('children' in el)) {
el = Ext.apply({
children: me.getTemplate()
}, el);
}
return el;
},
/**
* Returns the value of {@link Ext.Component#itemId} assigned to this component, or when that
* is not set, returns the value of {@link Ext.Component#id}.
* @return {String}
*/
getItemId: function() {
// needed by ComponentQuery
return this.itemId || this.id;
},
/**
* Returns the height and width of the Component.
* @return {Object} The current `height` and `width` of the Component.
* @return {Number} return.width
* @return {Number} return.height
*/
getSize: function() {
return {
width: this.getWidth(),
height: this.getHeight()
};
},
getTemplate: function() {
return this.template;
},
/**
* Initializes the Element for this Widget instance. If this is the first time a
* Widget of this type has been instantiated the {@link #element} config will be
* processed to create an Element. This Element is then cached on the prototype (see
* afterCachedConfig) so that future instances can obtain their element by simply
* cloning the Element that was cached by the first instance.
* @protected
*/
initElement: function() {
var me = this,
prototype = me.self.prototype,
id = me.getId(),
referenceList = me.referenceList = [],
cleanAttributes = true,
renderTemplate, renderElement, element, referenceNodes, i, ln, referenceNode,
reference;
if (prototype.hasOwnProperty('renderTemplate')) {
// we have already created an instance of this Widget type, so the element
// config has already been processed, and the resulting DOM has been cached on
// the prototype (see afterCachedConfig). This means we can obtain our element
// by simply cloning the cached element.
renderTemplate = me.renderTemplate.cloneNode(true);
renderElement = renderTemplate.firstChild;
} else {
// this is the first instantiation of this widget type. Process the element
// config from scratch to create our Element.
cleanAttributes = false;
renderTemplate = document.createDocumentFragment();
renderElement = Ext.Element.create(me.processElementConfig.call(prototype), true);
renderTemplate.appendChild(renderElement);
}
referenceNodes = renderTemplate.querySelectorAll('[reference]');
for (i = 0,ln = referenceNodes.length; i < ln; i++) {
referenceNode = referenceNodes[i];
reference = referenceNode.getAttribute('reference');
if (cleanAttributes) {
// on first instantiation we do not clean the reference attributes here.
// This is because this instance's element will be used as the template
// for future instances, and we need the reference attributes to be
// present in the template so that future instances can resolve their
// references. afterCachedConfig is responsible for removing the
// reference attributes from the DOM for the first instance after the
// Element has been cloned and cached as the template.
referenceNode.removeAttribute('reference');
}
if (reference === 'element') {
//<debug>
if (element) {
// already resolved a reference named element - can't have two
Ext.Error.raise("Duplicate 'element' reference detected in '" +
me.$className + "' template.");
}
//</debug>
referenceNode.id = id;
// element reference needs to be established ASAP, so add the reference
// immediately, not "on-demand"
element = me.el = me.addElementReference(reference, referenceNode);
} else {
me.addElementReferenceOnDemand(reference, referenceNode);
}
referenceList.push(reference);
}
//<debug>
if (!element) {
Ext.Error.raise("No 'element' reference found in '" + me.$className +
"' template.");
}
//</debug>
if (renderElement === element.dom) {
me.renderElement = element;
}
else {
me.addElementReferenceOnDemand('renderElement', renderElement);
}
},
/**
* Tests whether this Widget matches a {@link Ext.ComponentQuery ComponentQuery}
* selector string.
* @param {String} selector The selector string to test against.
* @return {Boolean} `true` if this Widget matches the selector.
*/
is: function(selector) {
return Ext.ComponentQuery.is(this, selector);
},
/**
* Tests whether or not this Component is of a specific xtype. This can test whether this Component is descended
* from the xtype (default) or whether it is directly of the xtype specified (`shallow = true`).
* **If using your own subclasses, be aware that a Component must register its own xtype
* to participate in determination of inherited xtypes.__
*
* For a list of all available xtypes, see the {@link Ext.Component} header.
*
* Example usage:
*
* var t = new Ext.field.Text();
* var isText = t.isXType('textfield'); // true
* var isBoxSubclass = t.isXType('field'); // true, descended from Ext.field.Field
* var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.field.Field instance
*
* @param {String} xtype The xtype to check for this Component.
* @param {Boolean} shallow (optional) `false` to check whether this Component is descended from the xtype (this is
* the default), or `true` to check whether this Component is directly of the specified xtype.
* @return {Boolean} `true` if this component descends from the specified xtype, `false` otherwise.
*/
isXType: function(xtype, shallow) {
return shallow ? (Ext.Array.indexOf(this.xtypes, xtype) !== -1) :
!!this.xtypesMap[xtype];
},
resolveListenerScope: function(defaultType) {
// break the tie between Observable and Inheritable resolveListenerScope
return this.mixins.inheritable.resolveListenerScope.call(this, defaultType);
},
/**
* Sets the size of the Component.
* @param {Number} width The new width for the Component.
* @param {Number} height The new height for the Component.
*/
setSize: function(width, height) {
if (width !== undefined) {
this.setWidth(width);
}
if (height !== undefined) {
this.setHeight(height);
}
},
//-------------------------------------------------------------------------
privates: {
/**
* Reduces instantiation time for a Widget by lazily instantiating Ext.Element
* references the first time they are used. This optimization only works for elements
* with no listeners specified.
*
* @param {String} name The name of the reference
* @param {HTMLElement} domNode
* @private
*/
addElementReferenceOnDemand: function(name, domNode) {
if (this._elementListeners[name]) {
// if the element was configured with listeners then we cannot add the
// reference on demand because we need to make sure the element responds
// immediately to any events, even if its reference is never accessed
this.addElementReference(name, domNode);
} else {
// no listeners - element reference can be resolved on demand.
// TODO: measure if this has any significant performance impact.
Ext.Object.defineProperty(this, name, {
get: function() {
// remove the property that was defined using defineProperty because
// addElementReference will set the property on the instance, - the
// getter is not needed after the first access.
delete this[name];
return this.addElementReference(name, domNode);
},
configurable: true
});
}
},
/**
* Adds an element reference to this Widget instance.
* @param {String} name The name of the reference
* @param {HTMLElement} domNode
* @return {Ext.dom.Element}
* @private
*/
addElementReference: function (name, domNode) {
var me = this,
referenceEl = me[name] = Ext.get(domNode),
listeners = me._elementListeners[name],
eventName, listener;
referenceEl.skipGarbageCollection = true;
referenceEl.component = me;
if (listeners) {
// TODO: these references will be needed when we use delegation to listen
// for element events, but for now, we'll just attach the listeners directly
// referenceEl.reference = name;
// referenceEl.component = me;
// referenceEl.listeners = listeners;
// at this point "listeners" exists on the class prototype. We need to clone
// it before poking the scope reference onto it, because it is used as the
// options object by Observable and so can't be safely shared.
listeners = Ext.clone(listeners);
// the outermost listeners object always needs the scope option. this covers
// a listeners object with the following shape
//
// {
// click: 'onClick'
// scope: this
// }
listeners.scope = me;
// if the listener is specified as an object it needs to have the scope
// option added to that object, for example:
//
// {
// click: {
// fn: 'onClick',
// scope: this
// }
// }
for (eventName in listeners) {
listener = listeners[eventName];
if (typeof listener === 'object') {
listener.scope = me;
}
}
// hopefully in the future we can stop calling on() here, and just use
// event delegation to dispatch events to Widgets that have declared their
// listeners in their template
referenceEl.on(listeners);
}
return referenceEl;
},
detachFromBody: function() {
// See reattachToBody
Ext.getDetachedBody().appendChild(this.element);
this.isDetached = true;
},
//@private
doAddListener: function(name, fn, scope, options, order, caller, manager) {
if (options && 'element' in options) {
//<debug>
if (this.referenceList.indexOf(options.element) === -1) {
Ext.Logger.error("Adding event listener with an invalid element reference of '" + options.element +
"' for this component. Available values are: '" + this.referenceList.join("', '") + "'", this);
}
//</debug>
// The default scope is this component
this[options.element].doAddListener(name, fn, scope || this, options, order);
}
this.callParent([name, fn, scope, options, order, caller, manager]);
},
filterLengthValue: function(value) {
if (value === 'auto' || (!value && value !== 0)) {
return null;
}
return value;
},
getFocusEl: function () {
return this.element;
},
/**
* Called for the first instance of this Widget to create an object that contains the
* listener configs for all of the element references keyed by reference name. The
* object is cached on the prototype and has the following shape:
*
* _elementListeners: {
* element: {
* click: 'onClick',
* scope: this
* },
* fooReference: {
* tap: {
* fn: someFunction,
* delay: 100
* }
* }
* }
*
* The returned object is prototype chained to the _elementListeners object of its
* superclass, and each key in the object is prototype chained to object with the
* corresponding key in the superclass _elementListeners. This allows element
* listeners to be inherited and overridden when subclassing widgets.
*
* This method is invoked with the prototype object as the scope
*
* @private
*/
initElementListeners: function(elementConfig) {
var prototype = this,
superPrototype = prototype.self.superclass,
superElementListeners = superPrototype._elementListeners,
reference = elementConfig.reference,
children = elementConfig.children,
elementListeners, listeners, superListeners, ln, i;
if (prototype.hasOwnProperty('_elementListeners')) {
elementListeners = prototype._elementListeners;
} else {
elementListeners = prototype._elementListeners =
(superElementListeners ? Ext.Object.chain(superElementListeners) : {});
}
if (reference) {
listeners = elementConfig.listeners;
if (listeners) {
if (superElementListeners) {
superListeners = superElementListeners[reference];
if (superListeners) {
listeners = Ext.Object.chain(superListeners);
Ext.apply(listeners, elementConfig.listeners);
}
}
elementListeners[reference] = listeners;
// null out the listeners on the elementConfig, since we are going to pass
// it to Element.create(), and don't want "listeners" to be treated as an
// attribute
elementConfig.listeners = null;
}
}
if (children) {
for (i = 0, ln = children.length; i < ln; i++) {
prototype.initElementListeners(children[i]);
}
}
},
initId: function(config) {
var me = this,
defaultConfig = me.config,
id = (config && config.id) || (defaultConfig && defaultConfig.id);
if (id) {
// setId() will normally be inherited from Identifiable, unless "id" is a
// proper config, in which case it will be generated by the config system.
me.setId(id);
me.id = id;
} else {
// if no id configured, generate one (Identifiable)
me.getId();
}
},
/**
* Recursively processes the element templates for this class and its superclasses,
* ascending the hierarchy until it reaches a superclass whose element template
* has already been processed. This method is invoked using the prototype as the scope.
*
* @private
* @return {Object}
*/
processElementConfig: function() {
var prototype = this,
superPrototype = prototype.self.superclass,
elementConfig;
if (prototype.hasOwnProperty('_elementConfig')) {
elementConfig = prototype._elementConfig;
} else {
// cache the elementConfig on the prototype, since we may end up here multiple
// times if there are multiple subclasses
elementConfig = prototype._elementConfig = prototype.getElementConfig();
if (superPrototype.isWidget) {
// Before initializing element listeners we must process the element template
// for our superclass so that we can chain our listeners to the superclass listeners
prototype.processElementConfig.call(superPrototype);
}
// initElementListeners needs to be called BEFORE passing the element config
// along to Ext.Element.create(). This ensures that the listener meta data is
// saved, and then the listeners objects are removed from the element config
// so that they do not get added as attributes by create()
prototype.initElementListeners(elementConfig);
}
return elementConfig;
},
reattachToBody: function() {
// See detachFromBody
this.isDetached = false;
}
}
}, function(Widget) {
// event options for listeners that use the "element" event options must also include
// event options from Ext.Element
(Widget.prototype.$elementEventOptions =
Ext.Object.chain(Ext.Element.prototype.$eventOptions)).element = 1;
});