]>
Commit | Line | Data |
---|---|---|
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 | |
68 | Ext.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 |