]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/mixin/Responsive.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / mixin / Responsive.js
CommitLineData
6527f429
DM
1/**\r
2 * This mixin provides its user with a `responsiveConfig` config that allows the class\r
3 * to conditionally control config properties.\r
4 *\r
5 * For example:\r
6 *\r
7 * Ext.define('ResponsiveClass', {\r
8 * mixin: [\r
9 * 'Ext.mixin.Responsive'\r
10 * ],\r
11 *\r
12 * responsiveConfig: {\r
13 * portrait: {\r
14 * },\r
15 *\r
16 * landscape: {\r
17 * }\r
18 * }\r
19 * });\r
20 *\r
21 * For a config to participate as a responsiveConfig it must have a "setter" method. In\r
22 * the below example, a "setRegion" method must exist.\r
23 *\r
24 * Ext.create({\r
25 * xtype: 'viewport',\r
26 * layout: 'border',\r
27 *\r
28 * items: [{\r
29 * title: 'Some Title',\r
30 * plugins: 'responsive',\r
31 *\r
32 * responsiveConfig: {\r
33 * 'width < 800': {\r
34 * region: 'north'\r
35 * },\r
36 * 'width >= 800': {\r
37 * region: 'west'\r
38 * }\r
39 * }\r
40 * }]\r
41 * });\r
42 *\r
43 * To use responsiveConfig the class must be defined using the Ext.mixin.Responsive mixin.\r
44 *\r
45 * Ext.define('App.view.Foo', {\r
46 * extend: 'Ext.panel.Panel',\r
47 * xtype: 'foo',\r
48 * mixins: [\r
49 * 'Ext.mixin.Responsive'\r
50 * ],\r
51 * ...\r
52 * });\r
53 *\r
54 * Otherwise, you will need to use the responsive plugin if the class is not one you authored.\r
55 *\r
56 * Ext.create('Ext.panel.Panel', {\r
57 * renderTo: document.body,\r
58 * plugins: 'responsive',\r
59 * ...\r
60 * });\r
61 * \r
62 * _Note:_ There is the exception of `Ext.container.Viewport` or other classes using `Ext.plugin.Viewport`.\r
63 * In those cases, the viewport plugin inherits from `Ext.plugin.Responsive`.\r
64 *\r
65 * For details see `{@link #responsiveConfig}`.\r
66 * @since 5.0.0\r
67 */\r
68Ext.define('Ext.mixin.Responsive', function (Responsive) { return {\r
69 extend: 'Ext.Mixin',\r
70 requires: [\r
71 'Ext.GlobalEvents'\r
72 ],\r
73\r
74 mixinConfig: {\r
75 id: 'responsive',\r
76\r
77 after: {\r
78 destroy: 'destroy'\r
79 }\r
80 },\r
81\r
82 config: {\r
83 /**\r
84 * @cfg {Object} responsiveConfig\r
85 * This object consists of keys that represent the conditions on which configs\r
86 * will be applied. For example:\r
87 *\r
88 * responsiveConfig: {\r
89 * landscape: {\r
90 * region: 'west'\r
91 * },\r
92 * portrait: {\r
93 * region: 'north'\r
94 * }\r
95 * }\r
96 *\r
97 * In this case the keys ("landscape" and "portrait") are the criteria (or "rules")\r
98 * and the object to their right contains the configs that will apply when that\r
99 * rule is true.\r
100 *\r
101 * These rules can be any valid JavaScript expression but the following values\r
102 * are considered in scope:\r
103 *\r
104 * * `landscape` - True if the device orientation is landscape (always `true` on\r
105 * desktop devices).\r
106 * * `portrait` - True if the device orientation is portrait (always `false` on\r
107 * desktop devices).\r
108 * * `tall` - True if `width` < `height` regardless of device type.\r
109 * * `wide` - True if `width` > `height` regardless of device type.\r
110 * * `width` - The width of the viewport in pixels.\r
111 * * `height` - The height of the viewport in pixels.\r
112 * * `platform` - An object containing various booleans describing the platform\r
113 * (see `{@link Ext#platformTags Ext.platformTags}`). The properties of this\r
114 * object are also available implicitly (without "platform." prefix) but this\r
115 * sub-object may be useful to resolve ambiguity (for example, if one of the\r
116 * `{@link #responsiveFormulas}` overlaps and hides any of these properties).\r
117 * Previous to Ext JS 5.1, the `platformTags` were only available using this\r
118 * prefix.\r
119 *\r
120 * A more complex example:\r
121 *\r
122 * responsiveConfig: {\r
123 * 'desktop || width > 800': {\r
124 * region: 'west'\r
125 * },\r
126 *\r
127 * '!(desktop || width > 800)': {\r
128 * region: 'north'\r
129 * }\r
130 * }\r
131 *\r
132 * **NOTE**: If multiple rules set a single config (like above), it is important\r
133 * that the rules be mutually exclusive. That is, only one rule should set each\r
134 * config. If multiple rules are actively setting a single config, the order of\r
135 * these (and therefore the config's value) is unspecified.\r
136 *\r
137 * For a config to participate as a `responsiveConfig` it must have a "setter"\r
138 * method. In the above example, a "setRegion" method must exist.\r
139 *\r
140 * @since 5.0.0\r
141 */\r
142 responsiveConfig: {\r
143 $value: undefined,\r
144\r
145 merge: function (newValue, oldValue, target, mixinClass) {\r
146 if (!newValue) {\r
147 return oldValue;\r
148 }\r
149\r
150 var ret = oldValue ? Ext.Object.chain(oldValue) : {},\r
151 rule;\r
152\r
153 for (rule in newValue) {\r
154 if (!mixinClass || !(rule in ret)) {\r
155 ret[rule] = {\r
156 fn: null, // created on first evaluation of this rule\r
157 config: newValue[rule]\r
158 };\r
159 }\r
160 }\r
161\r
162 return ret;\r
163 }\r
164 },\r
165\r
166 /**\r
167 * @cfg {Object} responsiveFormulas\r
168 * It is common when using `responsiveConfig` to have recurring expressions that\r
169 * make for complex configurations. Using `responsiveFormulas` allows you to cut\r
170 * down on this repetition by adding new properties to the "scope" for the rules\r
171 * in a `responsiveConfig`.\r
172 *\r
173 * For example:\r
174 *\r
175 * Ext.define('MyApp.view.main.Main', {\r
176 * extend: 'Ext.container.Container',\r
177 *\r
178 * mixins: [\r
179 * 'Ext.mixin.Responsive'\r
180 * ],\r
181 *\r
182 * responsiveFormulas: {\r
183 * small: 'width < 600',\r
184 *\r
185 * medium: 'width >= 600 && width < 800',\r
186 *\r
187 * large: 'width >= 800',\r
188 *\r
189 * tuesday: function (context) {\r
190 * return (new Date()).getDay() === 2;\r
191 * }\r
192 * }\r
193 * });\r
194 *\r
195 * With the above declaration, any `responsiveConfig` can now use these values\r
196 * like so:\r
197 *\r
198 * responsiveConfig: {\r
199 * small: {\r
200 * hidden: true\r
201 * },\r
202 * 'medium && !tuesday': {\r
203 * hidden: false,\r
204 * region: 'north'\r
205 * },\r
206 * large: {\r
207 * hidden: false,\r
208 * region: 'west'\r
209 * }\r
210 * }\r
211 *\r
212 * @since 5.0.1\r
213 */\r
214 responsiveFormulas: {\r
215 $value: 0,\r
216\r
217 merge: function (newValue, oldValue, target, mixinClass) {\r
218 return this.mergeNew(newValue, oldValue, target, mixinClass);\r
219 }\r
220 }\r
221 },\r
222\r
223 /**\r
224 * This method removes this instance from the Responsive collection.\r
225 */\r
226 destroy: function () {\r
227 Responsive.unregister(this);\r
228 this.callParent();\r
229 },\r
230\r
231 privates: {\r
232 statics: {\r
233 /**\r
234 * @property {Boolean} active\r
235 * @static\r
236 * @private\r
237 */\r
238 active: false,\r
239\r
240 /**\r
241 * @property {Object} all\r
242 * The collection of all `Responsive` instances. These are the instances that\r
243 * will be notified when dynamic conditions change.\r
244 * @static\r
245 * @private\r
246 */\r
247 all: {},\r
248\r
249 /**\r
250 * @property {Object} context\r
251 * This object holds the various context values passed to the rule evaluation\r
252 * functions.\r
253 * @static\r
254 * @private\r
255 */\r
256 context: Ext.Object.chain(Ext.platformTags),\r
257\r
258 /**\r
259 * @property {Number} count\r
260 * The number of instances in the `all` collection.\r
261 * @static\r
262 * @private\r
263 */\r
264 count: 0,\r
265\r
266 /**\r
267 * @property {Number} nextId\r
268 * The seed value used to assign `Responsive` instances a unique id for keying\r
269 * in the `all` collection.\r
270 * @static\r
271 * @private\r
272 */\r
273 nextId: 0,\r
274\r
275 /**\r
276 * Activates event listeners for all `Responsive` instances. This method is\r
277 * called when the first instance is registered.\r
278 * @private\r
279 */\r
280 activate: function () {\r
281 Responsive.active = true;\r
282 Responsive.updateContext();\r
283 Ext.on('resize', Responsive.onResize, Responsive);\r
284 },\r
285\r
286 /**\r
287 * Deactivates event listeners. This method is called when the last instance\r
288 * is destroyed.\r
289 * @private\r
290 */\r
291 deactivate: function () {\r
292 Responsive.active = false;\r
293 Ext.un('resize', Responsive.onResize, Responsive);\r
294 },\r
295\r
296 /**\r
297 * Updates all registered the `Responsive` instances (found in the `all`\r
298 * collection).\r
299 * @private\r
300 */\r
301 notify: function () {\r
302 var all = Responsive.all,\r
303 context = Responsive.context,\r
304 globalEvents = Ext.GlobalEvents,\r
305 timer = Responsive.timer,\r
306 id;\r
307\r
308 if (timer) {\r
309 Responsive.timer = null;\r
310 Ext.Function.cancelAnimationFrame(timer);\r
311 }\r
312\r
313 Responsive.updateContext();\r
314\r
315 Ext.suspendLayouts();\r
316\r
317 globalEvents.fireEvent('beforeresponsiveupdate', context);\r
318\r
319 for (id in all) {\r
320 all[id].setupResponsiveContext();\r
321 }\r
322\r
323 globalEvents.fireEvent('beginresponsiveupdate', context);\r
324\r
325 for (id in all) {\r
326 all[id].updateResponsiveState();\r
327 }\r
328\r
329 globalEvents.fireEvent('responsiveupdate', context);\r
330\r
331 Ext.resumeLayouts(true);\r
332 },\r
333\r
334 /**\r
335 * Handler of the window resize event. Schedules a timer so that we eventually\r
336 * call `notify`.\r
337 * @private\r
338 */\r
339 onResize: function () {\r
340 if (!Responsive.timer) {\r
341 Responsive.timer = Ext.Function.requestAnimationFrame(Responsive.onTimer);\r
342 }\r
343 },\r
344\r
345 /**\r
346 * This method is the timer handler. When called this removes the timer and\r
347 * calls `notify`.\r
348 * @private\r
349 */\r
350 onTimer: function () {\r
351 Responsive.timer = null;\r
352 Responsive.notify();\r
353 },\r
354\r
355 /**\r
356 * This method is called to update the internal state of a given config since\r
357 * the config is needed prior to `initConfig` processing the `instanceConfig`.\r
358 *\r
359 * @param {Ext.Base} instance The instance to configure.\r
360 * @param {Object} instanceConfig The config for the instance.\r
361 * @param {String} name The name of the config to process.\r
362 * @private\r
363 * @since 5.0.1\r
364 */\r
365 processConfig: function (instance, instanceConfig, name) {\r
366 var value = instanceConfig && instanceConfig[name],\r
367 config = instance.config,\r
368 cfg, configurator;\r
369\r
370 // Good news is that both configs we have to handle have custom merges\r
371 // so we just need to get the Ext.Config instance and call it.\r
372 if (value) {\r
373 configurator = instance.getConfigurator();\r
374 cfg = configurator.configs[name]; // the Ext.Config instance\r
375\r
376 // Update "this.config" which is the storage for this instance.\r
377 config[name] = cfg.merge(value, config[name], instance);\r
378 }\r
379 },\r
380\r
381 register: function (responder) {\r
382 var id = responder.$responsiveId;\r
383\r
384 if (!id) {\r
385 responder.$responsiveId = id = ++Responsive.nextId;\r
386\r
387 Responsive.all[id] = responder;\r
388\r
389 if (++Responsive.count === 1) {\r
390 Responsive.activate();\r
391 }\r
392 }\r
393 },\r
394\r
395 unregister: function (responder) {\r
396 var id = responder.$responsiveId;\r
397\r
398 if (id in Responsive.all) {\r
399 responder.$responsiveId = null;\r
400\r
401 delete Responsive.all[id];\r
402\r
403 if (--Responsive.count === 0) {\r
404 Responsive.deactivate();\r
405 }\r
406 }\r
407 },\r
408\r
409 /**\r
410 * Updates the `context` object base on the current environment.\r
411 * @private\r
412 */\r
413 updateContext: function () {\r
414 var El = Ext.Element,\r
415 width = El.getViewportWidth(),\r
416 height = El.getViewportHeight(),\r
417 context = Responsive.context;\r
418\r
419 context.width = width;\r
420 context.height = height;\r
421 context.tall = width < height;\r
422 context.wide = !context.tall;\r
423\r
424 context.landscape = context.portrait = false;\r
425 if (!context.platform) {\r
426 context.platform = Ext.platformTags;\r
427 }\r
428\r
429 context[Ext.dom.Element.getOrientation()] = true;\r
430 }\r
431 }, // private static\r
432\r
433 //--------------------------------------------------------------------------\r
434\r
435 /**\r
436 * This class system hook method is called at the tail end of the mixin process.\r
437 * We need to see if the `targetClass` has already got a `responsiveConfig` and\r
438 * if so, we must add its value to the real config.\r
439 * @param {Ext.Class} targetClass\r
440 * @private\r
441 */\r
442 afterClassMixedIn: function (targetClass) {\r
443 var proto = targetClass.prototype,\r
444 responsiveConfig = proto.responsiveConfig,\r
445 responsiveFormulas = proto.responsiveFormulas,\r
446 config;\r
447\r
448 if (responsiveConfig || responsiveFormulas) {\r
449 config = {};\r
450\r
451 if (responsiveConfig) {\r
452 delete proto.responsiveConfig;\r
453 config.responsiveConfig = responsiveConfig;\r
454 }\r
455\r
456 if (responsiveFormulas) {\r
457 delete proto.responsiveFormulas;\r
458 config.responsiveFormulas = responsiveFormulas;\r
459 }\r
460\r
461 targetClass.getConfigurator().add(config);\r
462 }\r
463 },\r
464\r
465 // The reason this method exists is so to convince the config system to put the\r
466 // "responsiveConfig" and "responsiveFormulas" in the initList. This needs to be\r
467 // done so that the initGetter is setup prior to calling transformInstanceConfig\r
468 // when we need to call the getters.\r
469\r
470 applyResponsiveConfig: function (rules) {\r
471 for (var rule in rules) {\r
472 rules[rule].fn = Ext.createRuleFn(rule);\r
473 }\r
474 return rules;\r
475 },\r
476\r
477 applyResponsiveFormulas: function (formulas) {\r
478 var ret = {},\r
479 fn, name;\r
480\r
481 if (formulas) {\r
482 for (name in formulas) {\r
483 if (Ext.isString(fn = formulas[name])) {\r
484 fn = Ext.createRuleFn(fn);\r
485 }\r
486 ret[name] = fn;\r
487 }\r
488 }\r
489\r
490 return ret;\r
491 },\r
492\r
493 /**\r
494 * Evaluates and returns the configs based on the `responsiveConfig`. This\r
495 * method relies on the state being captured by the `updateContext` method.\r
496 * @private\r
497 */\r
498 getResponsiveState: function () {\r
499 var context = Responsive.context,\r
500 rules = this.getResponsiveConfig(),\r
501 ret = {},\r
502 entry, rule;\r
503\r
504 if (rules) {\r
505 for (rule in rules) {\r
506 entry = rules[rule];\r
507 if (entry.fn.call(this, context)) {\r
508 Ext.merge(ret, entry.config);\r
509 }\r
510 }\r
511 }\r
512\r
513 return ret;\r
514 },\r
515\r
516 setupResponsiveContext: function () {\r
517 var formulas = this.getResponsiveFormulas(),\r
518 context = Responsive.context,\r
519 name;\r
520\r
521 if (formulas) {\r
522 for (name in formulas) {\r
523 context[name] = formulas[name].call(this, context);\r
524 }\r
525 }\r
526 },\r
527\r
528 /**\r
529 * This config system hook method is called just prior to processing the specified\r
530 * "instanceConfig". This hook returns the instanceConfig that will actually be\r
531 * processed by the config system.\r
532 * @param {Object} instanceConfig The user-supplied instance config object.\r
533 * @private\r
534 */\r
535 transformInstanceConfig: function (instanceConfig) {\r
536 var me = this,\r
537 ret;\r
538\r
539 Responsive.register(me);\r
540\r
541 // Since we are called immediately prior to the Configurator looking at the\r
542 // instanceConfig, that incoming value has not yet been merged on to\r
543 // "this.config". We need to call getResponsiveConfig and getResponsiveFormulas\r
544 // and still get all that merged goodness, so we have to do the merge here.\r
545\r
546 if (instanceConfig) {\r
547 Responsive.processConfig(me, instanceConfig, 'responsiveConfig');\r
548 Responsive.processConfig(me, instanceConfig, 'responsiveFormulas');\r
549 }\r
550\r
551 // For updates this is done in bulk prior to updating all of the responsive\r
552 // objects, but for instantiation, we have to do this for ourselves now.\r
553 me.setupResponsiveContext();\r
554\r
555 // Now we can merge the current responsive state with the incoming config.\r
556 // The responsiveConfig takes priority.\r
557 ret = me.getResponsiveState();\r
558\r
559 if (instanceConfig) {\r
560 ret = Ext.merge({}, instanceConfig, ret);\r
561\r
562 // We don't want these to remain since we've already handled them.\r
563 delete ret.responsiveConfig;\r
564 delete ret.responsiveFormulas;\r
565 }\r
566\r
567 return ret;\r
568 },\r
569\r
570 /**\r
571 * Evaluates and applies the `responsiveConfig` to this instance. This is called\r
572 * by `notify` automatically.\r
573 * @private\r
574 */\r
575 updateResponsiveState: function () {\r
576 var config = this.getResponsiveState();\r
577 this.setConfig(config);\r
578 }\r
579 } // private\r
580}});\r