/** * @class Ext.Configurator * This class manages the config properties for a class. * @private */(function () { // see end of file (and please don't indent the whole file) var ExtConfig = Ext.Config, configPropMap = ExtConfig.map, ExtObject = Ext.Object; Ext.Configurator = function (cls) {// @define Ext.class.Configurator // @define Ext.Configurator // @require Ext.Config var me = this, prototype = cls.prototype, superCfg = cls.superclass ? cls.superclass.self.$config : null; /** * @property {Ext.Class} cls The class to which this instance is associated. * @private * @readonly */ me.cls = cls; /** * The super class `Configurator` instance or `null` if there is no super class. * * @property {Ext.Configurator} superCfg * @private * @readonly */ me.superCfg = superCfg; if (superCfg) { /** * This object holds an `Ext.Config` value for each config property keyed by name. * This object has as its prototype object the `configs` of its super class. * * This map is maintained as each property is added via the `add` method. * * @property {Object} configs * @private * @readonly */ me.configs = ExtObject.chain(superCfg.configs); /** * This object holds a bool value for each cachedConfig property keyed by name. * * This map is maintained as each property is added via the `add` method. * * @property {Object} cachedConfigs * @private * @readonly */ me.cachedConfigs = ExtObject.chain(superCfg.cachedConfigs); /** * This object holds a `Number` for each config property keyed by name. This object has * as its prototype object the `initMap` of its super class. The value of each property * has the following meaning: * * * `0` - initial value is `null` and requires no processing. * * `1` - initial value must be set on each instance. * * `2` - initial value can be cached on the prototype by the first instance. * * Any `null` values will either never be added to this map or (if added by a base * class and set to `null` by a derived class) will cause the entry to be 0. * * This map is maintained as each property is added via the `add` method. * * @property {Object} initMap * @private * @readonly */ me.initMap = ExtObject.chain(superCfg.initMap); /** * This object holds the default value for each config property keyed by name. This * object has as its prototype object the `values` of its super class. * * This map is maintained as each property is added via the `add` method. * * @property {Object} values * @private * @readonly */ me.values = ExtObject.chain(superCfg.values); me.needsFork = superCfg.needsFork; //<debug> // The reason this feature is debug only is that we would have to create this // map for all classes because deprecations could be added to bases after the // derived class had created its Configurator. me.deprecations = ExtObject.chain(superCfg.deprecations); //</debug> } else { me.configs = {}; me.cachedConfigs = {}; me.initMap = {}; me.values = {}; //<debug> me.deprecations = {}; //</debug> } prototype.config = prototype.defaultConfig = me.values; cls.$config = me;}; Ext.Configurator.prototype = { self: Ext.Configurator, needsFork: false, /** * This array holds the properties that need to be set on new instances. * * This array is populated when the first instance is passed to `configure` (basically * when the first instance is created). The entries in `initMap` are iterated to find * those configs needing per-instance processing. * * @property {Ext.Config[]} initList * @private */ initList: null, /** * This method adds new config properties. This is called for classes when they are * declared, then for any mixins that class may define and finally for any overrides * defined that target the class. * * @param {Object} config The config object containing the new config properties. * @param {Ext.Class} [mixinClass] The mixin class if the configs are from a mixin. * @private */ add: function (config, mixinClass) { var me = this, Cls = me.cls, configs = me.configs, cachedConfigs = me.cachedConfigs, initMap = me.initMap, prototype = Cls.prototype, mixinConfigs = mixinClass && mixinClass.$config.configs, values = me.values, isObject, meta, isCached, merge, cfg, currentValue, name, names, s, value; for (name in config) { value = config[name]; isObject = value && value.constructor === Object; meta = isObject && '$value' in value ? value : null; isCached = false; if (meta) { isCached = !!meta.cached; value = meta.$value; isObject = value && value.constructor === Object; } merge = meta && meta.merge; cfg = configs[name]; if (cfg) { // Only proceed with a mixin if we have a custom merge. if (mixinClass) { merge = cfg.merge; if (!merge) { continue; } // Don't want the mixin meta modifying our own meta = null; } else { merge = merge || cfg.merge; } //<debug> // This means that we've already declared this as a config in a superclass // Let's not allow us to change it here. if (!mixinClass && isCached && !cachedConfigs[name]) { Ext.raise('Redefining config as cached: ' + name + ' in class: ' + Cls.$className); } //</debug> // There is already a value for this config and we are not allowed to // modify it. So, if it is an object and the new value is also an object, // the result is a merge so we have to merge both on to a new object. currentValue = values[name]; if (merge) { value = merge.call(cfg, value, currentValue, Cls, mixinClass); } else if (isObject) { if (currentValue && currentValue.constructor === Object) { // We favor moving the cost of an "extra" copy here because this // is likely to be a rare thing two object values for the same // property. The alternative would be to clone the initial value // to make it safely modifiable even though it is likely to never // need to be modified. value = ExtObject.merge({}, currentValue, value); } // else "currentValue" is a primitive so "value" can just replace it } // else "value" is a primitive and it can just replace currentValue } else { // This is a new property value, so add it to the various maps "as is". // In the majority of cases this value will not be overridden or need to // be forked. if (mixinConfigs) { // Since this is a config from a mixin, we don't want to apply its // meta-ness because it already has. Instead we want to use its cfg // instance: cfg = mixinConfigs[name]; meta = null; } else { cfg = ExtConfig.get(name); } configs[name] = cfg; if (cfg.cached || isCached) { cachedConfigs[name] = true; } // Ensure that the new config has a getter and setter. Because this method // is called during class creation as the "config" (or "cachedConfig") is // being processed, the user's methods will not be on the prototype yet. // // This has the following trade-offs: // // - Custom getters are rare so there is minimal waste generated by them. // // - Custom setters are more common but, by putting the default setter on // the prototype prior to addMembers, when the user methods are added // callParent can be used to call the generated setter. This is almost // certainly desirable as the setter has some very important semantics // that a custom setter would probably want to preserve by just adding // logic before and/or after the callParent. // // - By not adding these to the class body we avoid all the "is function" // tests that get applied to each class member thereby streamlining the // downstream class creation process. // // We still check for getter and/or setter but primarily for reasons of // backwards compatibility and "just in case" someone relied on inherited // getter/setter even though the base did not have the property listed as // a "config" (obscure case certainly). // names = cfg.names; if (!prototype[s = names.get]) { prototype[s] = cfg.getter || cfg.getGetter(); } if (!prototype[s = names.set]) { prototype[s] = (meta && meta.evented) ? (cfg.eventedSetter || cfg.getEventedSetter()) : (cfg.setter || cfg.getSetter()); } } if (meta) { if (cfg.owner !== Cls) { configs[name] = cfg = Ext.Object.chain(cfg); cfg.owner = Cls; } Ext.apply(cfg, meta); delete cfg.$value; } // Fork checks all the default values to see if they are arrays or objects // Do this to save us from doing it on each run if (!me.needsFork && value && (value.constructor === Object || value instanceof Array)) { me.needsFork = true; } // If the value is non-null, we need to initialize it. if (value !== null) { initMap[name] = true; } else { if (prototype.$configPrefixed) { prototype[configs[name].names.internal] = null; } else { prototype[configs[name].name] = null; } if (name in initMap) { // Only set this to false if we already have it in the map, otherwise, just leave it out! initMap[name] = false; } } values[name] = value; } }, //<debug> addDeprecations: function (configs) { var me = this, deprecations = me.deprecations, className = (me.cls.$className || '') + '#', message, newName, oldName; for (oldName in configs) { newName = configs[oldName]; // configs: { // dead: null, // // renamed: 'newName', // // removed: { // message: 'This config was replaced by pixie dust' // } // } if (!newName) { message = 'This config has been removed.'; } else if (!(message = newName.message)) { message = 'This config has been renamed to "' + newName + '"'; } deprecations[oldName] = className + oldName + ': ' + message; } }, //</debug> /** * This method configures the given `instance` using the specified `instanceConfig`. * The given `instance` should have been created by this object's `cls`. * * @param {Object} instance The instance to configure. * @param {Object} instanceConfig The configuration properties to apply to `instance`. * @private */ configure: function (instance, instanceConfig) { var me = this, configs = me.configs, //<debug> deprecations = me.deprecations, //</debug> initMap = me.initMap, initListMap = me.initListMap, initList = me.initList, prototype = me.cls.prototype, values = me.values, remaining = 0, firstInstance = !initList, cachedInitList, cfg, getter, i, internalName, ln, names, name, value, isCached, valuesKey, field; values = me.needsFork ? ExtObject.fork(values) : ExtObject.chain(values); // Let apply/update methods know that the initConfig is currently running. instance.isConfiguring = true; if (firstInstance) { // When called to configure the first instance of the class to which we are // bound we take a bit to plan for instance 2+. me.initList = initList = []; me.initListMap = initListMap = {}; instance.isFirstInstance = true; for (name in initMap) { cfg = configs[name]; isCached = cfg.cached; if (initMap[name]) { names = cfg.names; value = values[name]; if (!prototype[names.set].$isDefault || prototype[names.apply] || prototype[names.update] || typeof value === 'object') { if (isCached) { // This is a cachedConfig, so it needs to be initialized with // the default value and placed on the prototype... but the // instanceConfig may have a different value so the value may // need resetting. We have to defer the call to the setter so // that all of the initGetters are set up first. (cachedInitList || (cachedInitList = [])).push(cfg); } else { // Remember this config so that all instances (including this // one) can invoke the setter to properly initialize it. initList.push(cfg); initListMap[name] = true; } // Point all getters to the initGetters. By doing this here we // avoid creating initGetters for configs that don't need them // and we can easily pick up the cached fn to save the call. instance[names.get] = cfg.initGetter || cfg.getInitGetter(); } else { // Non-object configs w/o custom setter, applier or updater can // be simply stored on the prototype. prototype[cfg.getInternalName(prototype)] = value; } } else if (isCached) { prototype[cfg.getInternalName(prototype)] = undefined; } } } // TODO - we need to combine the cached loop with the instanceConfig loop to // avoid duplication of init getter setups (for correctness if a cached cfg // calls on a non-cached cfg) ln = cachedInitList && cachedInitList.length; if (ln) { // This is only ever done on the first instance we configure. Any config in // cachedInitList has to be set to the default value to allow any side-effects // or transformations to occur. The resulting values can then be elevated to // the prototype and this property need not be initialized on each instance. for (i = 0; i < ln; ++i) { internalName = cachedInitList[i].getInternalName(prototype); // Since these are cached configs the base class will potentially have put // its cached values on the prototype so we need to hide these while we // run the inits for our cached configs. instance[internalName] = null; } for (i = 0; i < ln; ++i) { names = (cfg = cachedInitList[i]).names; getter = names.get; if (instance.hasOwnProperty(getter)) { instance[names.set](values[cfg.name]); delete instance[getter]; } } for (i = 0; i < ln; ++i) { internalName = cachedInitList[i].getInternalName(prototype); prototype[internalName] = instance[internalName]; delete instance[internalName]; } // The cachedConfigs have all been set to the default values including any of // those that may have been triggered by their getter. } // If the instanceConfig has a platformConfig in it, we need to merge the active // rules of that object to make the actual instanceConfig. if (instanceConfig && instanceConfig.platformConfig) { instanceConfig = me.resolvePlatformConfig(instance, instanceConfig); } if (firstInstance) { // Allow the class to do things once the cachedConfig has been processed. // We need to call this method always when the first instance is configured // whether or not it actually has cached configs if (instance.afterCachedConfig && !instance.afterCachedConfig.$nullFn) { instance.afterCachedConfig(instanceConfig); } } // Now that the cachedConfigs have been processed we can apply the instanceConfig // and hide the "configs" on the prototype. This will serve as the source for any // configs that need to initialize from their initial getter call. instance.config = values; // There are 2 possibilities here: // 1) If it's the first time in this function, we may have had cachedConfigs running. // these configs may have called the getters for any of the normal getters, which // means the initial getters have been clobbered on the instance and won't be able // to be called below when we iterate over the initList. As such, we need to // reinitialize them here, even though we've done it up above. // // 2) If this the second time in this function, the cachedConfigs won't be processed, // so we don't need to worry about them clobbering config values. However, since // we've already done all our setup, we won't enter into the block that sets the // initGetter, so we need to do it here anyway. // // Also note, that lazy configs will appear in the initList because we need // to spin up the initGetter. for (i = 0, ln = initList.length; i < ln; ++i) { cfg = initList[i]; instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter(); } // Give the class a chance to transform the configs. if (instance.transformInstanceConfig) { instanceConfig = instance.transformInstanceConfig(instanceConfig); } // Important: We are looping here twice on purpose. This first loop serves 2 purposes: // // 1) Ensure the values collection is fully populated before we call any setters. Since // a setter may have an updater/applier, it could potentially call another getter() to grab // the value for some other property, so this ensures they are all set on the config object. // // 2) Ensure that the initGetter is set as the getter for any config that doesn't appear in // the initList. We need to ensure that the initGetter is pushed on for everything that we will // be setting during init time. // // The merging in this loop cannot be completed by Ext.merge(), since we do NOT want to merge // non-strict values, they should always just be assigned across without modification. if (instanceConfig) { for (name in instanceConfig) { value = instanceConfig[name]; cfg = configs[name]; //<debug> if (deprecations[name]) { Ext.log.warn(deprecations[name]); if (!cfg) { // If there is a Config for this, perhaps the class is emulating // the old config... If there is not a Config we don't want to // proceed and put the property on the instance. That will likely // hide the bug during development. continue; } } //</debug> if (!cfg) { //<debug> field = instance.self.prototype[name]; if (instance.$configStrict && (typeof field === 'function') && !field.$nullFn) { // In strict mode you cannot override functions Ext.raise('Cannot override method ' + name + ' on ' + instance.$className + ' instance.'); } //</debug> // Not all "configs" use the config system so in this case simply put // the value on the instance: instance[name] = value; } else { // However we still need to create the initial value that needs // to be used. We also need to spin up the initGetter. if (!cfg.lazy) { ++remaining; } if (!initListMap[name]) { instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter(); } if (cfg.merge) { value = cfg.merge(value, values[name], instance); } else if (value && value.constructor === Object) { valuesKey = values[name]; if (valuesKey && valuesKey.constructor === Object) { value = ExtObject.merge(values[name], value); } else { value = Ext.clone(value, false); } } } values[name] = value; } } // Give the class a chance to hook in prior to initializing the configs. if (instance.beforeInitConfig && !instance.beforeInitConfig.$nullFn) { if (instance.beforeInitConfig(instanceConfig) === false) { return; } } if (instanceConfig) { for (name in instanceConfig) { if (!remaining) { // For classes that have few proper Config properties, this saves us // from making the full 2 passes over the instanceConfig. break; } // We can ignore deprecated configs here because we warned about them // above. Further, since we only process proper Config's here we would // not be skipping them anyway. cfg = configs[name]; if (cfg && !cfg.lazy) { --remaining; // A proper "config" property so call the setter to set the value. names = cfg.names; getter = names.get; // At this point the initGetter may have already been called and // cleared if the getter was called from the applier or updater of a // previously processed instance config. checking if the instance has // its own getter ensures the setter does not get called twice. if (instance.hasOwnProperty(getter)) { instance[names.set](values[name]); // The generated setter will remove the initGetter from the instance // but the user may have provided their own setter so we have to do // this here as well: delete instance[names.get]; } } } } // Process configs declared on the class that need per-instance initialization. for (i = 0, ln = initList.length; i < ln; ++i) { cfg = initList[i]; names = cfg.names; getter = names.get; if (!cfg.lazy && instance.hasOwnProperty(getter)) { // Since the instance still hasOwn the getter, that means we've set an initGetter // and it hasn't been cleared by calling any setter. Since we've never set the value // because it wasn't passed in the instance, we go and set it here, taking the value // from our definition config and passing it through finally clear off the getter. instance[names.set](values[cfg.name]); delete instance[getter]; } } // Expose the value from the prototype chain (false): delete instance.isConfiguring; }, getCurrentConfig: function (instance) { var defaultConfig = instance.defaultConfig, config = {}, name; for (name in defaultConfig) { config[name] = instance[configPropMap[name].names.get](); } return config; }, /** * Merges the values of a config object onto a base config. * @param {Ext.Base} instance * @param {Object} baseConfig * @param {Object} config * @return {Object} the merged config * @private */ merge: function (instance, baseConfig, config) { // Although this is a "private" method. It is used by Sencha Architect and so // its api should remain stable. var configs = this.configs, name, value, baseValue, cfg; for (name in config) { value = config[name]; cfg = configs[name]; if (cfg) { if (cfg.merge) { value = cfg.merge(value, baseConfig[name], instance); } else if (value && value.constructor === Object) { baseValue = baseConfig[name]; if (baseValue && baseValue.constructor === Object) { value = Ext.Object.merge(baseValue, value); } else { value = Ext.clone(value, false); } } } baseConfig[name] = value; } return baseConfig; }, /** * @private */ reconfigure: function (instance, instanceConfig, options) { var currentConfig = instance.config, configList = [], strict = instance.$configStrict && !(options && options.strict === false), configs = this.configs, defaults = options && options.defaults, cfg, getter, i, len, name, names, prop; for (name in instanceConfig) { if (defaults && instance.hasOwnProperty(name)) { continue; } currentConfig[name] = instanceConfig[name]; cfg = configs[name]; //<debug> if (this.deprecations[name]) { // See similar logic doc in configure() method. Ext.log.warn(this.deprecations[name]); if (!cfg) { continue; } } //</debug> if (cfg) { // To ensure that configs being set here get processed in the proper order // we must give them init getters just in case they depend upon each other instance[cfg.names.get] = cfg.initGetter || cfg.getInitGetter(); } else { // Check for existence of the property on the prototype before proceeding. // If present on the prototype, and if the property is a function we // do not allow it to be overridden by a property in the config object // in strict mode (unless the function on the prototype is a emptyFn or // identityFn). Note that we always check the prototype, not the instance // because calling setConfig a second time should have the same results - // the first call may have set a function on the instance. prop = instance.self.prototype[name]; if (strict) { if ((typeof prop === 'function') && !prop.$nullFn) { //<debug> Ext.Error.raise("Cannot override method " + name + " on " + instance.$className + " instance."); //</debug> continue; } //<debug> else { if (name !== 'type') { Ext.log.warn('No such config "' + name + '" for class ' + instance.$className); } } //</debug> } } configList.push(name); } for (i = 0, len = configList.length; i < len; i++) { name = configList[i]; cfg = configs[name]; if (cfg) { names = cfg.names; getter = names.get; if (instance.hasOwnProperty(getter)) { // Since the instance still hasOwn the getter, that means we've set an initGetter // and it hasn't been cleared by calling any setter. Since we've never set the value // because it wasn't passed in the instance, we go and set it here, taking the value // from our definition config and passing it through finally clear off the getter. instance[names.set](instanceConfig[name]); delete instance[getter]; } } else { cfg = configPropMap[name] || Ext.Config.get(name); names = cfg.names; if (instance[names.set]) { instance[names.set](instanceConfig[name]); } else { // apply non-config props directly to the instance instance[name] = instanceConfig[name]; } } } }, /** * This method accepts an instance config object containing a `platformConfig` * property and merges the appropriate rules from that sub-object with the root object * to create the final config object that should be used. This is method called by * `{@link #configure}` when it receives an `instanceConfig` containing a * `platformConfig` property. * * @param {Ext.Base} instance * @param {Object} instanceConfig The instance config parameter. * @return {Object} The new instance config object with platformConfig results applied. * @private * @since 5.1.0 */ resolvePlatformConfig: function (instance, instanceConfig) { var platformConfig = instanceConfig && instanceConfig.platformConfig, ret = instanceConfig, i, keys, n; if (platformConfig) { keys = Ext.getPlatformConfigKeys(platformConfig); n = keys.length; if (n) { ret = Ext.merge({}, ret); // this deep copies sub-objects for (i = 0, n = keys.length; i < n; ++i) { this.merge(instance, ret, platformConfig[keys[i]]); } } } return ret; }}; // prototype }()); // closure on whole file