Форк 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.
 
 
 

580 lines
19 KiB

/**
* This mixin provides its user with a `responsiveConfig` config that allows the class
* to conditionally control config properties.
*
* For example:
*
* Ext.define('ResponsiveClass', {
* mixin: [
* 'Ext.mixin.Responsive'
* ],
*
* responsiveConfig: {
* portrait: {
* },
*
* landscape: {
* }
* }
* });
*
* For a config to participate as a responsiveConfig it must have a "setter" method. In
* the below example, a "setRegion" method must exist.
*
* Ext.create({
* xtype: 'viewport',
* layout: 'border',
*
* items: [{
* title: 'Some Title',
* plugins: 'responsive',
*
* responsiveConfig: {
* 'width < 800': {
* region: 'north'
* },
* 'width >= 800': {
* region: 'west'
* }
* }
* }]
* });
*
* To use responsiveConfig the class must be defined using the Ext.mixin.Responsive mixin.
*
* Ext.define('App.view.Foo', {
* extend: 'Ext.panel.Panel',
* xtype: 'foo',
* mixins: [
* 'Ext.mixin.Responsive'
* ],
* ...
* });
*
* Otherwise, you will need to use the responsive plugin if the class is not one you authored.
*
* Ext.create('Ext.panel.Panel', {
* renderTo: document.body,
* plugins: 'responsive',
* ...
* });
*
* _Note:_ There is the exception of `Ext.container.Viewport` or other classes using `Ext.plugin.Viewport`.
* In those cases, the viewport plugin inherits from `Ext.plugin.Responsive`.
*
* For details see `{@link #responsiveConfig}`.
* @since 5.0.0
*/
Ext.define('Ext.mixin.Responsive', function (Responsive) { return {
extend: 'Ext.Mixin',
requires: [
'Ext.GlobalEvents'
],
mixinConfig: {
id: 'responsive',
after: {
destroy: 'destroy'
}
},
config: {
/**
* @cfg {Object} responsiveConfig
* This object consists of keys that represent the conditions on which configs
* will be applied. For example:
*
* responsiveConfig: {
* landscape: {
* region: 'west'
* },
* portrait: {
* region: 'north'
* }
* }
*
* In this case the keys ("landscape" and "portrait") are the criteria (or "rules")
* and the object to their right contains the configs that will apply when that
* rule is true.
*
* These rules can be any valid JavaScript expression but the following values
* are considered in scope:
*
* * `landscape` - True if the device orientation is landscape (always `true` on
* desktop devices).
* * `portrait` - True if the device orientation is portrait (always `false` on
* desktop devices).
* * `tall` - True if `width` < `height` regardless of device type.
* * `wide` - True if `width` > `height` regardless of device type.
* * `width` - The width of the viewport
* * `height` - The height of the viewport.
* * `platform` - An object containing various booleans describing the platform
* (see `{@link Ext#platformTags Ext.platformTags}`). The properties of this
* object are also available implicitly (without "platform." prefix) but this
* sub-object may be useful to resolve ambiguity (for example, if one of the
* `{@link #responsiveFormulas}` overlaps and hides any of these properties).
* Previous to Ext JS 5.1, the `platformTags` were only available using this
* prefix.
*
* A more complex example:
*
* responsiveConfig: {
* 'desktop || width > 800': {
* region: 'west'
* },
*
* '!(desktop || width > 800)': {
* region: 'north'
* }
* }
*
* **NOTE**: If multiple rules set a single config (like above), it is important
* that the rules be mutually exclusive. That is, only one rule should set each
* config. If multiple rules are actively setting a single config, the order of
* these (and therefore the config's value) is unspecified.
*
* For a config to participate as a `responsiveConfig` it must have a "setter"
* method. In the above example, a "setRegion" method must exist.
*
* @since 5.0.0
*/
responsiveConfig: {
$value: undefined,
merge: function (newValue, oldValue, target, mixinClass) {
if (!newValue) {
return oldValue;
}
var ret = oldValue ? Ext.Object.chain(oldValue) : {},
rule;
for (rule in newValue) {
if (!mixinClass || !(rule in ret)) {
ret[rule] = {
fn: null, // created on first evaluation of this rule
config: newValue[rule]
};
}
}
return ret;
}
},
/**
* @cfg {Object} responsiveFormulas
* It is common when using `responsiveConfig` to have recurring expressions that
* make for complex configurations. Using `responsiveFormulas` allows you to cut
* down on this repetition by adding new properties to the "scope" for the rules
* in a `responsiveConfig`.
*
* For example:
*
* Ext.define('MyApp.view.main.Main', {
* extend: 'Ext.container.Container',
*
* mixins: [
* 'Ext.mixin.Responsive'
* ],
*
* responsiveFormulas: {
* small: 'width < 600',
*
* medium: 'width >= 600 && width < 800',
*
* large: 'width >= 800',
*
* tuesday: function (context) {
* return (new Date()).getDay() === 2;
* }
* }
* });
*
* With the above declaration, any `responsiveConfig` can now use these values
* like so:
*
* responsiveConfig: {
* small: {
* hidden: true
* },
* medium: {
* hidden: false,
* region: 'north'
* },
* large: {
* hidden: false,
* region: 'west'
* }
* }
*
* @since 5.0.1
*/
responsiveFormulas: {
$value: 0,
merge: function (newValue, oldValue, target, mixinClass) {
return this.mergeNew(newValue, oldValue, target, mixinClass);
}
}
},
/**
* This method removes this instance from the Responsive collection.
*/
destroy: function () {
Responsive.unregister(this);
this.callParent();
},
privates: {
statics: {
/**
* @property {Boolean} active
* @static
* @private
*/
active: false,
/**
* @property {Object} all
* The collection of all `Responsive` instances. These are the instances that
* will be notified when dynamic conditions change.
* @static
* @private
*/
all: {},
/**
* @property {Object} context
* This object holds the various context values passed to the rule evaluation
* functions.
* @static
* @private
*/
context: Ext.Object.chain(Ext.platformTags),
/**
* @property {Number} count
* The number of instances in the `all` collection.
* @static
* @private
*/
count: 0,
/**
* @property {Number} nextId
* The seed value used to assign `Responsive` instances a unique id for keying
* in the `all` collection.
* @static
* @private
*/
nextId: 0,
/**
* Activates event listeners for all `Responsive` instances. This method is
* called when the first instance is registered.
* @private
*/
activate: function () {
Responsive.active = true;
Responsive.updateContext();
Ext.on('resize', Responsive.onResize, Responsive);
},
/**
* Deactivates event listeners. This method is called when the last instance
* is destroyed.
* @private
*/
deactivate: function () {
Responsive.active = false;
Ext.un('resize', Responsive.onResize, Responsive);
},
/**
* Updates all registered the `Responsive` instances (found in the `all`
* collection).
* @private
*/
notify: function () {
var all = Responsive.all,
context = Responsive.context,
globalEvents = Ext.GlobalEvents,
timer = Responsive.timer,
id;
if (timer) {
Responsive.timer = null;
Ext.Function.cancelAnimationFrame(timer);
}
Responsive.updateContext();
Ext.suspendLayouts();
globalEvents.fireEvent('beforeresponsiveupdate', context);
for (id in all) {
all[id].setupResponsiveContext();
}
globalEvents.fireEvent('beginresponsiveupdate', context);
for (id in all) {
all[id].updateResponsiveState();
}
globalEvents.fireEvent('responsiveupdate', context);
Ext.resumeLayouts(true);
},
/**
* Handler of the window resize event. Schedules a timer so that we eventually
* call `notify`.
* @private
*/
onResize: function () {
if (!Responsive.timer) {
Responsive.timer = Ext.Function.requestAnimationFrame(Responsive.onTimer);
}
},
/**
* This method is the timer handler. When called this removes the timer and
* calls `notify`.
* @private
*/
onTimer: function () {
Responsive.timer = null;
Responsive.notify();
},
/**
* This method is called to update the internal state of a given config since
* the config is needed prior to `initConfig` processing the `instanceConfig`.
*
* @param {Ext.Base} instance The instance to configure.
* @param {Object} instanceConfig The config for the instance.
* @param {String} name The name of the config to process.
* @private
* @since 5.0.1
*/
processConfig: function (instance, instanceConfig, name) {
var value = instanceConfig && instanceConfig[name],
config = instance.config,
cfg, configurator;
// Good news is that both configs we have to handle have custom merges
// so we just need to get the Ext.Config instance and call it.
if (value) {
configurator = instance.getConfigurator();
cfg = configurator.configs[name]; // the Ext.Config instance
// Update "this.config" which is the storage for this instance.
config[name] = cfg.merge(value, config[name], instance);
}
},
register: function (responder) {
var id = responder.$responsiveId;
if (!id) {
responder.$responsiveId = id = ++Responsive.nextId;
Responsive.all[id] = responder;
if (++Responsive.count === 1) {
Responsive.activate();
}
}
},
unregister: function (responder) {
var id = responder.$responsiveId;
if (id in Responsive.all) {
responder.$responsiveId = null;
delete Responsive.all[id];
if (--Responsive.count === 0) {
Responsive.deactivate();
}
}
},
/**
* Updates the `context` object base on the current environment.
* @private
*/
updateContext: function () {
var El = Ext.Element,
width = El.getViewportWidth(),
height = El.getViewportHeight(),
context = Responsive.context;
context.width = width;
context.height = height;
context.tall = width < height;
context.wide = !context.tall;
context.landscape = context.portrait = false;
if (!context.platform) {
context.platform = Ext.platformTags;
}
context[Ext.dom.Element.getOrientation()] = true;
}
}, // private static
//--------------------------------------------------------------------------
/**
* This class system hook method is called at the tail end of the mixin process.
* We need to see if the `targetClass` has already got a `responsiveConfig` and
* if so, we must add its value to the real config.
* @param {Ext.Class} targetClass
* @private
*/
afterClassMixedIn: function (targetClass) {
var proto = targetClass.prototype,
responsiveConfig = proto.responsiveConfig,
responsiveFormulas = proto.responsiveFormulas,
config;
if (responsiveConfig || responsiveFormulas) {
config = {};
if (responsiveConfig) {
delete proto.responsiveConfig;
config.responsiveConfig = responsiveConfig;
}
if (responsiveFormulas) {
delete proto.responsiveFormulas;
config.responsiveFormulas = responsiveFormulas;
}
targetClass.getConfigurator().add(config);
}
},
// The reason this method exists is so to convince the config system to put the
// "responsiveConfig" and "responsiveFormulas" in the initList. This needs to be
// done so that the initGetter is setup prior to calling transformInstanceConfig
// when we need to call the getters.
applyResponsiveConfig: function (rules) {
for (var rule in rules) {
rules[rule].fn = Ext.createRuleFn(rule);
}
return rules;
},
applyResponsiveFormulas: function (formulas) {
var ret = {},
fn, name;
if (formulas) {
for (name in formulas) {
if (Ext.isString(fn = formulas[name])) {
fn = Ext.createRuleFn(fn);
}
ret[name] = fn;
}
}
return ret;
},
/**
* Evaluates and returns the configs based on the `responsiveConfig`. This
* method relies on the state being captured by the `updateContext` method.
* @private
*/
getResponsiveState: function () {
var context = Responsive.context,
rules = this.getResponsiveConfig(),
ret = {},
entry, rule;
if (rules) {
for (rule in rules) {
entry = rules[rule];
if (entry.fn.call(this, context)) {
Ext.merge(ret, entry.config);
}
}
}
return ret;
},
setupResponsiveContext: function () {
var formulas = this.getResponsiveFormulas(),
context = Responsive.context,
name;
if (formulas) {
for (name in formulas) {
context[name] = formulas[name].call(this, context);
}
}
},
/**
* This config system hook method is called just prior to processing the specified
* "instanceConfig". This hook returns the instanceConfig that will actually be
* processed by the config system.
* @param {Object} instanceConfig The user-supplied instance config object.
* @private
*/
transformInstanceConfig: function (instanceConfig) {
var me = this,
ret;
Responsive.register(me);
// Since we are called immediately prior to the Configurator looking at the
// instanceConfig, that incoming value has not yet been merged on to
// "this.config". We need to call getResponsiveConfig and getResponsiveFormulas
// and still get all that merged goodness, so we have to do the merge here.
if (instanceConfig) {
Responsive.processConfig(me, instanceConfig, 'responsiveConfig');
Responsive.processConfig(me, instanceConfig, 'responsiveFormulas');
}
// For updates this is done in bulk prior to updating all of the responsive
// objects, but for instantiation, we have to do this for ourselves now.
me.setupResponsiveContext();
// Now we can merge the current responsive state with the incoming config.
// The responsiveConfig takes priority.
ret = me.getResponsiveState();
if (instanceConfig) {
ret = Ext.merge({}, instanceConfig, ret);
// We don't want these to remain since we've already handled them.
delete ret.responsiveConfig;
delete ret.responsiveFormulas;
}
return ret;
},
/**
* Evaluates and applies the `responsiveConfig` to this instance. This is called
* by `notify` automatically.
* @private
*/
updateResponsiveState: function () {
var config = this.getResponsiveState();
this.setConfig(config);
}
} // private
}});