/**
* @private
*/
Ext.define('Ext.layout.container.boxOverflow.Menu', {
extend: 'Ext.layout.container.boxOverflow.None',
alternateClassName: 'Ext.layout.boxOverflow.Menu',
alias: [
'box.overflow.menu',
'box.overflow.Menu' // capitalized for 4.x compat
],
requires: [
'Ext.toolbar.Separator',
'Ext.button.Button'
],
/**
* @property {String} noItemsMenuText
* HTML fragment to render into the toolbar overflow menu if there are no items to display
*/
noItemsMenuText: '<div class="' + Ext.baseCSSPrefix +
'toolbar-no-items" role="menuitem">(None)</div>',
menuCls: Ext.baseCSSPrefix + 'box-menu',
/**
* The CSS class that gets added to toolbar items that have been
* overflowed to the overflow menu.
*
* @private
* @since 7.5.0
*/
menuItemOverflowedCls: Ext.baseCSSPrefix + 'menu-item-overflowed',
constructor: function(config) {
var me = this;
me.callParent([config]);
/**
* @property {Array} menuItems
* Array of all items that are currently hidden and should go into the dropdown menu
*/
me.menuItems = [];
},
beginLayout: function(ownerContext) {
this.callParent([ownerContext]);
// Before layout, we need to re-show all items which we may have hidden due to a
// previous overflow...
this.clearOverflow(ownerContext);
},
beginLayoutCycle: function(ownerContext, firstCycle) {
this.callParent([ownerContext, firstCycle]);
if (!firstCycle) {
// if we are being re-run, we need to clear any overflow from the last run and
// recache the childItems collection
this.clearOverflow(ownerContext);
this.layout.cacheChildItems(ownerContext);
}
},
onRemove: function(comp) {
Ext.Array.remove(this.menuItems, comp);
},
clearItem: function(comp) {
var menu = comp.menu;
if (comp.isButton && menu) {
// If the button had a menu, forcibly set it
// again so that the ownerCmp is reset correctly
// and is no longer pointing at the overflow
comp.setMenu(menu, false);
}
},
// We don't define a prefix in menu overflow.
getSuffixConfig: function() {
var me = this,
layout = me.layout,
owner = layout.owner,
oid = owner.id;
/**
* @private
* @property {Ext.menu.Menu} menu
* The expand menu - holds items for every item that cannot be shown
* because the container is currently not large enough.
*/
me.menu = new Ext.menu.Menu({
listeners: {
scope: me,
beforeshow: me.beforeMenuShow
}
});
/**
* @private
* @property {Ext.button.Button} menuTrigger
* The expand button which triggers the overflow menu to be shown
*/
me.menuTrigger = new Ext.button.Button({
id: oid + '-menu-trigger',
cls: me.menuCls + '-after ' + Ext.baseCSSPrefix + 'toolbar-item',
plain: owner.usePlainButtons,
// To enable the Menu to ascertain a valid zIndexManager owner in the same tree
ownerCt: owner,
ownerLayout: layout,
iconCls: Ext.baseCSSPrefix + me.getOwnerType(owner) + '-more-icon',
ui: owner.defaultButtonUI || 'default',
menu: me.menu,
// Menu will be empty when we're showing it because we populate items after
showEmptyMenu: true,
getSplitCls: function() {
return '';
}
});
return me.menuTrigger.getRenderTree();
},
getOverflowCls: function(direction) {
return this.menuCls + '-body-' + direction;
},
handleOverflow: function(ownerContext) {
var me = this,
layout = me.layout;
me.showTrigger(ownerContext);
// Center the menuTrigger button only if we are not vertical.
if (layout.direction !== 'vertical') {
me.menuTrigger.setLocalY(
(ownerContext.state.boxPlan.maxSize - me.menuTrigger[layout.names.getHeight]()) / 2
);
}
return {
reservedSpace: me.triggerTotalWidth
};
},
/**
* Finishes the render operation of the trigger Button.
* @private
*/
captureChildElements: function() {
var me = this,
menuTrigger = me.menuTrigger,
names = me.layout.names;
// The rendering flag is set when getRenderTree is called which we do
// when returning markup string for the owning layout's "suffix"
if (menuTrigger.rendering) {
menuTrigger.finishRender();
me.triggerTotalWidth = menuTrigger[names.getWidth]() +
menuTrigger.el.getMargin(names.parallelMargins);
}
},
/**
* @private
* Called by the layout, when it determines that there is no overflow.
* Also called as an interceptor to the layout's onLayout method to reshow
* previously hidden overflowing items.
*/
clearOverflow: function(ownerContext) {
var me = this,
items = me.menuItems,
length = items.length,
owner = me.layout.owner,
asLayoutRoot = owner._asLayoutRoot,
item, i;
owner.suspendLayouts();
me.captureChildElements();
me.hideTrigger();
owner.resumeLayouts();
for (i = 0; i < length; i++) {
item = items[i];
// What we are doing here is preventing the layout bubble from invalidating our
// owner component. We need just the button to be added to the layout run.
item.suspendLayouts();
item.removeCls(me.menuItemOverflowedCls);
me.clearItem(item);
item.resumeLayouts(asLayoutRoot);
}
items.length = 0;
},
/**
* @private
* Shows the overflow trigger when enableOverflow is set to true and the items
* in the layout are too wide to fit in the space available
*/
showTrigger: function(ownerContext) {
var me = this,
layout = me.layout,
owner = layout.owner,
names = layout.names,
startProp = names.x,
sizeProp = names.width,
plan = ownerContext.state.boxPlan,
available = plan.targetSize[sizeProp],
childItems = ownerContext.childItems,
menuTrigger = me.menuTrigger,
menuItems = me.menuItems,
childContext, comp, i, props, len;
// We don't want the menuTrigger.show to cause owner's layout to be invalidated, so
// we force just the button to be invalidated and added to the current run.
menuTrigger.suspendLayouts();
menuTrigger.show();
menuTrigger.resumeLayouts(me._asLayoutRoot);
available -= me.triggerTotalWidth;
owner.suspendLayouts();
// Hide all items which are off the end, and store them to allow them to be restored
// before each layout operation.
for (i = 0, len = menuItems.length; i < len; ++i) {
me.clearItem(menuItems[i]);
}
menuItems.length = 0;
for (i = 0, len = childItems.length; i < len; i++) {
childContext = childItems[i];
props = childContext.props;
if (props[startProp] + props[sizeProp] > available) {
comp = childContext.target;
me.menuItems.push(comp);
comp.addCls(me.menuItemOverflowedCls);
}
}
owner.resumeLayouts();
},
/**
* @private
*/
hideTrigger: function() {
var menuTrigger = this.menuTrigger;
if (menuTrigger) {
menuTrigger.hide();
}
},
/**
* @private
* Called before the overflow menu is shown. This constructs the menu's items,
* caching them for as long as it can.
*/
beforeMenuShow: function(menu) {
var me = this,
items = me.menuItems,
i = 0,
len = items.length,
item,
prev,
needsSep = function(group, prev) {
return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);
};
menu.suspendLayouts();
menu.removeAll(false);
for (; i < len; i++) {
item = items[i];
// Do not show a separator as a first item
if (!i && (item instanceof Ext.toolbar.Separator)) {
continue;
}
if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
menu.add('-');
}
me.addComponentToMenu(menu, item);
prev = item;
}
// put something so the menu isn't empty if no compatible items found
if (menu.items.length < 1) {
menu.add(me.noItemsMenuText);
}
menu.resumeLayouts();
},
/**
* @private
* Returns a menu config for a given component. This config is used to create a menu item
* to be added to the expander menu
* @param {Ext.Component} component The component to create the config for
* @param {Boolean} hideOnClick Passed through to the menu item
*/
createMenuConfig: function(component, hideOnClick) {
var config = Ext.apply({}, component.initialConfig),
group = component.toggleGroup;
Ext.copy(config, component, [
'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu', 'tabIndex'
]);
Ext.applyIf(config, {
hideOnClick: hideOnClick,
destroyMenu: false,
listeners: null
});
config.text = component.overflowText || component.text;
config.masterComponent = component;
// Clone must have same value, and must sync original's value on change
if (component.isFormField) {
config.value = component.getValue();
// If the component is a Checkbox/Radio field we replace the config with
// a menucheckitem so it will give the Menu a better look and feel.
// See additional information on the #addComponentToMenu method below.
if (component instanceof Ext.form.field.Checkbox) {
config = {
xtype: 'menucheckitem',
group: component.isRadio ? component.name + '_clone' : undefined,
text: component.boxLabel || component.fieldLabel,
name: component.name,
masterComponent: component,
checked: component.getValue(),
hideOnClick: false,
checkChangeDisabled: true
};
}
// Sync the original component's value when the clone changes value.
// This intentionally overwrites any developer-configured change listener on the clone.
// That's because we monitor the clone's change event, and sync the
// original field by calling setValue, so the original field's change
// event will still fire.
config.listeners = {
change: function(c, newVal, oldVal) {
c.masterComponent.setValue(newVal);
}
};
// Sync the cloned Component's value when the master changes value.
component.on('change', function(c, newVal, oldVal) {
c.overflowClone.setValue(newVal);
});
}
// ToggleButtons become CheckItems
else if (group || component.enableToggle) {
Ext.apply(config, {
hideOnClick: false,
group: group,
checked: component.pressed,
handler: function(item, e) {
item.masterComponent.onClick(e);
}
});
}
// Buttons may have their text or icon changed - this must be propagated
// to the clone in the overflow menu
if (component.isButton && !component.changeListenersAdded) {
component.on({
textchange: this.onButtonAttrChange,
iconchange: this.onButtonAttrChange,
toggle: this.onButtonToggle
});
component.changeListenersAdded = true;
}
// Adding additional listeners
component.on({
enable: this.onComponentStatusChange,
disable: this.onComponentStatusChange
});
// Typically margins are used to separate items in a toolbar
// but don't really make a lot of sense in a menu, so we strip
// them out here.
delete config.margin;
delete config.ownerCt;
delete config.xtype;
delete config.id;
delete config.itemId;
return config;
},
onButtonAttrChange: function(btn) {
var clone = btn.overflowClone;
clone.suspendLayouts();
clone.setText(btn.text);
clone.setIcon(btn.icon);
clone.setIconCls(btn.iconCls);
clone.resumeLayouts(true);
},
onButtonToggle: function(btn, state) {
// Keep the clone in sync with the original if necessary
if (btn.overflowClone.checked !== state) {
btn.overflowClone.setChecked(state);
}
},
onComponentStatusChange: function(cmp) {
var clone = cmp.overflowClone;
if (clone) {
clone.setDisabled(cmp.disabled);
}
},
/**
* @private
* Adds the given Toolbar item to the given menu. Buttons inside a buttongroup
* are added individually.
* @param {Ext.menu.Menu} menu The menu to add to
* @param {Ext.Component} component The component to add
* TODO: Implement overrides in Ext.layout.container.boxOverflow which create overrides
* for SplitButton, Button, ButtonGroup, and TextField. And a generic one for Component
* which create clones suitable for use in an overflow menu.
*/
addComponentToMenu: function(menu, component) {
var me = this,
i, items, iLen;
// No equivalent to fill, skip it
if (component instanceof Ext.toolbar.Fill) {
return;
}
// Separator maps to MenuSeparator
else if (component instanceof Ext.toolbar.Separator) {
menu.add('-');
}
else if (component.overflowClone) {
menu.add(component.overflowClone);
}
// Other types...
else if (component.isComponent) {
if (component.isXType('splitbutton')) {
component.overflowClone = menu.add(me.createMenuConfig(component, true));
}
else if (component.isXType('button')) {
component.overflowClone = menu.add(me.createMenuConfig(component, !component.menu));
}
else if (component.isXType('buttongroup')) {
items = component.items.items;
iLen = items.length;
for (i = 0; i < iLen; i++) {
me.addComponentToMenu(menu, items[i]);
}
// If the component is a CheckBox/Radio field, we are supposed to
// have a menucheckitem that will replace the original component.
// Because of that, we need to add a value getter/setter and an event listener that
// will fire the change event on click, making the menuitem behave as a
// checkbox/radio field would have.
}
else if (component.isCheckbox) {
component.overflowClone = menu.add(me.createMenuConfig(component));
Ext.apply(component.overflowClone, {
getValue: function() {
return component.overflowClone.checked;
},
setValue: function() {
component.overflowClone.setChecked(component.getValue());
}
});
component.overflowClone.on('click', function(item) {
item.setChecked(item.masterComponent.isRadio ? true : !item.checked);
item.fireEvent('change', item, item.checked);
});
}
else {
component.overflowClone = menu.add(
Ext.create(Ext.getClassName(component), me.createMenuConfig(component))
);
}
}
},
destroy: function() {
Ext.destroy(this.menu, this.menuTrigger);
this.callParent();
}
});