/*!
* UI development toolkit for HTML5 (OpenUI5)
* (c) Copyright 2009-2018 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Provides control sap.m.MenuButton.
sap.ui.define([
'./library',
'sap/ui/core/Control',
'./Button',
'./SplitButton',
'sap/ui/Device',
'sap/ui/core/EnabledPropagator',
'sap/ui/core/library',
'sap/ui/core/Popup',
'sap/ui/core/LabelEnablement',
'sap/m/Menu',
"./MenuButtonRenderer"
], function(
library,
Control,
Button,
SplitButton,
Device,
EnabledPropagator,
coreLibrary,
Popup,
LabelEnablement,
Menu,
MenuButtonRenderer
) {
"use strict";
// shortcut for sap.m.MenuButtonMode
var MenuButtonMode = library.MenuButtonMode;
// shortcut for sap.ui.core.TextDirection
var TextDirection = coreLibrary.TextDirection;
// shortcut for sap.m.ButtonType
var ButtonType = library.ButtonType;
// shortcut for sap.ui.core.Popup.Dock
var Dock = Popup.Dock;
// properties which shouldn't be applied on inner Button or SplitButton control since they don't have such properties
var aNoneForwardableProps = ["buttonMode", "useDefaultActionOnly", "width", "menuPosition"];
/**
* Constructor for a new MenuButton.
*
* @param {string} [sId] ID for the new control, generated automatically if no ID is given
* @param {object} [mSettings] Initial settings for the new control
*
* @class
* The sap.m.MenuButton
control enables the user to show a hierarchical menu.
* @extends sap.ui.core.Control
*
* @author SAP SE
* @version 1.60.23
*
* @constructor
* @public
* @alias sap.m.MenuButton
* @see {@link fiori:https://experience.sap.com/fiori-design-web/menu-button/ Menu Button}
* @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel
*/
var MenuButton = Control.extend("sap.m.MenuButton", /** @lends sap.m.MenuButton.prototype */ { metadata : {
library : "sap.m",
properties : {
/**
* Defines the text of the MenuButton
.
*
Note: In Split
buttonMode
with useDefaultActionOnly
* set to false
, the text is changed to display the last selected item's text,
* while in Regular
buttonMode
the text stays unchanged.
*/
text : {type : "string", group : "Misc", defaultValue : null},
/**
* Defines the type of the MenuButton
(for example, Default, Accept, Reject, Back, etc.)
*/
type : {type : "sap.m.ButtonType", group : "Appearance", defaultValue : ButtonType.Default},
/**
* Defines the width of the MenuButton
.
*
Note:As per visual design this width can be maximum of 12rem (192px).
*/
width : {type : "sap.ui.core.CSSSize", group : "Misc", defaultValue : null},
/**
* Boolean property to enable the control (default is true
).
*
Note: Depending on custom settings, the buttons that are disabled have other colors than the enabled ones.
*/
enabled : {type : "boolean", group : "Behavior", defaultValue : true},
/**
* Defines the icon to be displayed as a graphical element within the button.
* It can be an image or an icon from the icon font.
*/
icon : {type : "sap.ui.core.URI", group : "Appearance", defaultValue : null},
/**
* The source property of an alternative icon for the active (pressed) state of the button.
* Both active and default icon properties should be defined and of the same type - image or icon font.
* If the icon
property is not set or has a different type, the active icon is not displayed.
*/
activeIcon : {type : "sap.ui.core.URI", group : "Misc", defaultValue : null},
/**
* When set to true
(default), one or more requests are sent trying to get the
* density perfect version of image if this version of image doesn't exist on the server.
* If only one version of image is provided, set this value to false
to
* avoid the attempt of fetching density perfect image.
*/
iconDensityAware : {type : "boolean", group : "Misc", defaultValue : true},
/**
* Specifies the element's text directionality with enumerated options.
* By default, the control inherits text direction from the DOM.
*/
textDirection : {type : "sap.ui.core.TextDirection", group : "Appearance", defaultValue : TextDirection.Inherit},
/**
* Defines whether the MenuButton
is set to Regular
or Split
mode.
*/
buttonMode : { type : "sap.m.MenuButtonMode", group : "Misc", defaultValue : MenuButtonMode.Regular },
/**
* Specifies the position of the popup menu with enumerated options.
* By default, the control opens the menu at its bottom left side.
*
* Note: In the case that the menu has no space to show itself in the view port
* of the current window it tries to open itself to
* the inverted direction.
*
* @since 1.56.0
*/
menuPosition : {type : "sap.ui.core.Popup.Dock", group : "Misc", defaultValue : Dock.BeginBottom},
/**
* Controls whether the default action handler is invoked always or it is invoked only until a menu item is selected.
* Usable only if buttonMode
is set to Split
.
*/
useDefaultActionOnly : { type : "boolean", group : "Behavior", defaultValue: false }
},
aggregations: {
/**
* Defines the menu that opens for this button.
*/
menu: { type: "sap.m.Menu", multiple: false, singularName: "menu" },
/**
* Internal aggregation that contains the button part.
*/
_button: { type: "sap.ui.core.Control", multiple: false, visibility: "hidden" }
},
associations : {
/**
* Association to controls / ids which describe this control (see WAI-ARIA attribute aria-describedby).
*/
ariaDescribedBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaDescribedBy"},
/**
* Association to controls / ids which label this control (see WAI-ARIA attribute aria-labelledby).
*/
ariaLabelledBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy"}
},
events: {
/**
* Fired when the buttonMode
is set to Split
and the user presses the main button
* unless useDefaultActionOnly
is set to false
and another action
* from the menu has been selected previously.
*/
defaultAction: {}
},
defaultAggregation : "menu",
designtime: "sap/m/designtime/MenuButton.designtime"
}});
EnabledPropagator.call(MenuButton.prototype);
/**
* Initializes the control.
* @public
*/
MenuButton.prototype.init = function() {
this._initButtonControl();
};
/**
* Called from parent if the control is destroyed.
* @private
*/
MenuButton.prototype.exit = function() {
if (this._sDefaultText) {
this._sDefaultText = null;
}
if (this._sDefaultIcon) {
this._sDefaultIcon = null;
}
if (this._iInitialTextBtnContentWidth) {
this._iInitialTextBtnContentWidth = null;
}
if (this._lastActionItemId) {
this._lastActionItemId = null;
}
if (this.getMenu()) {
this.getMenu().detachClosed(this._menuClosed, this);
}
};
MenuButton.prototype.onBeforeRendering = function() {
if (!this._sDefaultText) {
this._sDefaultText = this.getText();
}
if (!this._sDefaultIcon) {
this._sDefaultIcon = this.getIcon();
}
this._updateButtonControl();
this._attachMenuEvents();
};
MenuButton.prototype._needsWidth = function() {
return this._isSplitButton() && this.getWidth() === "";
};
/**
* Gets the text button control DOM Element.
* @returns {Element} The Element's DOM Element
* @private
*/
MenuButton.prototype._getTextBtnContentDomRef = function() {
return this._getButtonControl()._getTextButton().getDomRef("content");
};
MenuButton.prototype.onAfterRendering = function() {
if (this._needsWidth() && sap.ui.getCore().isThemeApplied() && this._getTextBtnContentDomRef() && this._getInitialTextBtnWidth() > 0) {
this._getTextBtnContentDomRef().style.width = this._getInitialTextBtnWidth() + 'px';
}
this._setAriaHasPopup();
};
MenuButton.prototype.onThemeChanged = function(oEvent) {
//remember the initial width of the text button and hardcode it in the dom
if (this._needsWidth() && this.getDomRef() && !this._iInitialTextBtnContentWidth && this._getTextBtnContentDomRef() && this._getInitialTextBtnWidth() > 0) {
this._getTextBtnContentDomRef().style.width = this._getInitialTextBtnWidth() + 'px';
}
};
/**
* Gets the initial width of the text button control. To be used for 'split' mode only.
* @returns {int} The width after the text button control was rendered for the first time and theme applied
* @private
*/
MenuButton.prototype._getInitialTextBtnWidth = function() {
if (!this._iInitialTextBtnContentWidth) {
//round the width upward in order to prevent content overflow (ellipses)
this._iInitialTextBtnContentWidth = Math.ceil(this._getTextBtnContentDomRef().getBoundingClientRect().width);
}
return this._iInitialTextBtnContentWidth;
};
MenuButton.prototype._setAriaHasPopup = function() {
if (this._isSplitButton()) {
this._getButtonControl()._getArrowButton().$().attr("aria-haspopup", "true");
} else {
this._getButtonControl().$().attr("aria-haspopup", "true");
}
};
/**
* Sets the buttonMode
of the control.
* @param {sap.m.MenuButtonMode} sMode The new button mode
* @returns {sap.m.MenuButton} This instance
* @public
*/
MenuButton.prototype.setButtonMode = function(sMode) {
var sTooltip = this.getTooltip();
Control.prototype.setProperty.call(this, "buttonMode", sMode, true);
this._getButtonControl().destroy();
this._initButtonControl();
//update all properties
for (var key in this.mProperties) {
if (this.mProperties.hasOwnProperty(key) && aNoneForwardableProps.indexOf(key) < 0) {
this._getButtonControl().setProperty(key, this.mProperties[key], true);
}
}
//and tooltip aggregation
if (sTooltip) {
this._getButtonControl().setTooltip(sTooltip);
}
//update the text only
if (!this._isSplitButton() && this._sDefaultText) {
this.setText(this._sDefaultText);
} else if (!this.getUseDefaultActionOnly() && this._getLastSelectedItem()) {
this.setText(sap.ui.getCore().byId(this._getLastSelectedItem()).getText());
}
if (!this._isSplitButton() && this._sDefaultIcon) {
this.setIcon(this._sDefaultIcon);
} else if (!this.getUseDefaultActionOnly() && this._getLastSelectedItem()) {
this.setIcon(sap.ui.getCore().byId(this._getLastSelectedItem()).getIcon());
}
this.invalidate();
return this;
};
/**
* Creates the button part of a MenuButton
in regular mode.
* @returns {object} The created Button
* @private
*/
MenuButton.prototype._initButton = function() {
var oBtn = new Button(this.getId() + "-internalBtn", {
width: "100%"
});
oBtn.attachPress(this._handleButtonPress, this);
return oBtn;
};
/**
* Creates the button part of a MenuButton
in split mode.
* @returns {object} The created SplitButton
* @private
*/
MenuButton.prototype._initSplitButton = function() {
var oBtn = new SplitButton(this.getId() + "-internalSplitBtn", {
width: "100%"
});
oBtn.attachPress(this._handleActionPress, this);
oBtn.attachArrowPress(this._handleButtonPress, this);
return oBtn;
};
/**
* Creates the button part of a MenuButton
.
* @private
*/
MenuButton.prototype._initButtonControl = function() {
var oBtn;
if (this._isSplitButton()) {
oBtn = this._initSplitButton();
} else {
oBtn = this._initButton();
}
this.setAggregation("_button", oBtn, true);
};
MenuButton.prototype._updateButtonControl = function() {
this._getButtonControl().setText(this.getText());
};
/**
* Gets the button part of a MenuButton
.
* @private
*/
MenuButton.prototype._getButtonControl = function() {
return this.getAggregation("_button");
};
/**
* Handles the buttonPress
event and opens the menu.
* @param {boolean} bWithKeyboard If keyboard is used
* @private
*/
MenuButton.prototype._handleButtonPress = function(bWithKeyboard) {
var oMenu = this.getMenu(),
oOffset = {
zero: "0 0",
plus2_right: "0 +2",
minus2_right: "0 -2",
plus2_left: "+2 0",
minus2_left: "-2 0"
};
if (this._bPopupOpen) {
this.getMenu().close();
return;
}
if (!oMenu) {
return;
}
if (!oMenu.getTitle()) {
oMenu.setTitle(this.getText());
}
var aParam = [this, bWithKeyboard];
switch (this.getMenuPosition()) {
case Dock.BeginTop:
aParam.push(Dock.BeginBottom, Dock.BeginTop, oOffset.plus2_right);
break;
case Dock.BeginCenter:
aParam.push(Dock.BeginCenter, Dock.BeginCenter, oOffset.zero);
break;
case Dock.LeftTop:
aParam.push(Dock.RightBottom, Dock.LeftBottom, oOffset.plus2_left);
break;
case Dock.LeftCenter:
aParam.push(Dock.RightCenter, Dock.LeftCenter, oOffset.plus2_left);
break;
case Dock.LeftBottom:
aParam.push(Dock.RightTop, Dock.LeftTop, oOffset.plus2_left);
break;
case Dock.CenterTop:
aParam.push(Dock.CenterBottom, Dock.CenterTop, oOffset.plus2_left);
break;
case Dock.CenterCenter:
aParam.push(Dock.CenterCenter, Dock.CenterCenter, oOffset.zero);
break;
case Dock.CenterBottom:
aParam.push(Dock.CenterTop, Dock.CenterBottom, oOffset.minus2_right);
break;
case Dock.RightTop:
aParam.push(Dock.LeftBottom, Dock.RightBottom, oOffset.minus2_left);
break;
case Dock.RightCenter:
aParam.push(Dock.LeftCenter, Dock.RightCenter, oOffset.minus2_left);
break;
case Dock.RightBottom:
aParam.push(Dock.LeftTop, Dock.RightTop, oOffset.minus2_left);
break;
case Dock.EndTop:
aParam.push(Dock.EndBottom, Dock.EndTop, oOffset.plus2_right);
break;
case Dock.EndCenter:
aParam.push(Dock.EndCenter, Dock.EndCenter, oOffset.zero);
break;
case Dock.EndBottom:
aParam.push(Dock.EndTop, Dock.EndBottom, oOffset.minus2_right);
break;
default:
case Dock.BeginBottom:
aParam.push(Dock.BeginTop, Dock.BeginBottom, oOffset.minus2_right);
break;
}
Menu.prototype.openBy.apply(oMenu, aParam);
this._writeAriaAttributes();
if (this._isSplitButton() && !Device.system.phone) {
this._getButtonControl().setArrowState(true);
}
};
MenuButton.prototype._handleActionPress = function() {
var sLastSelectedItemId = this._getLastSelectedItem(),
oLastSelectedItem;
if (!this.getUseDefaultActionOnly() && sLastSelectedItemId) {
oLastSelectedItem = sap.ui.getCore().byId(sLastSelectedItemId);
this.getMenu().fireItemSelected({ item: oLastSelectedItem });
} else {
this.fireDefaultAction();
}
};
MenuButton.prototype._menuClosed = function() {
if (this._isSplitButton()) {
this._getButtonControl().setArrowState(false);
}
};
MenuButton.prototype._menuItemSelected = function(oEvent) {
var oMenuItem = oEvent.getParameter("item");
this.fireEvent("_menuItemSelected", { item: oMenuItem }); // needed for controls that listen to interaction events from within the control (e.g. for sap.m.OverflowToolbar)
this._bPopupOpen = false;
if (
!this._isSplitButton() ||
this.getUseDefaultActionOnly() ||
!oMenuItem
) {
return;
}
this._lastActionItemId = oMenuItem.getId();
!!this._sDefaultText && this.setText(oMenuItem.getText());
!!this._sDefaultIcon && this.setIcon(oMenuItem.getIcon());
};
/**
* Gets the last selected menu item, which can be used
* to trigger the same default action on MenuItem
press.
* @returns {string} The last selected item's ID
* @private
*/
MenuButton.prototype._getLastSelectedItem = function() {
return this._lastActionItemId;
};
MenuButton.prototype._attachMenuEvents = function() {
if (this.getMenu()) {
this.getMenu().attachClosed(this._menuClosed, this);
this.getMenu().attachItemSelected(this._menuItemSelected, this);
}
};
MenuButton.prototype._isSplitButton = function() {
return this.getButtonMode() === MenuButtonMode.Split;
};
/**
* Overriding the setProperty method in order to keep in sync internal aggregations properties.
* @override
* @param {string} sPropertyName The name of the property being changed
* @param {object} vValue The new value
* @param {boolean} bSuppressInvalidate Flag indicating of re-rendering should be suppressed
* @returns {object} this Instance for chaining.
*/
MenuButton.prototype.setProperty = function(sPropertyName, vValue, bSuppressInvalidate) {
// Several button type property values are not allowed
function isForbiddenType(sType) {
var aTypes = [ButtonType.Up, ButtonType.Back, ButtonType.Unstyled];
return aTypes.indexOf(sType) !== -1;
}
if (sPropertyName === "type" && isForbiddenType(vValue)) {
return this;
}
if (sPropertyName === 'text') {
this._sDefaultText = vValue;
}
// For certain properties, propagate the new value to the inner button
switch (sPropertyName) {
case 'activeIcon':
case 'iconDensityAware':
case 'textDirection':
case 'enabled':
this._getButtonControl().setProperty(sPropertyName, vValue);
break;
}
return Control.prototype.setProperty.apply(this, arguments);
};
/**
* Sets the tooltip for the MenuButton
.
* Can either be an instance of a TooltipBase subclass or a simple string.
* @param {sap.ui.core.TooltipBase} vTooltip The tooltip that should be shown.
* @returns {*} this instance
* @public
*/
MenuButton.prototype.setTooltip = function(vTooltip) {
this._getButtonControl().setTooltip(vTooltip);
return Control.prototype.setTooltip.apply(this, arguments);
};
/*
* Override setter because the parent control has placed custom logic in it and all changes need to be propagated
* to the internal button aggregation.
* @param {string} sValue The text of the sap.m.MenuButton
* @return {sap.m.MenuButton} This instance for chaining
*/
MenuButton.prototype.setText = function (sValue) {
Button.prototype.setProperty.call(this, 'text', sValue);
this._getButtonControl().setText(sValue);
return this;
};
/*
* Override setter because the parent control has placed custom logic in it and all changes need to be propagated
* to the internal button aggregation.
* @param {string} sValue`
* @return {sap.m.MenuButton} This instance for chaining
*/
MenuButton.prototype.setType = function (sValue) {
Button.prototype.setProperty.call(this, 'type', sValue);
this._getButtonControl().setType(sValue);
return this;
};
/*
* Override setter because the parent control has placed custom logic in it and all changes need to be propagated
* to the internal button aggregation.
* @param {string} vValue
* @return {sap.m.MenuButton} This instance for chaining
*/
MenuButton.prototype.setIcon = function (vValue) {
Button.prototype.setProperty.call(this, 'icon', vValue);
this._getButtonControl().setIcon(vValue);
return this;
};
MenuButton.prototype.getFocusDomRef = function() {
return this._getButtonControl().getDomRef();
};
MenuButton.prototype.onsapup = function(oEvent) {
this.openMenuByKeyboard();
};
MenuButton.prototype.onsapdown = function(oEvent) {
this.openMenuByKeyboard();
};
MenuButton.prototype.onsapupmodifiers = function(oEvent) {
this.openMenuByKeyboard();
};
MenuButton.prototype.onsapdownmodifiers = function(oEvent) {
this.openMenuByKeyboard();
};
//F4
MenuButton.prototype.onsapshow = function(oEvent) {
this.openMenuByKeyboard();
!!oEvent && oEvent.preventDefault();
};
MenuButton.prototype.ontouchstart = function() {
this._bPopupOpen = this.getMenu() && this.getMenu()._getMenu() && this.getMenu()._getMenu().getPopup().isOpen();
};
MenuButton.prototype.openMenuByKeyboard = function() {
if (!this._isSplitButton()) {
this._handleButtonPress(true);
}
};
MenuButton.prototype._writeAriaAttributes = function() {
if (this.getMenu()) {
this.$().attr("aria-controls", this.getMenu().getDomRefId());
}
};
/**
* Returns the DOMNode Id to be used for the "labelFor" attribute of the label.
*
* By default, this is the Id of the control itself.
*
* @return {string} Id to be used for the labelFor
* @public
*/
MenuButton.prototype.getIdForLabel = function () {
return this.getId() + "-internalBtn";
};
/**
* Ensures that MenuButton's internal button will have a reference back to the labels, by which
* the MenuButton is labelled
*
* @returns {sap.m.MenuButton} For chaining
* @private
*/
MenuButton.prototype._ensureBackwardsReference = function () {
var oInternalButton = this._getButtonControl(),
aInternalButtonAriaLabelledBy = oInternalButton.getAriaLabelledBy(),
aReferencingLabels = LabelEnablement.getReferencingLabels(this);
aReferencingLabels.forEach(function (sLabelId) {
if (aInternalButtonAriaLabelledBy && aInternalButtonAriaLabelledBy.indexOf(sLabelId) === -1) {
oInternalButton.addAriaLabelledBy(sLabelId);
}
});
return this;
};
return MenuButton;
});