UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,753 lines (1,487 loc) 101 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ 'sap/ui/core/Element', './Dialog', './Popover', './SelectList', './library', 'sap/ui/core/Control', 'sap/ui/core/EnabledPropagator', 'sap/ui/core/LabelEnablement', 'sap/ui/core/Icon', 'sap/ui/core/IconPool', './Button', './Bar', './Title', './delegate/ValueStateMessage', 'sap/ui/core/message/MessageMixin', 'sap/ui/core/library', 'sap/ui/core/Item', 'sap/ui/Device', 'sap/ui/core/InvisibleText', './SelectRenderer', "sap/ui/dom/containsOrEquals", "sap/ui/events/KeyCodes", './Text', 'sap/m/SimpleFixFlex', 'sap/base/Log', 'sap/ui/core/ValueStateSupport', "sap/ui/core/InvisibleMessage", "sap/ui/core/Lib", "sap/ui/core/ResizeHandler" ], function( Element, Dialog, Popover, SelectList, library, Control, EnabledPropagator, LabelEnablement, Icon, IconPool, Button, Bar, Title, ValueStateMessage, MessageMixin, coreLibrary, Item, Device, InvisibleText, SelectRenderer, containsOrEquals, KeyCodes, Text, SimpleFixFlex, Log, ValueStateSupport, InvisibleMessage, Library, ResizeHandler ) { "use strict"; // shortcut for sap.m.SelectListKeyboardNavigationMode var SelectListKeyboardNavigationMode = library.SelectListKeyboardNavigationMode; // shortcut for sap.m.PlacementType var PlacementType = library.PlacementType; // shortcut for sap.m.SelectTwoColumnSeparator var TwoColumnSeparator = library.SelectTwoColumnSeparator; // shortcut for sap.ui.core.ValueState var ValueState = coreLibrary.ValueState; // shortcut for sap.ui.core.TextDirection var TextDirection = coreLibrary.TextDirection; // shortcut for sap.ui.core.TextAlign var TextAlign = coreLibrary.TextAlign; // shortcut for sap.ui.core.OpenState var OpenState = coreLibrary.OpenState; // shortcut for sap.m.SelectType var SelectType = library.SelectType; // shortcut for sap.ui.core.InvisibleMessageMode var InvisibleMessageMode = coreLibrary.InvisibleMessageMode; // shortcut for sap.ui.core.TitleLevel var TitleLevel = coreLibrary.TitleLevel; // constant for two column separator var TWO_COLUMN_SEPARATOR_MAP = { "Dash": "\u2013", // En Dash – "Bullet": "\u2022", // Bullet • "VerticalLine": "\u007C" // Vertical Line | }; /** * Constructor for a new <code>sap.m.Select</code>. * * @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 <code>sap.m.Select</code> control provides a list of items that allows users to select an item. * * @see {@link fiori:https://experience.sap.com/fiori-design-web/select/ Select} * * @extends sap.ui.core.Control * @implements sap.ui.core.IFormContent, sap.ui.core.ISemanticFormContent, sap.ui.core.ILabelable * * @borrows sap.ui.core.ISemanticFormContent.getFormFormattedValue as #getFormFormattedValue * @borrows sap.ui.core.ISemanticFormContent.getFormValueProperty as #getFormValueProperty * @borrows sap.ui.core.ISemanticFormContent.getFormObservingProperties as #getFormObservingProperties * @borrows sap.ui.core.ISemanticFormContent.getFormRenderAsControl as #getFormRenderAsControl * @borrows sap.ui.core.ILabelable.hasLabelableHTMLElement as #hasLabelableHTMLElement * * @author SAP SE * @version 1.146.0 * * @constructor * @public * @alias sap.m.Select */ var Select = Control.extend("sap.m.Select", /** @lends sap.m.Select.prototype */ { metadata: { interfaces: [ "sap.ui.core.IFormContent", "sap.m.IOverflowToolbarContent", "sap.m.IToolbarInteractiveControl", "sap.f.IShellBar", "sap.ui.core.ISemanticFormContent", "sap.ui.core.ILabelable" ], library: "sap.m", properties: { /** * The name to be used in the HTML code (for example, for HTML forms that send data to the server via submit). */ name: { type: "string", group: "Misc", defaultValue: "" }, /** * Determines whether the user can modify the selected item. When the property is set * to <code>false</code>, the control appears as disabled and CANNOT be focused. * * <b>Note:</b> When both <code>enabled</code> and <code>editable</code> properties * are set to <code>false</code>, <code>enabled</code> has priority over * <code>editable</code>. */ enabled: { type: "boolean", group: "Behavior", defaultValue: true }, /** * Determines whether the user can modify the selected item. When the property is set * to <code>false</code>, the control appears as disabled but CAN still be focused. * * <b>Note:</b> When both <code>enabled</code> and <code>editable</code> properties * are set to <code>false</code>, <code>enabled</code> has priority over * <code>editable</code>. * * @since 1.66.0 */ editable: { type: "boolean", group: "Behavior", defaultValue: true }, /** * Sets the width of the field. By default, the field width is automatically adjusted to the size * of its content and the default width of the field is calculated based on the widest list item * in the dropdown list. * If the width defined is smaller than its content, only the field width is changed whereas * the dropdown list keeps the width of its content. * If the dropdown list is wider than the visual viewport, it is truncated and an ellipsis is displayed * for each item. * For phones, the width of the dropdown list is always the same as the viewport. * * <b>Note:</b> This property is ignored if the <code>autoAdjustWidth</code> property is set to <code>true</code>. */ width: { type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: "auto" }, /** * Sets the maximum width of the control. * * <b>Note:</b> This property is ignored if the <code>autoAdjustWidth</code> property is set to <code>true</code>. */ maxWidth: { type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: "100%" }, /** * Key of the selected item. * * <b>Notes:</b> * <ul> * <li> If duplicate keys exist, the first item matching the key is used.</li> * <li> If invalid or none <code>selectedKey</code> is used, the first item is * being selected.</li> * <li> Invalid or missing <code>selectedKey</code> leads to severe functional * issues in <code>sap.ui.table.Table</code>, when the <code>sap.m.Select</code> is used inside a * <code>sap.ui.table.Table</code> column.</li> * <li> If an item with the default key exists and we try to select it, it happens only if the * <code>forceSelection</code> property is set to <code>true</code>.</li> * </ul> * * @since 1.11 */ selectedKey: { type: "string", group: "Data", defaultValue: "" }, /** * ID of the selected item. * @since 1.12 */ selectedItemId: { type: "string", group: "Data", defaultValue: "" }, /** * The URI to the icon that will be displayed only when using the <code>IconOnly</code> type. * @since 1.16 */ icon: { type: "sap.ui.core.URI", group: "Appearance", defaultValue: "" }, /** * Type of a select. Possible values <code>Default</code>, <code>IconOnly</code>. * @since 1.16 */ type: { type: "sap.m.SelectType", group: "Appearance", defaultValue: SelectType.Default }, /** * Indicates whether the width of the input field is determined by the selected item's content. * @since 1.16 */ autoAdjustWidth: { type: "boolean", group: "Appearance", defaultValue: false }, /** * Sets the horizontal alignment of the text within the input field. * @since 1.28 */ textAlign: { type: "sap.ui.core.TextAlign", group: "Appearance", defaultValue: TextAlign.Initial }, /** * Specifies the direction of the text within the input field with enumerated options. * By default, the control inherits text direction from the DOM. * @since 1.28 */ textDirection: { type: "sap.ui.core.TextDirection", group: "Appearance", defaultValue: TextDirection.Inherit }, /** * Visualizes the validation state of the control, e.g. <code>Error</code>, <code>Warning</code>, * <code>Success</code>, <code>Information</code>. * @since 1.40.2 */ valueState: { type: "sap.ui.core.ValueState", group: "Appearance", defaultValue: ValueState.None }, /** * Defines the text of the value state message popup. * If this is not specified, a default text is shown from the resource bundle. * * @since 1.40.5 */ valueStateText: { type: "string", group: "Misc", defaultValue: "" }, /** * Indicates whether the text values of the <code>additionalText</code> property of a * {@link sap.ui.core.ListItem} are shown. * @since 1.40 */ showSecondaryValues: { type: "boolean", group: "Misc", defaultValue: false }, /** * Modifies the behavior of the <code>setSelectedKey</code> method so that * the selected item is cleared when a provided selected key is missing. * @since 1.77 */ resetOnMissingKey: { type: "boolean", group: "Behavior", defaultValue: false }, /** * Indicates whether the selection is restricted to one of the items in the list. * <b>Note:</b> We strongly recommend that you always set this property to <code>false</code> and bind * the <code>selectedKey</code> property to the desired value for better interoperability with data binding. * @since 1.34 */ forceSelection: { type: "boolean", group: "Behavior", defaultValue: true }, /** * Determines whether the text in the items wraps on multiple lines when the available width is not enough. * When the text is truncated (<code>wrapItemsText</code> property is set to <code>false</code>), * the max width of the <code>SelectList</code> is 600px. When <code>wrapItemsText</code> is set to * <code>true</code>, <code>SelectList</code> takes all of the available width. * @since 1.69 */ wrapItemsText: { type: "boolean", group: "Behavior", defaultValue: false }, /** * Determines the ratio between the first and the second column when secondary values are displayed. * * <b>Note:</b> This property takes effect only when the <code>showSecondaryValues</code> property is set to <code>true</code>. * @since 1.86 */ columnRatio: { type: "sap.m.SelectColumnRatio", group: "Appearance", defaultValue: "3:2" }, /** * Indicates that user input is required. This property is only needed for accessibility purposes when a single relationship between * the field and a label (see aggregation <code>labelFor</code> of <code>sap.m.Label</code>) cannot be established * (e.g. one label should label multiple fields). * @since 1.74 */ required : {type : "boolean", group : "Misc", defaultValue : false}, /** * Defines the separator type for the two columns layout when Select is in read-only mode. * @since 1.140 */ twoColumnSeparator: { type: "sap.m.SelectTwoColumnSeparator", group: "Appearance", defaultValue: TwoColumnSeparator.Dash } }, defaultAggregation : "items", aggregations: { /** * Defines the items contained within this control. * * <b>Note:</b> For items with icons you can use {@link sap.ui.core.ListItem}. * * Example: * * <pre> * <code> &lt;ListItem text="Paper plane" icon="sap-icon://paper-plane"&gt;&lt;/ListItem&gt; </code> * </pre> */ items: { type: "sap.ui.core.Item", multiple: true, singularName: "item", bindable: "bindable", forwarding: { getter: "getList", aggregation: "items" } }, /** * Internal aggregation to hold the inner picker popup. */ picker: { type: "sap.ui.core.PopupInterface", multiple: false, visibility: "hidden" }, /** * Icon, displayed in the left most area of the <code>Select</code> input. */ _valueIcon: { type: "sap.ui.core.Icon", multiple: false, visibility: "hidden" }, /** * Internal aggregation to hold the picker's header * @since 1.52 */ _pickerHeader: { type: "sap.m.Bar", multiple: false, visibility: "hidden" }, /** * Internal aggregation to hold the picker's subheader. */ _pickerValueStateContent: { type: "sap.m.Text", multiple: false, visibility: "hidden" } }, associations: { /** * Sets or retrieves the selected item from the aggregation named items. */ selectedItem: { type: "sap.ui.core.Item", multiple: false }, /** * Association to controls / IDs which label this control (see WAI-ARIA attribute <code>aria-labelledby</code>). * @since 1.27.0 */ ariaLabelledBy: { type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy" } }, events: { /** * This event is fired when the value in the selection field is changed in combination with one of * the following actions: * <ul> * <li>The focus leaves the selection field</li> * <li>The <i>Enter</i> key is pressed</li> * <li>The item is pressed</li> * </ul> */ change: { parameters: { /** * The selected item. */ selectedItem: { type: "sap.ui.core.Item" }, /** * The previous selected item. * @since 1.95 */ previousSelectedItem: { type: "sap.ui.core.Item" } } }, /** * Fires when the user navigates through the <code>Select</code> items. * It's also fired on revert of the currently selected item. * * <b>Note:</b> Revert occurs in some of the following actions: * <ul> * <li>The user clicks outside of the <code>Select</code></li> * <li>The <i>Escape</i> key is pressed</li> * </ul> * * @since 1.100 */ liveChange: { parameters: { /** * The selected item. */ selectedItem: { type: "sap.ui.core.Item" } } }, /** * This event is triggered prior to the opening of the <code>sap.m.SelectList</code>. * @since 1.130 */ beforeOpen: {} }, designtime: "sap/m/designtime/Select.designtime" }, renderer: SelectRenderer }); IconPool.insertFontFaceStyle(); EnabledPropagator.apply(Select.prototype, [true]); // apply the message mixin so all message on the input will get the associated label-texts injected MessageMixin.call(Select.prototype); /* =========================================================== */ /* Private methods and properties */ /* =========================================================== */ /* ----------------------------------------------------------- */ /* Private methods */ /* ----------------------------------------------------------- */ function fnHandleKeyboardNavigation(oItem) { if (this._isIconOnly() && !this.isOpen()) { return; } if (oItem) { if (this.getSelectedItemId() !== oItem.getId()) { this.fireEvent("liveChange", {selectedItem: oItem}); } this.setSelection(oItem); this.setValue(oItem.getText()); this.scrollToItem(oItem); } } Select.prototype._attachHiddenSelectHandlers = function () { var oSelect = this._getHiddenSelect(), oInput = this._getHiddenInput(); oSelect.on("focus", this._addFocusClass.bind(this)); oSelect.on("blur", this._removeFocusClass.bind(this)); oInput.on("focus", this.focus.bind(this)); }; Select.prototype.focus = function() { this._getHiddenSelect().trigger("focus"); Control.prototype.focus.call(this, arguments); }; Select.prototype._addFocusClass = function () { this.$().addClass("sapMSltFocused"); }; Select.prototype._removeFocusClass = function () { this.$().removeClass("sapMSltFocused"); }; Select.prototype._detachHiddenSelectHandlers = function () { var oSelect = this._getHiddenSelect(), oInput = this._getHiddenInput(); if (oSelect) { oSelect.off("focus"); oSelect.off("blur"); } if (oInput) { oInput.off("focus"); } }; Select.prototype._getHiddenSelect = function () { return this.$("hiddenSelect"); }; Select.prototype._getHiddenInput = function () { return this.$("hiddenInput"); }; Select.prototype._announceValueStateText = function() { var sValueStateText = this._getValueStateText(); if (this._oInvisibleMessage) { this._oInvisibleMessage.announce(sValueStateText, InvisibleMessageMode.Polite); } }; Select.prototype._getValueStateText = function() { var sValueState = this.getValueState(), sValueStateTypeText, sValueStateText; if (sValueState === ValueState.None) { return ""; } sValueStateTypeText = Library.getResourceBundleFor("sap.m").getText("INPUTBASE_VALUE_STATE_" + sValueState.toUpperCase()); sValueStateText = sValueStateTypeText + " " + (this.getValueStateText() || ValueStateSupport.getAdditionalText(this)); return sValueStateText; }; Select.prototype._isFocused = function () { return this.getFocusDomRef() === document.activeElement; }; Select.prototype._isIconOnly = function () { return this.getType() === SelectType.IconOnly; }; Select.prototype._handleFocusout = function(oEvent) { this._bFocusoutDueRendering = this.bRenderingPhase; if (this._bFocusoutDueRendering) { this._bProcessChange = false; return; } if (this._bProcessChange) { // if the focus-out is outside of the picker we should revert the selection if (!this.isOpen() || oEvent.target === this.getAggregation("picker")) { this._checkSelectionChange(); } else { this._revertSelection(); } this._bProcessChange = false; } else { this._bProcessChange = true; } }; Select.prototype._checkSelectionChange = function() { var oItem = this.getSelectedItem(); if (this._oSelectionOnFocus !== oItem) { this.fireChange({ selectedItem: oItem, previousSelectedItem: this._oSelectionOnFocus }); } }; Select.prototype._revertSelection = function() { var oItem = this.getSelectedItem(); if (this._oSelectionOnFocus !== oItem) { this.fireEvent("liveChange", {selectedItem: this._oSelectionOnFocus}); this.setSelection(this._oSelectionOnFocus); this.setValue(this._getSelectedItemText()); } }; /** * Handles immediate selection of the initially highlighted item for accessibility. * This eliminates the confusing "highlighted but not selectable" state for keyboard users. * * @private */ Select.prototype._selectInitialHighlightedItem = function() { if (this._oInitialHighlightedItem && !this.getSelectedItem() && !this.getForceSelection()) { this.setSelection(this._oInitialHighlightedItem); this.setValue(this._getSelectedItemText(this._oInitialHighlightedItem)); this._oInitialHighlightedItem = null; } }; Select.prototype._getSelectedItemText = function(vItem) { vItem = vItem || this.getSelectedItem(); if (!vItem) { vItem = this.getDefaultSelectedItem(); } if (vItem) { var oParent = vItem.getParent(), sMainText = oParent ? vItem.getText() : null, sAdditionalText, sSeparatorKey, sSeparator, sText; if (this.getEditable()) { return sMainText; } else { sSeparatorKey = this.getTwoColumnSeparator(); sSeparator = TWO_COLUMN_SEPARATOR_MAP[sSeparatorKey]; sAdditionalText = oParent ? vItem.getAdditionalText?.() : null; if (sAdditionalText && this.getShowSecondaryValues()) { sText = `${sMainText} ${sSeparator} ${sAdditionalText}`; } else { sText = sMainText; } return sText; } } return ""; }; /** * Enables the <code>sap.m.Select</code> to move inside the sap.m.OverflowToolbar. * Required by the {@link sap.m.IOverflowToolbarContent} interface. * * @public * @returns {sap.m.OverflowToolbarConfig} Configuration information for the <code>sap.m.IOverflowToolbarContent</code> interface. */ Select.prototype.getOverflowToolbarConfig = function() { var noInvalidationProps = ["enabled", "selectedKey"]; if (!this.getAutoAdjustWidth() || this._bIsInOverflow) { noInvalidationProps.push("selectedItemId"); } var oConfig = { canOverflow: true, autoCloseEvents: ["change"], invalidationEvents: ["_itemTextChange"], propsUnrelatedToSize: noInvalidationProps }; oConfig.onBeforeEnterOverflow = function(oSelect) { var oToolbar = oSelect.getParent(); if (!oToolbar.isA("sap.m.OverflowToolbar")) { return; } oSelect._prevSelectType = oSelect.getType(); oSelect._bIsInOverflow = true; if (oSelect.getType() !== SelectType.Default) { oSelect.setProperty("type", SelectType.Default, true); } }; oConfig.onAfterExitOverflow = function(oSelect) { var oToolbar = oSelect.getParent(); if (!oToolbar.isA("sap.m.OverflowToolbar")) { return; } oSelect._bIsInOverflow = false; if (oSelect.getType() !== oSelect._prevSelectType) { oSelect.setProperty("type", oSelect._prevSelectType, true); } }; return oConfig; }; /** * Gets the Select's <code>list</code>. * * @returns {sap.m.List} * @private * @since 1.22.0 */ Select.prototype.getList = function() { if (this._bIsBeingDestroyed) { return null; } return this._oList; }; /** * Retrieves the first enabled item from the aggregation named <code>items</code>. * * @param {array} [aItems] * @returns {sap.ui.core.Item | null} * @private */ Select.prototype.findFirstEnabledItem = function(aItems) { var oList = this.getList(); return oList ? oList.findFirstEnabledItem(aItems) : null; }; /** * Retrieves the last enabled item from the aggregation named <code>items</code>. * * @param {array} [aItems] * @returns {sap.ui.core.Item | null} * @private */ Select.prototype.findLastEnabledItem = function(aItems) { var oList = this.getList(); return oList ? oList.findLastEnabledItem(aItems) : null; }; /** * Sets the selected item by its index. * * @param {int} iIndex * @private */ Select.prototype.setSelectedIndex = function(iIndex, _aItems /* only for internal usage */) { var oItem; _aItems = _aItems || this.getItems(); // constrain the new index iIndex = (iIndex > _aItems.length - 1) ? _aItems.length - 1 : Math.max(0, iIndex); oItem = _aItems[iIndex]; if (oItem) { this.setSelection(oItem); } }; /** * Scrolls an item into the visual viewport. * * @private */ Select.prototype.scrollToItem = function(oItem) { var oPickerDomRef = this.getPicker().getDomRef(), oItemDomRef = oItem && oItem.getDomRef(); if (!oPickerDomRef || !oItemDomRef) { return; } var oPickerSelectListDomRef = oPickerDomRef.querySelector('.sapUiSimpleFixFlexFlexContent'), oPickerValueStateContentDomRef = oPickerDomRef.querySelector('.sapMSltPickerValueState'), iPickerValueStateContentHeight = oPickerValueStateContentDomRef ? oPickerValueStateContentDomRef.clientHeight : 0, iPickerScrollTop = oPickerSelectListDomRef.scrollTop, iItemOffsetTop = oItemDomRef.offsetTop - iPickerValueStateContentHeight, iPickerHeight = oPickerSelectListDomRef.clientHeight, iItemHeight = oItemDomRef.offsetHeight; if (iPickerScrollTop > iItemOffsetTop) { // scroll up oPickerSelectListDomRef.scrollTop = iItemOffsetTop; // bottom edge of item > bottom edge of viewport } else if ((iItemOffsetTop + iItemHeight) > (iPickerScrollTop + iPickerHeight)) { // scroll down, the item is partly below the viewport of the list oPickerSelectListDomRef.scrollTop = Math.ceil(iItemOffsetTop + iItemHeight - iPickerHeight); } }; /** * Sets the text value of the <code>Select</code> field. * * @param {string} sValue * @private */ Select.prototype.setValue = function(sValue) { var oDomRef = this.getDomRef(), oTextPlaceholder = oDomRef && oDomRef.querySelector(".sapMSelectListItemText"), bForceAnnounce = !this.isOpen() && this._isFocused() && this._oInvisibleMessage; if (oTextPlaceholder) { oTextPlaceholder.textContent = sValue; } this._setHiddenSelectValue(); this._getValueIcon(); if (bForceAnnounce) { // InvisibleMessage is used in case the popover is closed. // (aria-activedescendant is announcing the new selection when popover is opened). this._oInvisibleMessage.announce(sValue, InvisibleMessageMode.Assertive); } }; Select.prototype._setHiddenSelectValue = function () { var oSelect = this._getHiddenSelect(), oInput = this._getHiddenInput(), oSelectedKey = this.getSelectedKey(), sSelectedItemText = this._getSelectedItemText(); // the hidden INPUT is only used when the select is submitted // with a form so update its value in all cases oInput.attr("value", oSelectedKey || ""); if (!this._isIconOnly()) { oSelect.text(sSelectedItemText); } }; Select.prototype._getValueIcon = function() { if (this._bIsBeingDestroyed) { return null; } var oValueIcon = this.getAggregation("_valueIcon"), oSelectedItem = this.getSelectedItem(), bHaveIcon = !!(oSelectedItem && oSelectedItem.getIcon && oSelectedItem.getIcon()), sIconSrc = bHaveIcon ? oSelectedItem.getIcon() : "sap-icon://pull-down"; if (!oValueIcon) { oValueIcon = new Icon(this.getId() + "-labelIcon", {src: sIconSrc, visible: false}); this.setAggregation("_valueIcon", oValueIcon, true); } if (oValueIcon.getVisible() !== bHaveIcon) { oValueIcon.setVisible(bHaveIcon); oValueIcon.toggleStyleClass("sapMSelectListItemIcon", bHaveIcon); } if (bHaveIcon && oSelectedItem.getIcon() !== oValueIcon.getSrc()) { oValueIcon.setSrc(sIconSrc); } return oValueIcon; }; /** * Whether a shadow list should be render inside the HTML content of the select's field, to * automatically size it to fit its content. * * @returns {boolean} * @private */ Select.prototype._isShadowListRequired = function() { if (this.getAutoAdjustWidth()) { return false; } else if (this.getWidth() === "auto") { return true; } return false; }; /** * Handles the virtual focus of items. * * @param {sap.ui.core.Item | null} vItem * @private * @since 1.30 */ Select.prototype._handleAriaActiveDescendant = function(vItem) { var oDomRef = this.getFocusDomRef(), oItemDomRef = vItem && vItem.getDomRef(), sActivedescendant = "aria-activedescendant"; if (!oDomRef) { return; } // the aria-activedescendant attribute is set when the item is rendered if (oItemDomRef && this.isOpen()) { oDomRef.setAttribute(sActivedescendant, vItem.getId()); } else { oDomRef.removeAttribute(sActivedescendant); } }; Select.prototype.updateItems = function(sReason) { SelectList.prototype.updateItems.apply(this, arguments); // note: after the items are recreated, the selected item association // points to the new item this._oSelectionOnFocus = this.getSelectedItem(); }; /** * Called when the items aggregation needs to be refreshed. * * <b>Note:</b> This method has been overwritten to prevent <code>updateItems()</code> * from being called when the bindings are refreshed. * @see sap.ui.base.ManagedObject#bindAggregation */ Select.prototype.refreshItems = function() { SelectList.prototype.refreshItems.apply(this, arguments); }; /* ----------------------------------------------------------- */ /* Picker */ /* ----------------------------------------------------------- */ /** * This event handler is called before the picker popup is opened. * * @private */ Select.prototype.onBeforeOpen = function(oControlEvent) { var fnPickerTypeBeforeOpen = this["_onBeforeOpen" + this.getPickerType()], CSS_CLASS = this.getRenderer().CSS_CLASS; // add the active and expanded states to the field this.addStyleClass(CSS_CLASS + "Pressed"); this.addStyleClass(CSS_CLASS + "Expanded"); // close value state message before opening the picker this.closeValueStateMessage(); // call the hook to add additional content to the list this.addContent(); this.fireEvent("beforeOpen"); this.addContentToFlex(); fnPickerTypeBeforeOpen && fnPickerTypeBeforeOpen.call(this); }; /** * This event handler will be called after the picker popup is opened. * * @private */ Select.prototype.onAfterOpen = function(oControlEvent) { var oDomRef = this.getFocusDomRef(), oItem = null; if (!oDomRef) { return; } oItem = this.getSelectedItem(); oDomRef.setAttribute("aria-expanded", "true"); // expose a parent/child contextual relationship to assistive technologies // note: the "aria-controls" attribute is set when the list is visible and in view oDomRef.setAttribute("aria-controls", this.getList().getId()); if (oItem) { // note: the "aria-activedescendant" attribute is set // when the currently active descendant is visible and in view oDomRef.setAttribute("aria-activedescendant", oItem.getId()); this.scrollToItem(oItem); } else if (!this.getForceSelection()) { // For forceSelection=false, highlight first item to make it immediately selectable // This ensures keyboard/screen reader users can select the highlighted item with Enter/Space // without requiring arrow key navigation first var oFirstItem = this.getSelectableItems()[0]; if (oFirstItem) { oDomRef.setAttribute("aria-activedescendant", oFirstItem.getId()); this.scrollToItem(oFirstItem); this._oInitialHighlightedItem = oFirstItem; } } }; /** * This event handler is called before the picker popup is closed. * */ Select.prototype.onBeforeClose = function(oControlEvent) { var oDomRef = this.getFocusDomRef(), CSS_CLASS = this.getRenderer().CSS_CLASS; if (oDomRef) { // note: the "aria-controls" attribute is removed when the list is not visible and in view oDomRef.removeAttribute("aria-controls"); // the "aria-activedescendant" attribute is removed when the currently active descendant is not visible oDomRef.removeAttribute("aria-activedescendant"); // if the focus is back to the input after closing the picker, // the value state message should be reopened if (this.shouldValueStateMessageBeOpened() && (document.activeElement === oDomRef)) { this.openValueStateMessage(); } } // Clean up accessibility state this._oInitialHighlightedItem = null; // remove the expanded states of the field this.removeStyleClass(CSS_CLASS + "Expanded"); }; /** * This event handler is called after the picker popup is closed. * */ Select.prototype.onAfterClose = function(oControlEvent) { var oDomRef = this.getFocusDomRef(), CSS_CLASS = this.getRenderer().CSS_CLASS, sPressedCSSClass = CSS_CLASS + "Pressed"; if (oDomRef) { oDomRef.setAttribute("aria-expanded", "false"); oDomRef.removeAttribute("aria-activedescendant"); } // Remove the active state this.removeStyleClass(sPressedCSSClass); }; /** * Gets the control's picker popup. * * @returns {sap.m.Dialog | sap.m.Popover | null} The picker instance, creating it if necessary by calling <code>createPicker()</code> method. * @private */ Select.prototype.getPicker = function() { if (this._bIsBeingDestroyed) { return null; } // initialize the control's picker return this.createPicker(this.getPickerType()); }; /** * Gets the InvisibleText instance for the <code>valueStateText</code> property. * * @returns {sap.ui.core.InvisibleText | null} The <code>InvisibleText</code> instance, used to reference the text in aria-labelledby of interested controls. * @private */ Select.prototype.getValueStateTextInvisibleText = function() { if (this._bIsBeingDestroyed) { return null; } if (!this._oValueStateTextInvisibleText) { this._oValueStateTextInvisibleText = new InvisibleText({ id: this.getId() + "-valueStateText-InvisibleText" }); this._oValueStateTextInvisibleText.toStatic(); } return this._oValueStateTextInvisibleText; }; Select.prototype.getSimpleFixFlex = function() { if (this._bIsBeingDestroyed) { return null; } else if (this.oSimpleFixFlex) { return this.oSimpleFixFlex; } // initialize the SimpleFixFlex this.oSimpleFixFlex = new SimpleFixFlex({ id: this.getPickerValueStateContentId(), fixContent: this._getPickerValueStateContent() .addStyleClass(this.getRenderer().CSS_CLASS + "PickerValueState"), flexContent: this.createList() }); return this.oSimpleFixFlex; }; /** * Setter for property <code>_sPickerType</code>. * * @private */ Select.prototype.setPickerType = function(sPickerType) { this._sPickerType = sPickerType; }; /** * Getter for property <code>_sPickerType</code> * * @returns {string} * @private */ Select.prototype.getPickerType = function() { return this._sPickerType; }; /** * Get's the picker's subheader. * * @returns {sap.m.Bar} Picker's header * @private */ Select.prototype._getPickerValueStateContent = function() { if (!this.getAggregation("_pickerValueStateContent")) { this.setAggregation("_pickerValueStateContent", new Text({ wrapping: true, text: this._getTextForPickerValueStateContent() })); } return this.getAggregation("_pickerValueStateContent"); }; /** * Sets the <code>valueStateText</code> into the picker's subheader title. * @returns {void} * @private */ Select.prototype._updatePickerValueStateContentText = function() { var oPicker = this.getPicker(), oPickerValueStateContent = oPicker && oPicker.getContent()[0].getFixContent(), sText; if (oPickerValueStateContent) { sText = this._getTextForPickerValueStateContent(); oPickerValueStateContent.setText(sText); } }; /** * Gets the text for the picker's subheader title. * In case <code>valueStateText</code> is not set, a default value is returned. * @returns {string} * @private */ Select.prototype._getTextForPickerValueStateContent = function() { var sValueStateText = this.getValueStateText(), sText; if (sValueStateText) { sText = sValueStateText; } else { sText = this._getDefaultTextForPickerValueStateContent(); } return sText; }; /** * Gets the default text for the picker's subheader title. * @returns {string} * @private */ Select.prototype._getDefaultTextForPickerValueStateContent = function() { var sValueState = this.getValueState(), oResourceBundle, sText; if (sValueState === ValueState.None) { sText = ""; } else { oResourceBundle = Library.getResourceBundleFor("sap.ui.core"); sText = oResourceBundle.getText("VALUE_STATE_" + sValueState.toUpperCase()); } return sText; }; /** * Updates CSS classes for the <code>valueStateText</code> in the picker's subheader. * @private */ Select.prototype._updatePickerValueStateContentStyles = function() { var sValueState = this.getValueState(), mValueState = ValueState, CSS_CLASS = this.getRenderer().CSS_CLASS, PICKER_CSS_CLASS = CSS_CLASS + "Picker", sCssClass = PICKER_CSS_CLASS + sValueState + "State", sPickerWithSubHeader = PICKER_CSS_CLASS + "WithSubHeader", oPicker = this.getPicker(), oCustomHeader = oPicker && oPicker.getContent()[0].getFixContent(); if (oCustomHeader) { this._removeValueStateClassesForPickerValueStateContent(oPicker); oCustomHeader.addStyleClass(sCssClass); if (sValueState !== mValueState.None) { oPicker.addStyleClass(sPickerWithSubHeader); } else { oPicker.removeStyleClass(sPickerWithSubHeader); } } }; /** * Removes the picker's subheader value state classes for all available value states. * @param {sap.m.Popover | sap.m.Dialog} oPicker * @returns {void} * @private */ Select.prototype._removeValueStateClassesForPickerValueStateContent = function(oPicker) { var mValueState = ValueState, CSS_CLASS = this.getRenderer().CSS_CLASS, PICKER_CSS_CLASS = CSS_CLASS + "Picker", subHeader = oPicker.getContent()[0].getFixContent(); Object.keys(mValueState).forEach(function (key) { var sOldCssClass = PICKER_CSS_CLASS + key + "State"; subHeader.removeStyleClass(sOldCssClass); }); }; /* ----------------------------------------------------------- */ /* Popover */ /* ----------------------------------------------------------- */ /** * Creates an instance of <code>sap.m.Popover</code>. * * @returns {sap.m.Popover} * @private */ Select.prototype._createPopover = function() { var that = this; var oPicker = new Popover({ showArrow: false, showHeader: false, placement: PlacementType.VerticalPreferredBottom, offsetX: 0, offsetY: 0, initialFocus: this, ariaLabelledBy: this._getPickerHiddenLabelId() }); // detect when the scrollbar or an item is pressed oPicker.addEventDelegate({ ontouchstart: function(oEvent) { var oPickerDomRef = this.getDomRef("cont"); if ((oEvent.target === oPickerDomRef) || (oEvent.srcControl instanceof Item)) { that._bProcessChange = false; } } }, oPicker); this._decoratePopover(oPicker); return oPicker; }; /** * Decorates a <code>sap.m.Popover</code> instance. * * @param {sap.m.Popover} oPopover * @private */ Select.prototype._decoratePopover = function(oPopover) { var that = this; oPopover.open = function() { return this.openBy(that); }; }; /** * Required adaptations before rendering of the popover. * * @private */ Select.prototype._onBeforeRenderingPopover = function() { var oPopover = this.getPicker(), sWidth = this.$().outerWidth() + "px"; // set popover content min-width in px due to rendering issue in Chrome and small % if (oPopover) { // Don't set min-width for wrapped items - let them size to content if (!this.getWrapItemsText()) { oPopover.setContentMinWidth(sWidth); } else { oPopover.setContentMinWidth(""); } } }; /* ----------------------------------------------------------- */ /* Dialog */ /* ----------------------------------------------------------- */ /** * Creates an instance of <code>sap.m.Dialog</code>. * * @returns {sap.m.Dialog} * @private */ Select.prototype._createDialog = function() { var that = this, oHeader = this._getPickerHeader(), oDialog = new Dialog({ stretch: true, ariaLabelledBy: this._getPickerHiddenLabelId(), customHeader: oHeader, beforeOpen: function() { that.updatePickerHeaderTitle(); } }); return oDialog; }; /** * Gets the picker header title. * * @returns {sap.m.Title | null} The title instance of the Picker * @private * @since 1.52 */ Select.prototype._getPickerTitle = function() { var oPicker = this.getPicker(), oHeader = oPicker && oPicker.getCustomHeader(); if (oHeader) { return oHeader.getContentMiddle()[0]; } return null; }; /** * Get's the Picker's header. * * @returns {sap.m.Bar} Picker's header * @private * @since 1.52 */ Select.prototype._getPickerHeader = function() { var sIconURI = IconPool.getIconURI("decline"), oResourceBundle; if (!this.getAggregation("_pickerHeader")) { oResourceBundle = Library.getResourceBundleFor("sap.m"); this.setAggregation("_pickerHeader", new Bar({ titleAlignment: library.TitleAlignment.Auto, contentMiddle: new Title({ text: oResourceBundle.getText("SELECT_PICKER_TITLE_TEXT"), level: TitleLevel.H1 }), contentRight: new Button({ icon: sIconURI, press: this.close.bind(this) }) })); } return this.getAggregation("_pickerHeader"); }; Select.prototype._getPickerHiddenLabelId = function() { return InvisibleText.getStaticId("sap.m", "INPUT_AVALIABLE_VALUES"); }; Select.prototype.getPickerValueStateContentId = function() { return this.getId() + "-valueStateText"; }; Select.prototype.updatePickerHeaderTitle = function() { var oPicker = this.getPicker(); if (!oPicker) { return; } var aLabels = this.getLabels(); if (aLabels.length) { var oLabel = aLabels[0], oPickerTitle = this._getPickerTitle(); if (oLabel && (typeof oLabel.getText === "function")) { oPickerTitle && oPickerTitle.setText(oLabel.getText()); } } }; /** * This event handler is called before the dialog is opened. * * @private */ Select.prototype._onBeforeOpenDialog = function() {}; /* =========================================================== */ /* Lifecycle methods */ /* =========================================================== */ Select.prototype.init = function() { // set the picker type this.setPickerType(Device.system.phone ? "Dialog" : "Popover"); // initialize composites this.createPicker(this.getPickerType()); // selected item on focus this._oSelectionOnFocus = null; // to detect when the control is in the rendering phase this.bRenderingPhase = false; // to detect if the focusout event is triggered due a re-rendering this._bFocusoutDueRendering = false; // used to prevent the change event from firing when the user scrolls // the picker popup (dropdown) list using the mouse this._bProcessChange = false; this.sTypedChars = ""; this.iTypingTimeoutID = -1; // delegate object used to open/close value state message popups this._oValueStateMessage = new ValueStateMessage(this); this._bValueStateMessageOpened = false; this._sAriaRoleDescription = Library.getResourceBundleFor("sap.m").getText("SELECT_ROLE_DESCRIPTION"); this._oInvisibleMessage = null; this._referencingLabelsHandlers = []; }; /** * Called after rendering. * * @private */ Select.prototype._attachResizeHandlers = function () { if (this.getAutoAdjustWidth() && this.getPicker() && this.getPickerType() === "Popover") { this._iResizeHandlerId = ResizeHandler.register(this, this._onResizeRef.bind(this)); } }; /** * Called after rendering. * * @private */ Select.prototype._detachResizeHandlers = function () { if (this._iResizeHandlerId) { ResizeHandler.deregister(this._iResizeHandlerId); this._iResizeHandlerId = null; } }; Select.prototype._onResizeRef = function() { //we make sure the repositioning of the popup, due to its openBy parent`s eidth change, is supressed this.getPicker().oPopup.setFollowOf(true); }; Select.prototype.onBeforeRendering = function() { if (!this._oInvisibleMessage) { this._oInvisibleMessage = InvisibleMessage.getInstance(); } // rendering phase is started this.bRenderingPhase = true; this.synchronizeSelection({ forceSelection: this.getForceSelection() }); this._updatePickerValueStateContentText(); this._updatePickerValueStateContentStyles(); this._detachHiddenSelectHandlers(); if (this._isIconOnly()) { this.setAutoAdjustWidth(true); } }; Select.prototype.onAfterRendering = function() { this._detachResizeHandlers(); this._attachResizeHandlers(); // rendering phase is finished this.bRenderingPhase = false; this._setHiddenSelectValue(); this._attachHiddenSelectHandlers(); this._clearReferencingLabelsHandlers(); this._handleReferencingLabels(); this._updateToolTip(); }; Select.prototype.exit = function() { var oValueStateMessage = this.getValueStateMessage(), oValueIcon = this._getValueIcon(); this._oSelectionOnFocus = null; if (this._oValueStateTextInvisibleText) { this._oValueStateTextInvisibleText.destroy(); this._oValueStateTextInvisibleText = null; } if (oValueStateMessage) { this.closeValueStateMessage(); oValueStateMessage.destroy(); } if (oValueIcon) { oValueIcon.destroy(); } this._oValueStateMessage = null; this._bValueStateMessageOpened = false; }; /* =========================================================== */ /* Event handlers */ /* =========================================================== */ /** * Handle the touch start event on the Select. * * @param {jQuery.Event} oEvent The event object. * @private */ Select.prototype.ontouchstart = function(oEvent) { // mark the event for components that needs to know if the event was handled oEvent.setMarked(); if (this.getEnabled() && this.getEditable()) { // add the active state to the Select's field this.addStyleClass(this.getRenderer().CSS_CLASS + "Pressed"); this.focus(); } }; /** * Handle the touch end event on the Select. * * @param {jQuery.Event} oEvent The event object. * @private */ Select.prototype.ontouchend = function(oEvent) { // mark the event for components that needs to know if the event was handled oEvent.setMarked(); if (this.getEnabled() && this.getEditable() && !this.isOpen() && this.isOpenArea(oEvent.target)) { // remove the active state of the Select HTMLDIVElement container this.removeStyleClass(this.getRenderer().CSS_CLASS + "Pressed"); } }; /** * Handle the tap event on the Select. * * @param {jQuery.Event} oEvent The event object. * @private */ Select.prototype.ontap = function(oEvent) { var CSS_CLASS = this.getRenderer().CSS_CLASS; // mark the event for components that needs to know if the event was handled oEvent.setMarked(); if (!this.getEnabled() || !this.getEditable()) { return; } if (this.isOpenArea(oEvent.target)) { if (this.isOpen()) { this.close(); this.removeStyleClass(CSS_CLASS + "Pressed"); return; } if (Device.system.phone) { // focus has to be set to the native select DOM element in order to restore the focus back to it // after the select dialog is closed this.focus(); } this.open(); } if (this.isOpen()) { // add the active state to the Select's field this.addStyleClass(CSS_CLASS + "Pressed"); } }; /** * Handles the <code>liveChange</code> event on the <code>SelectList</code>. * * @param {sap.ui.base.Event} oControlEvent * @private */ Select.prototype.onSelectionChange = function(oControlEvent) { var oItem = oControlEvent.getParameter("selectedItem"), oPreviousSelectedItem = this.getSelectedItem(); this.close(); this.setSelection(oItem); this.fireChange({ selectedItem: oItem, previousSelectedItem: oPreviousSelectedItem }); // check and update icon this.setValue(this._getSelectedItemText()); }; /* ----------------------------------------------------------- */ /* Keyboard handling */ /* ----------------------------------------------------------- */ /** * Handles the <code>keypress</code> event. * * @param {jQuery.Event} oEvent The event object. * @private */ Select.prototype.onkeypress = function(oEvent) { // prevents actions from occurring when the control is non-editable if (!this.getEditable()) { return; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // note: jQuery oEvent.which normalizes oEvent.keyCode and oEvent.charCode var sTypedCharacter = String.fromCharCode(oEvent.which), sText; this.sTypedChars += sTypedCharacter; // We check if we have more than one characters and they are all duplicate, we set the // text to be the last input character (sTypedCharacter). If not, we set the text to be // the whole input string. sText = (/^(.)\1+$/i).test(this.sTypedChars) ? sTypedCharacter : this.sTypedChars; clearTimeout(this.iTypingTimeoutID); this.iTypingTimeoutID = setTimeout(function() { this.sTypedChars = ""; this.iTypingTimeoutID = -1; }.bind(this), 1000); fnHandleKeyboardNavigation.call(this, this.searchNextItemByText(sText)); }; /** * Handle when F4 or Alt + DOWN arrow are pressed. * * @param {jQuery.Event