/** * This mixin allows a class to easily forward (or proxy) configs to other objects. Once * mixed in, the using class (and its derived classes) can add a `proxyConfig` object * property to their class body that specifies the accessor and configs to manage. * * For example: * * Ext.define('ParentThing', { * mixins: [ * 'Ext.mixin.ConfigProxy' * ], * * config: { * childThing: { * xtype: 'panel' * } * }, * * proxyConfig: { * // The keys of this object are themselves configs. Their getters * // are used to identify the target to which the listed configs are * // proxied. * * childThing: [ * // This list of config names will be proxied to the object * // returned by the getter (getChildThing in this case). In * // addition, each of these will be defined as configs on this * // class but with a special getter and setter. * // * // These configs cannot be previously defined nor can their * // be getters or setters already present. * * 'title' * ] * } * }); * * If the getter for a proxy target returns `null`, the setter for the proxied config * will simply discard the value. It is expected that the target will generally always * exist. * * To proxy methods, the array of config names is replaced by an object: * * Ext.define('ParentThing', { * mixins: [ * 'Ext.mixin.ConfigProxy' * ], * * config: { * childThing: { * xtype: 'panel' * } * }, * * proxyConfig: { * // The keys of this object are themselves configs. Their getters * // are used to identify the target to which the listed configs are * // proxied. * * childThing: { * configs: [ * // same as when "childThing" was just this array... * ], * * methods: [ * // A list of methods to proxy to the childThing. * 'doStuff' * ] * ] * } * }); * * @private * @since 6.5.0 */Ext.define('Ext.mixin.ConfigProxy', function(ConfigProxy) { return { // eslint-disable-line brace-style, max-len extend: 'Ext.Mixin', mixinConfig: { id: 'configproxy', extended: function(baseClass, derivedClass, classBody) { var proxyConfig = classBody.proxyConfig; derivedClass.$configProxies = Ext.apply( {}, derivedClass.superclass.self.$configProxies ); if (proxyConfig) { delete classBody.proxyConfig; ConfigProxy.processClass(derivedClass, proxyConfig); } } }, onClassMixedIn: function(targetClass) { var prototype = targetClass.prototype, proxyConfig = prototype.proxyConfig, initConfig = prototype.initConfig; prototype.$proxiedConfigs = null; // constant shape targetClass.$configProxies = { // contents are basically the same as the proxyConfig object. }; prototype.initConfig = function(config) { initConfig.apply(this, arguments); // ensure future setter calls will pass through to the target: this.$proxiedConfigs = null; return this; }; if (proxyConfig) { delete prototype.proxyConfig; ConfigProxy.processClass(targetClass, proxyConfig); } }, /** * This method returns an object of all proxied config values for a given target. This * is only useful during the class initialization phase to avoid passing in "wrong" * initial config values for a child object and then proxying down all the configs * from the parent. * * This method is not typically called directly but rather `mergeProxiedConfigs` is * more likely. * @param {String} name The proxy target config name (in the class example, this would * be "childThing"). * @return {Object} * @private * @since 6.5.0 */ getProxiedConfigs: function(name) { var me = this, configs = me.config, // the merged config set configProxies = me.self.$configProxies[name], i = configProxies && configProxies.length, cfg, proxiedConfigs, ret, s, v; if (i && me.isConfiguring) { // Lazily create the $proxiedConfigs map to track the config properties // we are "stealing" away. proxiedConfigs = me.$proxiedConfigs || (me.$proxiedConfigs = {}); while (i-- > 0) { cfg = configProxies[i]; proxiedConfigs[s = cfg.name] = cfg; if ((v = configs[s]) !== undefined) { (ret || (ret = {}))[s] = v; } } } return ret; }, /** * This method accepts the normal config object (`itemConfig`) for the child object * (`name`) and merges any proxied configs into a new config object. This is useful * during the class initialization phase to avoid passing in "wrong" initial config * values for a child object and then proxying down the rest of the configs. * * This method is typically called during an "applier" method for a proxy target. If * called at any other time this method simply returns the given `itemConfig`. This * makes it safe to code such appliers as follows: * * applyChildThing: function(config) { * config = this.mergeProxiedConfigs('childThing', config); * * return new ChildThing(config); * } * * @param {String} name The proxy target config name (in the class example, this would * be "childThing"). * @param {Mixed} itemConfig The default configuration for the child item. * @param {Boolean} [alwaysClone] Pass `true` to ensure a new object is returned. * @return {Object} * @private * @since 6.5.0 */ mergeProxiedConfigs: function(name, itemConfig, alwaysClone) { var me = this, ret = itemConfig, proxied = me.getProxiedConfigs(name), configurator; if (proxied) { if (!itemConfig) { ret = proxied; } else if (itemConfig.constructor === Object) { configurator = me.self.getConfigurator(); // First clone() so don't mutate the config: ret = configurator.merge(me, Ext.clone(itemConfig), proxied); } } if (alwaysClone && ret === itemConfig) { ret = Ext.clone(ret); } return ret; }, statics: { processClass: function(targetClass, proxyConfig) { var ExtConfig = Ext.Config, targetProto = targetClass.prototype, add = {}, proxies = targetClass.$configProxies, cfg, configs, itemGetter, i, item, methods, n, name, proxiedConfigs, s; for (item in proxyConfig) { itemGetter = ExtConfig.get(item).names.get; configs = proxyConfig[item]; if (Ext.isArray(configs)) { methods = null; } else { methods = configs.methods; configs = configs.configs; } if (!(proxiedConfigs = proxies[item])) { proxies[item] = proxiedConfigs = []; } else { // this array comes from the superclass so slice it for this class: proxies[item] = proxiedConfigs = proxiedConfigs.slice(); } for (i = 0, n = methods && methods.length; i < n; ++i) { if (!targetProto[name = methods[i]]) { targetProto[name] = ConfigProxy.wrapFn(itemGetter, name); } //<debug> else { Ext.raise('Cannot proxy method "' + name + '"'); } //</debug> } for (i = 0, n = configs && configs.length; i < n; ++i) { cfg = ExtConfig.get(s = configs[i]); //<debug> if (s in add) { Ext.raise('Duplicate proxy config definitions for "' + s + '"'); } if (s in targetProto.config) { Ext.raise('Config "' + s + '" already defined for class ' + targetProto.$className); } //</debug> add[s] = undefined; // sentinel initial value to avoid smashing proxiedConfigs.push(cfg); if (!targetProto[name = cfg.names.get]) { targetProto[name] = ConfigProxy.wrapGet(itemGetter, name); } //<debug> else { Ext.raise('Cannot proxy "' + s + '" config getter'); } //</debug> if (!targetProto[name = cfg.names.set]) { targetProto[name] = ConfigProxy.wrapSet(itemGetter, name, s); } //<debug> else { Ext.raise('Cannot proxy "' + s + '" config setter'); } //</debug> } } targetClass.addConfig(add); }, wrapFn: function(itemGetter, name) { return function() { var item = this[itemGetter](); return item && item[name].apply(item, arguments); }; }, wrapGet: function(itemGetter, configGetter) { return function() { var item = this[itemGetter](); return item && item[configGetter](); }; }, wrapSet: function(itemGetter, configSetter, itemName) { return function(value) { var me = this, item, proxiedConfigs; // We define the proxied configs with "undefined" value so that we can // detect this and not smash them by default. if (!me.isConfiguring || value !== undefined) { // If the item's applier called mergeProxiedConfigs or getProxiedConfigs // then each config is marked as processed in proxiedConfigs (only during // initialization). item = me[itemGetter](); proxiedConfigs = me.$proxiedConfigs; // lazy created by itemGetter if (proxiedConfigs && proxiedConfigs[itemName]) { delete proxiedConfigs[itemName]; // drop only the first set call item = null; } if (item) { item[configSetter](value); } } return me; }; } }};});