]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * @private\r | |
3 | */\r | |
4 | Ext.define('Ext.layout.container.boxOverflow.Menu', {\r | |
5 | \r | |
6 | /* Begin Definitions */\r | |
7 | \r | |
8 | extend: 'Ext.layout.container.boxOverflow.None',\r | |
9 | requires: ['Ext.toolbar.Separator', 'Ext.button.Button'],\r | |
10 | alternateClassName: 'Ext.layout.boxOverflow.Menu',\r | |
11 | alias: [\r | |
12 | 'box.overflow.menu',\r | |
13 | 'box.overflow.Menu' // capitalized for 4.x compat\r | |
14 | ],\r | |
15 | \r | |
16 | /* End Definitions */\r | |
17 | \r | |
18 | /**\r | |
19 | * @property {String} noItemsMenuText\r | |
20 | * HTML fragment to render into the toolbar overflow menu if there are no items to display\r | |
21 | */\r | |
22 | noItemsMenuText : '<div class="' + Ext.baseCSSPrefix + 'toolbar-no-items" role="menuitem">(None)</div>',\r | |
23 | \r | |
24 | menuCls: Ext.baseCSSPrefix + 'box-menu',\r | |
25 | \r | |
26 | constructor: function(config) {\r | |
27 | var me = this;\r | |
28 | \r | |
29 | me.callParent([config]);\r | |
30 | \r | |
31 | /**\r | |
32 | * @property {Array} menuItems\r | |
33 | * Array of all items that are currently hidden and should go into the dropdown menu\r | |
34 | */\r | |
35 | me.menuItems = [];\r | |
36 | },\r | |
37 | \r | |
38 | beginLayout: function (ownerContext) {\r | |
39 | this.callParent([ownerContext]);\r | |
40 | \r | |
41 | // Before layout, we need to re-show all items which we may have hidden due to a\r | |
42 | // previous overflow...\r | |
43 | this.clearOverflow(ownerContext);\r | |
44 | },\r | |
45 | \r | |
46 | beginLayoutCycle: function (ownerContext, firstCycle) {\r | |
47 | this.callParent([ownerContext, firstCycle]);\r | |
48 | \r | |
49 | if (!firstCycle) {\r | |
50 | // if we are being re-run, we need to clear any overflow from the last run and\r | |
51 | // recache the childItems collection\r | |
52 | this.clearOverflow(ownerContext);\r | |
53 | \r | |
54 | this.layout.cacheChildItems(ownerContext);\r | |
55 | }\r | |
56 | },\r | |
57 | \r | |
58 | onRemove: function(comp) {\r | |
59 | Ext.Array.remove(this.menuItems, comp);\r | |
60 | },\r | |
61 | \r | |
62 | clearItem: function(comp) {\r | |
63 | var menu = comp.menu;\r | |
64 | \r | |
65 | if (comp.isButton && menu) {\r | |
66 | // If the button had a menu, forcibly set it\r | |
67 | // again so that the ownerCmp is reset correctly\r | |
68 | // and is no longer pointing at the overflow\r | |
69 | comp.setMenu(menu, false);\r | |
70 | }\r | |
71 | },\r | |
72 | \r | |
73 | // We don't define a prefix in menu overflow.\r | |
74 | getSuffixConfig: function() {\r | |
75 | var me = this,\r | |
76 | layout = me.layout,\r | |
77 | owner = layout.owner,\r | |
78 | oid = owner.id;\r | |
79 | \r | |
80 | /**\r | |
81 | * @private\r | |
82 | * @property {Ext.menu.Menu} menu\r | |
83 | * The expand menu - holds items for every item that cannot be shown\r | |
84 | * because the container is currently not large enough.\r | |
85 | */\r | |
86 | me.menu = new Ext.menu.Menu({\r | |
87 | listeners: {\r | |
88 | scope: me,\r | |
89 | beforeshow: me.beforeMenuShow\r | |
90 | }\r | |
91 | });\r | |
92 | \r | |
93 | /**\r | |
94 | * @private\r | |
95 | * @property {Ext.button.Button} menuTrigger\r | |
96 | * The expand button which triggers the overflow menu to be shown\r | |
97 | */\r | |
98 | me.menuTrigger = new Ext.button.Button({\r | |
99 | id: oid + '-menu-trigger',\r | |
100 | cls: me.menuCls + '-after ' + Ext.baseCSSPrefix + 'toolbar-item',\r | |
101 | plain: owner.usePlainButtons,\r | |
102 | ownerCt: owner, // To enable the Menu to ascertain a valid zIndexManager owner in the same tree\r | |
103 | ownerLayout: layout,\r | |
104 | iconCls: Ext.baseCSSPrefix + me.getOwnerType(owner) + '-more-icon',\r | |
105 | ui: owner.defaultButtonUI || 'default',\r | |
106 | menu: me.menu,\r | |
107 | // Menu will be empty when we're showing it because we populate items after\r | |
108 | showEmptyMenu: true,\r | |
109 | getSplitCls: function() { return '';}\r | |
110 | });\r | |
111 | \r | |
112 | return me.menuTrigger.getRenderTree();\r | |
113 | },\r | |
114 | \r | |
115 | getOverflowCls: function(direction) {\r | |
116 | return this.menuCls + '-body-' + direction;\r | |
117 | },\r | |
118 | \r | |
119 | handleOverflow: function(ownerContext) {\r | |
120 | var me = this,\r | |
121 | layout = me.layout;\r | |
122 | \r | |
123 | me.showTrigger(ownerContext);\r | |
124 | \r | |
125 | // Center the menuTrigger button only if we are not vertical.\r | |
126 | if (layout.direction !== 'vertical') {\r | |
127 | me.menuTrigger.setLocalY(\r | |
128 | (ownerContext.state.boxPlan.maxSize - me.menuTrigger[layout.names.getHeight]()) / 2\r | |
129 | );\r | |
130 | }\r | |
131 | \r | |
132 | return {\r | |
133 | reservedSpace: me.triggerTotalWidth\r | |
134 | };\r | |
135 | },\r | |
136 | \r | |
137 | /**\r | |
138 | * Finishes the render operation of the trigger Button.\r | |
139 | * @private\r | |
140 | */\r | |
141 | captureChildElements: function() {\r | |
142 | var me = this,\r | |
143 | menuTrigger = me.menuTrigger,\r | |
144 | names = me.layout.names;\r | |
145 | \r | |
146 | // The rendering flag is set when getRenderTree is called which we do when returning markup string for the owning layout's "suffix"\r | |
147 | if (menuTrigger.rendering) {\r | |
148 | menuTrigger.finishRender();\r | |
149 | me.triggerTotalWidth = menuTrigger[names.getWidth]() + menuTrigger.el.getMargin(names.parallelMargins);\r | |
150 | }\r | |
151 | },\r | |
152 | \r | |
153 | /**\r | |
154 | * @private\r | |
155 | * Called by the layout, when it determines that there is no overflow.\r | |
156 | * Also called as an interceptor to the layout's onLayout method to reshow\r | |
157 | * previously hidden overflowing items.\r | |
158 | */\r | |
159 | clearOverflow: function(ownerContext) {\r | |
160 | var me = this,\r | |
161 | items = me.menuItems,\r | |
162 | length = items.length,\r | |
163 | owner = me.layout.owner,\r | |
164 | asLayoutRoot = owner._asLayoutRoot,\r | |
165 | item, i;\r | |
166 | \r | |
167 | owner.suspendLayouts();\r | |
168 | me.captureChildElements();\r | |
169 | me.hideTrigger();\r | |
170 | owner.resumeLayouts();\r | |
171 | \r | |
172 | for (i = 0; i < length; i++) {\r | |
173 | item = items[i];\r | |
174 | \r | |
175 | // What we are doing here is preventing the layout bubble from invalidating our\r | |
176 | // owner component. We need just the button to be added to the layout run.\r | |
177 | item.suspendLayouts();\r | |
178 | item.show();\r | |
179 | me.clearItem(item);\r | |
180 | item.resumeLayouts(asLayoutRoot);\r | |
181 | }\r | |
182 | \r | |
183 | items.length = 0;\r | |
184 | },\r | |
185 | \r | |
186 | /**\r | |
187 | * @private\r | |
188 | * Shows the overflow trigger when enableOverflow is set to true and the items\r | |
189 | * in the layout are too wide to fit in the space available\r | |
190 | */\r | |
191 | showTrigger: function(ownerContext) {\r | |
192 | var me = this,\r | |
193 | layout = me.layout,\r | |
194 | owner = layout.owner,\r | |
195 | names = layout.names,\r | |
196 | startProp = names.x,\r | |
197 | sizeProp = names.width,\r | |
198 | plan = ownerContext.state.boxPlan,\r | |
199 | available = plan.targetSize[sizeProp],\r | |
200 | childItems = ownerContext.childItems,\r | |
201 | menuTrigger = me.menuTrigger,\r | |
202 | menuItems = me.menuItems,\r | |
203 | childContext, comp, i, props, len;\r | |
204 | \r | |
205 | // We don't want the menuTrigger.show to cause owner's layout to be invalidated, so\r | |
206 | // we force just the button to be invalidated and added to the current run.\r | |
207 | menuTrigger.suspendLayouts();\r | |
208 | menuTrigger.show();\r | |
209 | menuTrigger.resumeLayouts(me._asLayoutRoot);\r | |
210 | \r | |
211 | available -= me.triggerTotalWidth;\r | |
212 | \r | |
213 | owner.suspendLayouts();\r | |
214 | \r | |
215 | // Hide all items which are off the end, and store them to allow them to be restored\r | |
216 | // before each layout operation.\r | |
217 | for (i = 0, len = menuItems.length; i < len; ++i) {\r | |
218 | me.clearItem(menuItems[i]);\r | |
219 | }\r | |
220 | menuItems.length = 0;\r | |
221 | \r | |
222 | for (i = 0, len = childItems.length; i < len; i++) {\r | |
223 | childContext = childItems[i];\r | |
224 | props = childContext.props;\r | |
225 | if (props[startProp] + props[sizeProp] > available) {\r | |
226 | comp = childContext.target;\r | |
227 | me.menuItems.push(comp);\r | |
228 | comp.hide();\r | |
229 | }\r | |
230 | }\r | |
231 | \r | |
232 | owner.resumeLayouts();\r | |
233 | },\r | |
234 | \r | |
235 | /**\r | |
236 | * @private\r | |
237 | */\r | |
238 | hideTrigger: function() {\r | |
239 | var menuTrigger = this.menuTrigger;\r | |
240 | if (menuTrigger) {\r | |
241 | menuTrigger.hide();\r | |
242 | }\r | |
243 | },\r | |
244 | \r | |
245 | /**\r | |
246 | * @private\r | |
247 | * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can.\r | |
248 | */\r | |
249 | beforeMenuShow: function(menu) {\r | |
250 | var me = this,\r | |
251 | items = me.menuItems,\r | |
252 | i = 0,\r | |
253 | len = items.length,\r | |
254 | item,\r | |
255 | prev,\r | |
256 | needsSep = function(group, prev){\r | |
257 | return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);\r | |
258 | };\r | |
259 | \r | |
260 | menu.suspendLayouts();\r | |
261 | menu.removeAll(false);\r | |
262 | \r | |
263 | for (; i < len; i++) {\r | |
264 | item = items[i];\r | |
265 | \r | |
266 | // Do not show a separator as a first item\r | |
267 | if (!i && (item instanceof Ext.toolbar.Separator)) {\r | |
268 | continue;\r | |
269 | }\r | |
270 | if (prev && (needsSep(item, prev) || needsSep(prev, item))) {\r | |
271 | menu.add('-');\r | |
272 | }\r | |
273 | \r | |
274 | me.addComponentToMenu(menu, item);\r | |
275 | prev = item;\r | |
276 | }\r | |
277 | \r | |
278 | // put something so the menu isn't empty if no compatible items found\r | |
279 | if (menu.items.length < 1) {\r | |
280 | menu.add(me.noItemsMenuText);\r | |
281 | }\r | |
282 | menu.resumeLayouts();\r | |
283 | },\r | |
284 | \r | |
285 | /**\r | |
286 | * @private\r | |
287 | * Returns a menu config for a given component. This config is used to create a menu item\r | |
288 | * to be added to the expander menu\r | |
289 | * @param {Ext.Component} component The component to create the config for\r | |
290 | * @param {Boolean} hideOnClick Passed through to the menu item\r | |
291 | */\r | |
292 | createMenuConfig: function (component, hideOnClick) {\r | |
293 | var config = Ext.apply({}, component.initialConfig),\r | |
294 | group = component.toggleGroup;\r | |
295 | \r | |
296 | Ext.copy(config, component, [\r | |
297 | 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu', 'tabIndex'\r | |
298 | ]);\r | |
299 | \r | |
300 | Ext.applyIf(config, {\r | |
301 | text: component.overflowText || component.text,\r | |
302 | hideOnClick: hideOnClick,\r | |
303 | destroyMenu: false,\r | |
304 | listeners: null\r | |
305 | });\r | |
306 | config.masterComponent = component;\r | |
307 | \r | |
308 | // Clone must have same value, and must sync original's value on change\r | |
309 | if (component.isFormField) {\r | |
310 | config.value = component.getValue();\r | |
311 | \r | |
312 | // Sync the original component's value when the clone changes value.\r | |
313 | // This intentionally overwrites any developer-configured change listener on the clone.\r | |
314 | // That's because we monitor the clone's change event, and sync the\r | |
315 | // original field by calling setValue, so the original field's change\r | |
316 | // event will still fire.\r | |
317 | config.listeners = {\r | |
318 | change: function(c, newVal, oldVal) { \r | |
319 | c.masterComponent.setValue(newVal);\r | |
320 | }\r | |
321 | };\r | |
322 | // Sync the cloned Component's value when the master changes value.\r | |
323 | component.on('change', function(c, newVal, oldVal) {\r | |
324 | c.overflowClone.setValue(newVal);\r | |
325 | });\r | |
326 | }\r | |
327 | \r | |
328 | // ToggleButtons become CheckItems\r | |
329 | else if (group || component.enableToggle) {\r | |
330 | Ext.apply(config, {\r | |
331 | hideOnClick: false,\r | |
332 | group: group,\r | |
333 | checked: component.pressed,\r | |
334 | handler: function (item, e) {\r | |
335 | item.masterComponent.onClick(e);\r | |
336 | }\r | |
337 | });\r | |
338 | }\r | |
339 | \r | |
340 | // Buttons may have their text or icon changed - this must be propagated to the clone in the overflow menu\r | |
341 | if (component.isButton && !component.changeListenersAdded) {\r | |
342 | component.on({\r | |
343 | textchange: this.onButtonAttrChange,\r | |
344 | iconchange: this.onButtonAttrChange,\r | |
345 | toggle: this.onButtonToggle\r | |
346 | });\r | |
347 | component.changeListenersAdded = true;\r | |
348 | }\r | |
349 | \r | |
350 | // Typically margins are used to separate items in a toolbar\r | |
351 | // but don't really make a lot of sense in a menu, so we strip\r | |
352 | // them out here.\r | |
353 | delete config.margin;\r | |
354 | delete config.ownerCt;\r | |
355 | delete config.xtype;\r | |
356 | delete config.id;\r | |
357 | delete config.itemId;\r | |
358 | return config;\r | |
359 | },\r | |
360 | \r | |
361 | onButtonAttrChange: function(btn) {\r | |
362 | var clone = btn.overflowClone;\r | |
363 | clone.suspendLayouts();\r | |
364 | clone.setText(btn.text);\r | |
365 | clone.setIcon(btn.icon);\r | |
366 | clone.setIconCls(btn.iconCls);\r | |
367 | clone.resumeLayouts(true);\r | |
368 | },\r | |
369 | \r | |
370 | onButtonToggle: function(btn, state) {\r | |
371 | // Keep the clone in sync with the original if necessary\r | |
372 | if (btn.overflowClone.checked !== state) {\r | |
373 | btn.overflowClone.setChecked(state);\r | |
374 | }\r | |
375 | },\r | |
376 | \r | |
377 | /**\r | |
378 | * @private\r | |
379 | * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually.\r | |
380 | * @param {Ext.menu.Menu} menu The menu to add to\r | |
381 | * @param {Ext.Component} component The component to add\r | |
382 | * TODO: Implement overrides in Ext.layout.container.boxOverflow which create overrides\r | |
383 | * for SplitButton, Button, ButtonGroup, and TextField. And a generic one for Component\r | |
384 | * which create clones suitable for use in an overflow menu.\r | |
385 | */\r | |
386 | addComponentToMenu: function(menu, component) {\r | |
387 | var me = this,\r | |
388 | i, items, iLen;\r | |
389 | \r | |
390 | // No equivalent to fill, skip it\r | |
391 | if (component instanceof Ext.toolbar.Fill) {\r | |
392 | return;\r | |
393 | }\r | |
394 | // Separator maps to MenuSeparator\r | |
395 | else if (component instanceof Ext.toolbar.Separator) {\r | |
396 | menu.add('-');\r | |
397 | }\r | |
398 | else if (component.overflowClone) {\r | |
399 | menu.add(component.overflowClone);\r | |
400 | }\r | |
401 | // Other types...\r | |
402 | else if (component.isComponent) {\r | |
403 | if (component.isXType('splitbutton')) {\r | |
404 | component.overflowClone = menu.add(me.createMenuConfig(component, true));\r | |
405 | \r | |
406 | } else if (component.isXType('button')) {\r | |
407 | component.overflowClone = menu.add(me.createMenuConfig(component, !component.menu));\r | |
408 | \r | |
409 | } else if (component.isXType('buttongroup')) {\r | |
410 | items = component.items.items;\r | |
411 | iLen = items.length;\r | |
412 | \r | |
413 | for (i = 0; i < iLen; i++) {\r | |
414 | me.addComponentToMenu(menu, items[i]);\r | |
415 | }\r | |
416 | } else {\r | |
417 | component.overflowClone = menu.add(Ext.create(Ext.getClassName(component), me.createMenuConfig(component)));\r | |
418 | }\r | |
419 | }\r | |
420 | },\r | |
421 | \r | |
422 | destroy: function() {\r | |
423 | var me = this,\r | |
424 | trigger = me.menuTrigger;\r | |
425 | \r | |
426 | if (trigger && !me.layout.owner.items.contains(trigger)) {\r | |
427 | // Ensure we delete the ownerCt if it's not in the items\r | |
428 | // so we don't get spurious container remove warnings.\r | |
429 | delete trigger.ownerCt;\r | |
430 | }\r | |
431 | me.menu = me.menuTrigger = Ext.destroy(me.menu, trigger);\r | |
432 | me.callParent();\r | |
433 | }\r | |
434 | }); |