]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * This layout manages multiple child Components, each fitted to the Container, where only a single child Component can be\r | |
3 | * visible at any given time. This layout style is most commonly used for wizards, tab implementations, etc.\r | |
4 | * This class is intended to be extended or created via the layout:'card' {@link Ext.container.Container#layout} config,\r | |
5 | * and should generally not need to be created directly via the new keyword.\r | |
6 | *\r | |
7 | * The CardLayout's focal method is {@link #setActiveItem}. Since only one panel is displayed at a time,\r | |
8 | * the only way to move from one Component to the next is by calling setActiveItem, passing the next panel to display\r | |
9 | * (or its id or index). The layout itself does not provide a user interface for handling this navigation,\r | |
10 | * so that functionality must be provided by the developer.\r | |
11 | *\r | |
12 | * To change the active card of a container, call the setActiveItem method of its layout:\r | |
13 | *\r | |
14 | * @example\r | |
15 | * var p = Ext.create('Ext.panel.Panel', {\r | |
16 | * layout: 'card',\r | |
17 | * items: [\r | |
18 | * { html: 'Card 1' },\r | |
19 | * { html: 'Card 2' }\r | |
20 | * ],\r | |
21 | * renderTo: Ext.getBody()\r | |
22 | * });\r | |
23 | *\r | |
24 | * p.getLayout().setActiveItem(1);\r | |
25 | * \r | |
26 | * The {@link Ext.Component#beforedeactivate beforedeactivate} and {@link Ext.Component#beforeactivate beforeactivate}\r | |
27 | * events can be used to prevent a card from activating or deactivating by returning `false`.\r | |
28 | * \r | |
29 | * @example \r | |
30 | * var active = 0;\r | |
31 | * var main = Ext.create('Ext.panel.Panel', {\r | |
32 | * renderTo: Ext.getBody(),\r | |
33 | * width: 200,\r | |
34 | * height: 200,\r | |
35 | * layout: 'card',\r | |
36 | * tbar: [{\r | |
37 | * text: 'Next',\r | |
38 | * handler: function(){\r | |
39 | * var layout = main.getLayout();\r | |
40 | * ++active;\r | |
41 | * layout.setActiveItem(active);\r | |
42 | * active = main.items.indexOf(layout.getActiveItem());\r | |
43 | * }\r | |
44 | * }],\r | |
45 | * items: [{\r | |
46 | * title: 'P1'\r | |
47 | * }, {\r | |
48 | * title: 'P2'\r | |
49 | * }, {\r | |
50 | * title: 'P3',\r | |
51 | * listeners: {\r | |
52 | * beforeactivate: function(){\r | |
53 | * return false;\r | |
54 | * }\r | |
55 | * }\r | |
56 | * }]\r | |
57 | * });\r | |
58 | *\r | |
59 | * In the following example, a simplistic wizard setup is demonstrated. A button bar is added\r | |
60 | * to the footer of the containing panel to provide navigation buttons. The buttons will be handled by a\r | |
61 | * common navigation routine. Note that other uses of a CardLayout (like a tab control) would require a\r | |
62 | * completely different implementation. For serious implementations, a better approach would be to extend\r | |
63 | * CardLayout to provide the custom functionality needed.\r | |
64 | *\r | |
65 | * @example\r | |
66 | * var navigate = function(panel, direction){\r | |
67 | * // This routine could contain business logic required to manage the navigation steps.\r | |
68 | * // It would call setActiveItem as needed, manage navigation button state, handle any\r | |
69 | * // branching logic that might be required, handle alternate actions like cancellation\r | |
70 | * // or finalization, etc. A complete wizard implementation could get pretty\r | |
71 | * // sophisticated depending on the complexity required, and should probably be\r | |
72 | * // done as a subclass of CardLayout in a real-world implementation.\r | |
73 | * var layout = panel.getLayout();\r | |
74 | * layout[direction]();\r | |
75 | * Ext.getCmp('move-prev').setDisabled(!layout.getPrev());\r | |
76 | * Ext.getCmp('move-next').setDisabled(!layout.getNext());\r | |
77 | * };\r | |
78 | *\r | |
79 | * Ext.create('Ext.panel.Panel', {\r | |
80 | * title: 'Example Wizard',\r | |
81 | * width: 300,\r | |
82 | * height: 200,\r | |
83 | * layout: 'card',\r | |
84 | * bodyStyle: 'padding:15px',\r | |
85 | * defaults: {\r | |
86 | * // applied to each contained panel\r | |
87 | * border: false\r | |
88 | * },\r | |
89 | * // just an example of one possible navigation scheme, using buttons\r | |
90 | * bbar: [\r | |
91 | * {\r | |
92 | * id: 'move-prev',\r | |
93 | * text: 'Back',\r | |
94 | * handler: function(btn) {\r | |
95 | * navigate(btn.up("panel"), "prev");\r | |
96 | * },\r | |
97 | * disabled: true\r | |
98 | * },\r | |
99 | * '->', // greedy spacer so that the buttons are aligned to each side\r | |
100 | * {\r | |
101 | * id: 'move-next',\r | |
102 | * text: 'Next',\r | |
103 | * handler: function(btn) {\r | |
104 | * navigate(btn.up("panel"), "next");\r | |
105 | * }\r | |
106 | * }\r | |
107 | * ],\r | |
108 | * // the panels (or "cards") within the layout\r | |
109 | * items: [{\r | |
110 | * id: 'card-0',\r | |
111 | * html: '<h1>Welcome to the Wizard!</h1><p>Step 1 of 3</p>'\r | |
112 | * },{\r | |
113 | * id: 'card-1',\r | |
114 | * html: '<p>Step 2 of 3</p>'\r | |
115 | * },{\r | |
116 | * id: 'card-2',\r | |
117 | * html: '<h1>Congratulations!</h1><p>Step 3 of 3 - Complete</p>'\r | |
118 | * }],\r | |
119 | * renderTo: Ext.getBody()\r | |
120 | * });\r | |
121 | */\r | |
122 | Ext.define('Ext.layout.container.Card', {\r | |
123 | \r | |
124 | /* Begin Definitions */\r | |
125 | \r | |
126 | extend: 'Ext.layout.container.Fit',\r | |
127 | \r | |
128 | alternateClassName: 'Ext.layout.CardLayout',\r | |
129 | \r | |
130 | alias: 'layout.card',\r | |
131 | \r | |
132 | /* End Definitions */\r | |
133 | \r | |
134 | type: 'card',\r | |
135 | \r | |
136 | hideInactive: true,\r | |
137 | \r | |
138 | /**\r | |
139 | * @cfg {Boolean} deferredRender\r | |
140 | * True to render each contained item at the time it becomes active, false to render all contained items\r | |
141 | * as soon as the layout is rendered (defaults to false). If there is a significant amount of content or\r | |
142 | * a lot of heavy controls being rendered into panels that are not displayed by default, setting this to\r | |
143 | * true might improve performance.\r | |
144 | */\r | |
145 | deferredRender : false,\r | |
146 | \r | |
147 | getRenderTree: function () {\r | |
148 | var me = this,\r | |
149 | activeItem = me.getActiveItem();\r | |
150 | \r | |
151 | if (activeItem) {\r | |
152 | \r | |
153 | // If they veto the activate, we have no active item\r | |
154 | if (activeItem.hasListeners.beforeactivate && activeItem.fireEvent('beforeactivate', activeItem) === false) {\r | |
155 | \r | |
156 | // We must null our activeItem reference, AND the one in our owning Container.\r | |
157 | // Because upon layout invalidation, renderChildren will use this.getActiveItem which\r | |
158 | // uses this.activeItem || this.owner.activeItem\r | |
159 | activeItem = me.activeItem = me.owner.activeItem = null;\r | |
160 | }\r | |
161 | \r | |
162 | // Item is to be the active one. Fire event after it is first layed out\r | |
163 | else if (activeItem.hasListeners.activate) {\r | |
164 | activeItem.on({\r | |
165 | boxready: function() {\r | |
166 | activeItem.fireEvent('activate', activeItem);\r | |
167 | },\r | |
168 | single: true\r | |
169 | });\r | |
170 | }\r | |
171 | \r | |
172 | if (me.deferredRender) {\r | |
173 | if (activeItem) {\r | |
174 | return me.getItemsRenderTree([activeItem]);\r | |
175 | }\r | |
176 | } else {\r | |
177 | return me.callParent(arguments);\r | |
178 | }\r | |
179 | }\r | |
180 | },\r | |
181 | \r | |
182 | renderChildren: function () {\r | |
183 | var me = this,\r | |
184 | active = me.getActiveItem();\r | |
185 | \r | |
186 | if (!me.deferredRender) {\r | |
187 | me.callParent();\r | |
188 | } else if (active) {\r | |
189 | // ensure the active item is configured for the layout\r | |
190 | me.renderItems([active], me.getRenderTarget());\r | |
191 | }\r | |
192 | },\r | |
193 | \r | |
194 | isValidParent : function(item, target, position) {\r | |
195 | // Note: Card layout does not care about order within the target because only one is ever visible.\r | |
196 | // We only care whether the item is a direct child of the target.\r | |
197 | var itemEl = item.el ? item.el.dom : Ext.getDom(item);\r | |
198 | return (itemEl && itemEl.parentNode === (target.dom || target)) || false;\r | |
199 | },\r | |
200 | \r | |
201 | /**\r | |
202 | * Return the active (visible) component in the layout.\r | |
203 | * @return {Ext.Component}\r | |
204 | */\r | |
205 | getActiveItem: function() {\r | |
206 | var me = this,\r | |
207 | // It's necessary to check that me.activeItem is not undefined as it could be 0 (falsey). We're more interested in\r | |
208 | // checking the layout's activeItem property, since that is the source of truth for an activeItem. If it's\r | |
209 | // determined to be empty, check the owner. Note that a default item is returned if activeItem is `undefined` but\r | |
210 | // not `null`. Also, note that `null` is legitimate value and completely different from `undefined`.\r | |
211 | item = me.activeItem === undefined ? (me.owner && me.owner.activeItem) : me.activeItem,\r | |
212 | result = me.parseActiveItem(item);\r | |
213 | \r | |
214 | // Sanitize the result in case the active item is no longer there.\r | |
215 | if (result && me.owner.items.indexOf(result) !== -1) {\r | |
216 | me.activeItem = result;\r | |
217 | }\r | |
218 | \r | |
219 | // Note that in every use case me.activeItem will have a truthy value except for when a container or tabpanel is explicity\r | |
220 | // configured with activeItem/Tab === null or when an out-of-range index is given for an active tab (as it will be undefined).\r | |
221 | // In those cases, it is meaningful to return the null value, so do so.\r | |
222 | return result == null ? null : (me.activeItem || me.owner.activeItem);\r | |
223 | },\r | |
224 | \r | |
225 | /**\r | |
226 | * @private\r | |
227 | */\r | |
228 | parseActiveItem: function (item) {\r | |
229 | var activeItem;\r | |
230 | \r | |
231 | if (item && item.isComponent) {\r | |
232 | activeItem = item;\r | |
233 | } else if (typeof item === 'number' || item === undefined) {\r | |
234 | activeItem = this.getLayoutItems()[item || 0];\r | |
235 | } else if (item === null) {\r | |
236 | activeItem = null;\r | |
237 | } else {\r | |
238 | activeItem = this.owner.getComponent(item);\r | |
239 | }\r | |
240 | \r | |
241 | return activeItem;\r | |
242 | },\r | |
243 | \r | |
244 | /**\r | |
245 | * @private\r | |
246 | * Called before both dynamic render, and bulk render.\r | |
247 | * Ensure that the active item starts visible, and inactive ones start invisible.\r | |
248 | */\r | |
249 | configureItem: function(item) {\r | |
250 | item.setHiddenState(item !== this.getActiveItem());\r | |
251 | this.callParent(arguments);\r | |
252 | },\r | |
253 | \r | |
254 | onAdd: function (item, pos) {\r | |
255 | this.callParent([item, pos]);\r | |
256 | this.setItemHideMode(item);\r | |
257 | },\r | |
258 | \r | |
259 | onRemove: function(component) {\r | |
260 | var me = this;\r | |
261 | \r | |
262 | me.callParent([component]);\r | |
263 | me.resetItemHideMode(component);\r | |
264 | \r | |
265 | if (component === me.activeItem) {\r | |
266 | // Note setting to `undefined` is intentional. Don't null it out since null now has a specific meaning in\r | |
267 | // tab management (it specifies not setting an active item).\r | |
268 | me.activeItem = undefined;\r | |
269 | }\r | |
270 | },\r | |
271 | \r | |
272 | /**\r | |
273 | * @private\r | |
274 | */\r | |
275 | getAnimation: function(newCard, owner) {\r | |
276 | var newAnim = (newCard || {}).cardSwitchAnimation;\r | |
277 | if (newAnim === false) {\r | |
278 | return false;\r | |
279 | }\r | |
280 | return newAnim || owner.cardSwitchAnimation;\r | |
281 | },\r | |
282 | \r | |
283 | /**\r | |
284 | * Return the active (visible) component in the layout to the next card\r | |
285 | * @return {Ext.Component} The next component or false.\r | |
286 | */\r | |
287 | getNext: function() {\r | |
288 | var wrap = arguments[0],\r | |
289 | items = this.getLayoutItems(),\r | |
290 | index = Ext.Array.indexOf(items, this.activeItem);\r | |
291 | \r | |
292 | return items[index + 1] || (wrap ? items[0] : false);\r | |
293 | },\r | |
294 | \r | |
295 | /**\r | |
296 | * Sets the active (visible) component in the layout to the next card\r | |
297 | * @return {Ext.Component} the activated component or false when nothing activated.\r | |
298 | */\r | |
299 | next: function() {\r | |
300 | var anim = arguments[0], \r | |
301 | wrap = arguments[1];\r | |
302 | return this.setActiveItem(this.getNext(wrap), anim);\r | |
303 | },\r | |
304 | \r | |
305 | /**\r | |
306 | * Return the active (visible) component in the layout to the previous card\r | |
307 | * @return {Ext.Component} The previous component or false.\r | |
308 | */\r | |
309 | getPrev: function() {\r | |
310 | var wrap = arguments[0],\r | |
311 | items = this.getLayoutItems(),\r | |
312 | index = Ext.Array.indexOf(items, this.activeItem);\r | |
313 | \r | |
314 | return items[index - 1] || (wrap ? items[items.length - 1] : false);\r | |
315 | },\r | |
316 | \r | |
317 | /**\r | |
318 | * Sets the active (visible) component in the layout to the previous card\r | |
319 | * @return {Ext.Component} the activated component or false when nothing activated.\r | |
320 | */\r | |
321 | prev: function() {\r | |
322 | var anim = arguments[0], \r | |
323 | wrap = arguments[1];\r | |
324 | return this.setActiveItem(this.getPrev(wrap), anim);\r | |
325 | },\r | |
326 | \r | |
327 | /**\r | |
328 | * Makes the given card active.\r | |
329 | *\r | |
330 | * var card1 = Ext.create('Ext.panel.Panel', {itemId: 'card-1'});\r | |
331 | * var card2 = Ext.create('Ext.panel.Panel', {itemId: 'card-2'});\r | |
332 | * var panel = Ext.create('Ext.panel.Panel', {\r | |
333 | * layout: 'card',\r | |
334 | * activeItem: 0,\r | |
335 | * items: [card1, card2]\r | |
336 | * });\r | |
337 | * // These are all equivalent\r | |
338 | * panel.getLayout().setActiveItem(card2);\r | |
339 | * panel.getLayout().setActiveItem('card-2');\r | |
340 | * panel.getLayout().setActiveItem(1);\r | |
341 | *\r | |
342 | * @param {Ext.Component/Number/String} newCard The component, component {@link Ext.Component#id id},\r | |
343 | * {@link Ext.Component#itemId itemId}, or index of component.\r | |
344 | * @return {Ext.Component} the activated component or false when nothing activated.\r | |
345 | * False is returned also when trying to activate an already active card.\r | |
346 | */\r | |
347 | setActiveItem: function(newCard) {\r | |
348 | var me = this,\r | |
349 | owner = me.owner,\r | |
350 | oldCard = me.activeItem,\r | |
351 | rendered = owner.rendered,\r | |
352 | newIndex, focusNewCard;\r | |
353 | \r | |
354 | newCard = me.parseActiveItem(newCard);\r | |
355 | newIndex = owner.items.indexOf(newCard);\r | |
356 | \r | |
357 | // If the card is not a child of the owner, then add it.\r | |
358 | // Without doing a layout!\r | |
359 | if (newIndex === -1) {\r | |
360 | newIndex = owner.items.items.length;\r | |
361 | Ext.suspendLayouts();\r | |
362 | newCard = owner.add(newCard);\r | |
363 | Ext.resumeLayouts();\r | |
364 | }\r | |
365 | \r | |
366 | // Is this a valid, different card?\r | |
367 | if (newCard && oldCard !== newCard) {\r | |
368 | // Fire the beforeactivate and beforedeactivate events on the cards\r | |
369 | if (newCard.fireEvent('beforeactivate', newCard, oldCard) === false) {\r | |
370 | return false;\r | |
371 | }\r | |
372 | if (oldCard && oldCard.fireEvent('beforedeactivate', oldCard, newCard) === false) {\r | |
373 | return false;\r | |
374 | }\r | |
375 | \r | |
376 | if (rendered) {\r | |
377 | Ext.suspendLayouts();\r | |
378 | \r | |
379 | // If the card has not been rendered yet, now is the time to do so.\r | |
380 | if (!newCard.rendered) {\r | |
381 | me.renderItem(newCard, me.getRenderTarget(), owner.items.length);\r | |
382 | }\r | |
383 | \r | |
384 | if (oldCard) {\r | |
385 | if (me.hideInactive) {\r | |
386 | focusNewCard = oldCard.el.contains(Ext.Element.getActiveElement());\r | |
387 | \r | |
388 | oldCard.hide();\r | |
389 | \r | |
390 | if (oldCard.hidden) {\r | |
391 | oldCard.hiddenByLayout = true;\r | |
392 | oldCard.fireEvent('deactivate', oldCard, newCard);\r | |
393 | }\r | |
394 | // Hide was vetoed, we cannot change cards.\r | |
395 | else {\r | |
396 | return false;\r | |
397 | }\r | |
398 | }\r | |
399 | }\r | |
400 | \r | |
401 | // Make sure the new card is shown\r | |
402 | if (newCard.hidden) {\r | |
403 | newCard.show();\r | |
404 | }\r | |
405 | \r | |
406 | // Layout needs activeItem to be correct, so clear it if the show has been vetoed,\r | |
407 | // set it if the show has *not* been vetoed.\r | |
408 | if (newCard.hidden) {\r | |
409 | me.activeItem = newCard = null;\r | |
410 | } else {\r | |
411 | me.activeItem = newCard;\r | |
412 | \r | |
413 | // If the card being hidden contained focus, attempt to focus the new card\r | |
414 | // So as not to leave focus undefined.\r | |
415 | // The focus() call will focus the defaultFocus if it is a container\r | |
416 | // so ensure there is a defaultFocus.\r | |
417 | if (focusNewCard) {\r | |
418 | if (!newCard.defaultFocus) {\r | |
419 | newCard.defaultFocus = ':focusable';\r | |
420 | }\r | |
421 | newCard.focus();\r | |
422 | }\r | |
423 | }\r | |
424 | \r | |
425 | Ext.resumeLayouts(true);\r | |
426 | } else {\r | |
427 | me.activeItem = newCard;\r | |
428 | }\r | |
429 | \r | |
430 | newCard.fireEvent('activate', newCard, oldCard);\r | |
431 | \r | |
432 | return me.activeItem;\r | |
433 | }\r | |
434 | return false;\r | |
435 | },\r | |
436 | \r | |
437 | /**\r | |
438 | * @private\r | |
439 | * Reset back to initial config when item is removed from the panel.\r | |
440 | */\r | |
441 | resetItemHideMode: function (item) {\r | |
442 | item.hideMode = item.originalHideMode;\r | |
443 | delete item.originalHideMode;\r | |
444 | },\r | |
445 | \r | |
446 | /**\r | |
447 | * @private\r | |
448 | * A card layout items must have its visibility mode set to OFFSETS so its scroll\r | |
449 | * positions isn't reset when hidden.\r | |
450 | *\r | |
451 | * Do this automatically when an item is added to the panel.\r | |
452 | */\r | |
453 | setItemHideMode: function (item) {\r | |
454 | item.originalHideMode = item.hideMode;\r | |
455 | item.hideMode = 'offsets';\r | |
456 | }\r | |
457 | });\r |