UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,589 lines (1,338 loc) 62.2 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([ './ComboBoxTextField', './ComboBoxBase', './List', './library', 'sap/ui/Device', "sap/ui/core/Element", 'sap/ui/core/Item', './ComboBoxRenderer', "sap/ui/dom/containsOrEquals", "sap/m/inputUtils/scrollToItem", "sap/m/inputUtils/inputsDefaultFilter", "sap/m/inputUtils/typeAhead", "sap/m/inputUtils/filterItems", "sap/m/inputUtils/ListHelpers", "sap/m/inputUtils/itemsVisibilityHandler", "sap/m/inputUtils/selectionRange", "sap/m/inputUtils/calculateSelectionStart", "sap/ui/events/KeyCodes", "sap/base/Log" ], function( ComboBoxTextField, ComboBoxBase, List, library, Device, Element, Item, ComboBoxRenderer, containsOrEquals, scrollToItem, inputsDefaultFilter, typeAhead, filterItems, ListHelpers, itemsVisibilityHandler, selectionRange, calculateSelectionStart, KeyCodes, Log ) { "use strict"; // shortcut for sap.m.ListMode var ListMode = library.ListMode; /** * Constructor for a new ComboBox. * * @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 * A drop-down list for selecting and filtering values. * <h3>Overview</h3> * The control represents a drop-down menu with a list of the available options and a text input field to narrow down the options. * <h3>Structure</h3> * The combo-box consists of the following elements: * <ul> * <li> Input field - displays the selected option or a custom user entry. Users can type to narrow down the list or enter their own value.</li> * <li> Drop-down arrow - expands\collapses the option list.</li> * <li> Option list - the list of available options. <b>Note:</b> Disabled items are not visualized in the list with the available options, however they can still be accessed through the <code>items</code> aggregation.</li> * </ul> * By setting the <code>showSecondaryValues</code> property, the combo box can display an additional value for each option (if there is one). * <b>Note:</b> The typeahead feature is not available on Android devices due to a OS specific issue. * <h3>Usage</h3> * <h4>When to use:</h4> * <ul> * <li>You need to select only one item in a long list of options (between 13 and 200) or your custom user input.</li> * </ul> * <h4>When not to use:</h4> * <ul> * <li>You need to select between only two options. Use a {@link sap.m.Switch switch} control instead.</li> * <li>You need to select between up to 12 options. Use a {@link sap.m.Select select} control instead.</li> * <li>You need to select between more than 200 options. Use a {@link sap.m.Input input} control with value help instead.</li> * </ul> * * <h4>Note:</h4> * The control has the following behavior regarding the <code>selectedKey</code> and <code>value</code> properties: * <ul> * <li> On initial loading, if the control has a <code>selectedKey</code> set which corresponds to a matching item, and a set <code>value</code>, the <code>value</code> will be updated to the matching item's text. </li> * <li> If a <code>selectedKey</code> is set and the user types an input which corresponds to an item's text, the <code>selectedKey</code> will be updated with the matching item's key. </li> * <li> If a <code>selectedKey</code> is set and the user types an input which does not correspond to any item's text, the <code>selectedKey</code> will be set to an empty string ("") </li> * <li> If a <code>selectedKey</code> is set and the user selects an item, the <code>selectedKey</code> will be updated to match the selected item's key. </li> * <li> If a <code>selectedKey</code> is bound and the user types before the data is loaded, the user's input will be overwritten by the binding update. </li> * </ul> * * <h3>Responsive Behavior</h3> * <ul> * <li>As the <code>sap.m.ComboBox</code> control allows free text, as well as has <code>selectedKey</code> / <code>selectedItem</code> properties, here is brief explanation of how they are updated during model change:</li> * <ul> * <li>If the ComboBox has <code>selectedKey</code> and <code>selectedItem</code> set, the model changes and the item key is no longer amongst the newly added items, the value of the ComboBox will remain the same and the <code>selectedKey</code> and <code>selectedItem</code> properties <strong>will not</strong> be changed.</li> * <li>If the ComboBox has <code>selectedKey</code> and <code>selectedItem</code> set, the model changes and the item key corresponds to newly added item, with different text, the value of the ComboBox <strong>will</strong> be updated with the text of the newly corresponding item.</li> * <li>If the ComboBox has only value, but no <code>selectedKey</code> and <code>selectedItem</code> set, the model changes, the value <strong>will</strong> remain the same and the <code>selectedKey</code> and <code>selectedItem</code> properties <strong>will not</strong> be changed.</li> * </ul> * <li>The width of the option list adapts to its content. The minimum width is the input field plus the drop-down arrow.</li> * <li>There is no horizontal scrolling in the option list. Entries in the list that are too long will be truncated.</li> * <li>On phone devices the combo box option list opens a dialog.</li> * </ul> * * @author SAP SE * @version 1.146.0 * * @constructor * @extends sap.m.ComboBoxBase * @public * @since 1.22 * @alias sap.m.ComboBox * @see {@link fiori:https://experience.sap.com/fiori-design-web/combo-box/ Combo Box} */ var ComboBox = ComboBoxBase.extend("sap.m.ComboBox", /** @lends sap.m.ComboBox.prototype */ { metadata: { interfaces : [ "sap.m.IToolbarInteractiveControl" ], library: "sap.m", designtime: "sap/m/designtime/ComboBox.designtime", properties: { /** * Key of the selected item. * * <b>Note:</b> If duplicate keys exist, the first item matching the key is used. */ selectedKey: { type: "string", group: "Data", defaultValue: "" }, /** * ID of the selected item. */ selectedItemId: { type: "string", group: "Misc", defaultValue: "" }, /** * Indicates whether the filter should check in both the <code>text</code> and the <code>additionalText</code> property of the * {@link sap.ui.core.ListItem} for the suggestion. * @since 1.46 */ filterSecondaryValues: { type: "boolean", group: "Misc", defaultValue: false }, /** * Indicates whether the picker is opened. * @private */ _open: { type: "boolean", defaultValue: false, visibility: "hidden" } }, associations: { /** * Sets or retrieves the selected item from the aggregation named items. */ selectedItem: { type: "sap.ui.core.Item", multiple: false } }, events: { /** * This event is fired when the value in the text input field is changed in combination with one of * the following actions: * * <ul> * <li>The focus leaves the text input field</li> * <li>The <i>Enter</i> key is pressed</li> * <li>An item in the list is selected</li> * </ul> * */ change: { parameters: { /** * The new <code>value</code> of the <code>control</code> */ value: { type: "string" }, /** * Indicates whether the change event was caused by selecting an item in the list */ itemPressed: { type: "boolean" } } }, /** * This event is fired when the user types something that matches with an item in the list; * it is also fired when the user presses on a list item, or when navigating via keyboard. */ selectionChange: { parameters: { /** * The selected item. */ selectedItem: { type: "sap.ui.core.Item" } } } }, dnd: { draggable: false, droppable: true } }, renderer: ComboBoxRenderer }); /* =========================================================== */ /* Private methods */ /* =========================================================== */ function fnSelectedItemOnViewPort(bIsListHidden) { var oItem = this.getSelectedItem(), oListItem = ListHelpers.getListItem(oItem), oItemDomRef = oItem && oListItem && oListItem.getDomRef(), oItemOffsetTop = oItemDomRef && oItemDomRef.offsetTop, oItemOffsetHeight = oItemDomRef && oItemDomRef.offsetHeight, oPicker = this.getPicker(), oPickerDomRef = oPicker.getDomRef("cont"), oPickerClientHeight = oPickerDomRef.clientHeight; //check if the selected item is on the viewport if (oItem && ((oItemOffsetTop + oItemOffsetHeight) > (oPickerClientHeight))) { // hide the list to scroll to the selected item if (!bIsListHidden) { this._getList().$().css("visibility", "hidden"); } else { // scroll to the selected item minus half the height of an item showing partly the // previous one, to indicate that there are items above and show the list oPickerDomRef.scrollTop = oItemOffsetTop - oItemOffsetHeight / 2; this._getList().$().css("visibility", "visible"); } } } /** * Gets the text of the selected item. * * @param {sap.ui.core.Item | null} vItem The item which text should be taken * @returns {string} Items text or empty string * @private */ ComboBox.prototype._getSelectedItemText = function(vItem) { vItem = vItem || this.getSelectedItem(); if (!vItem) { vItem = this.getDefaultSelectedItem(); } if (vItem) { return vItem.getText(); } return ""; }; /** * Sets the selected item by its index. * * @param {int} iIndex The item index * @param {sap.ui.core.Item[]} _aItems The item array * @private */ ComboBox.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); } }; /** * Reverts the selection of the ComboBox to the previously selected item before the picker was opened. * * @private */ ComboBox.prototype.revertSelection = function() { var sPickerTextFieldValue, oPickerTextField = this.getPickerTextField(); this.setSelectedItem(this._oSelectedItemBeforeOpen); this.setValue(this._sValueBeforeOpen); if (this.getSelectedItem() === null) { sPickerTextFieldValue = this._sValueBeforeOpen; } else { sPickerTextFieldValue = this._oSelectedItemBeforeOpen.getText(); } oPickerTextField && oPickerTextField.setValue(sPickerTextFieldValue); }; /** * Filters all items with 'starts with' filter * * @param {string} sInputValue Value to start item * @param {string} sMutator A Method to be called on an item to retrieve its value (could be getText or getAdditionalText) * @private * @returns {sap.ui.core.Item[]} Array of filtered items */ ComboBox.prototype._filterStartsWithItems = function (sInputValue, sMutator) { var sLowerCaseValue = sInputValue.toLowerCase(); var aItems = this.getItems(), aFilteredItems = aItems.filter(function (oItem) { return oItem[sMutator] && oItem[sMutator]().toLowerCase().startsWith(sLowerCaseValue); }); return aFilteredItems; }; /** * Updates and synchronizes the <code>selectedItem</code> association, <code>selectedItemId</code> * and <code>selectedKey</code> properties. * * @param {sap.ui.core.Item | null} vItem The selected item * @private */ ComboBox.prototype.setSelection = function(vItem) { var oList = this._getList(), oListItem, sKey; this.setAssociation("selectedItem", vItem); this._setPropertyProtected("selectedItemId", (vItem instanceof Item) ? vItem.getId() : vItem, true); if (typeof vItem === "string") { vItem = Element.getElementById(vItem); } if (oList) { oListItem = ListHelpers.getListItem(vItem); if (oListItem) { oList.setSelectedItem(oListItem, true); } else { oList.removeSelections(true); } } sKey = vItem ? vItem.getKey() : this.getMetadata().getProperty("selectedKey").defaultValue; this._setPropertyProtected("selectedKey", sKey); }; /** * Determines whether the <code>selectedItem</code> association and <code>selectedKey</code> * property are synchronized. * * @returns {boolean} Whether the selection is synchronized * @private * @since 1.24.0 */ ComboBox.prototype.isSelectionSynchronized = function() { var vItem = this.getSelectedItem(); return this.getSelectedKey() === (vItem && vItem.getKey()); }; /** * Indicates whether the provided item is selected. * * @param {sap.ui.core.Item} vItem The item to be checked * @returns {boolean} True if the item is selected * @private * @since 1.24.0 */ ComboBox.prototype.isItemSelected = function(vItem) { return vItem && (vItem.getId() === this.getAssociation("selectedItem")); }; /** * Sets an association of the ComboBox with given name. * * @param {string} sAssociationName The name of the association. * @param {string} sId The ID which should be set as association. * @param {boolean} bSuppressInvalidate Should the control invalidation be suppressed. * @returns {this} <code>this</code> to allow method chaining * @private * @since 1.22.1 */ ComboBox.prototype.setAssociation = function(sAssociationName, sId, bSuppressInvalidate) { var oList = this._getList(); if (oList && (sAssociationName === "selectedItem")) { // propagate the value of the "selectedItem" association to the list if (!(sId instanceof Item)) { sId = this.findItem("id", sId); } oList.setSelectedItem(ListHelpers.getListItem(sId), true); } return ComboBoxBase.prototype.setAssociation.apply(this, arguments); }; /** * Removes all the ids in the association named <code>sAssociationName</code>. * * @param {string} sAssociationName The name of the association. * @param {boolean} bSuppressInvalidate Should the control invalidation be suppressed. * @returns {string[]} An array with the removed IDs * @private * @since 1.22.1 */ ComboBox.prototype.removeAllAssociation = function(sAssociationName, bSuppressInvalidate) { var oList = this._getList(); if (oList && (sAssociationName === "selectedItem")) { List.prototype.removeAllAssociation.apply(oList, arguments); } return ComboBoxBase.prototype.removeAllAssociation.apply(this, arguments); }; /* =========================================================== */ /* Lifecycle methods */ /* =========================================================== */ /** * This method will be called when the ComboBox is initially created. * * @protected */ ComboBox.prototype.init = function() { ComboBoxBase.prototype.init.apply(this, arguments); this.bOpenValueStateMessage = true; this._sValueBeforeOpen = ""; // stores the value of the input before opening the picker this._sInputValueBeforeOpen = ""; // the last selected item before opening the picker this._oSelectedItemBeforeOpen = null; if (Device.system.phone) { this.attachEvent("_change", this.onPropertyChange, this); } // holds reference to the last focused GroupHeaderListItem if such exists this.setLastFocusedListItem(null); }; /** * This event handler will be called before the ComboBox is rendered. * * @protected */ ComboBox.prototype.onBeforeRendering = function() { ComboBoxBase.prototype.onBeforeRendering.apply(this, arguments); var aItems = this.getItems(); var oClearIcon = this.getShowClearIcon() ? this._getClearIcon() : this._oClearIcon; if (this.getRecreateItems()) { ListHelpers.fillList(aItems, this._getList(), this._mapItemToListItem.bind(this)); this.setRecreateItems(false); } this.synchronizeSelection(); if (!this.isOpen() && document.activeElement === this.getFocusDomRef() && this.getEnabled()) { this.addStyleClass("sapMFocus"); } // if selected item is not among items => select default item if (this.getSelectedItem() && aItems.indexOf(this.getSelectedItem()) === -1) { var sValue = this.getValue(); this.clearSelection(); this.setValue(sValue); } if (!oClearIcon) { return; } if (this.shouldShowClearIcon()) { oClearIcon.removeStyleClass("sapMComboBoxBaseHideClearIcon"); return; } oClearIcon.addStyleClass("sapMComboBoxBaseHideClearIcon"); }; /** * This method will be called when the ComboBox is being destroyed. * * @protected */ ComboBox.prototype.exit = function () { ComboBoxBase.prototype.exit.apply(this, arguments); this._oSelectedItemBeforeOpen = null; this._bInputFired = null; this.setLastFocusedListItem(null); }; /** * This event handler will be called before the ComboBox's Picker is rendered. * * @protected */ ComboBox.prototype.onBeforeRenderingPicker = function() { var fnOnBeforeRenderingPickerType = this["onBeforeRendering" + this.getPickerType()]; fnOnBeforeRenderingPickerType && fnOnBeforeRenderingPickerType.call(this); }; /** * This event handler will be called before the ComboBox' Picker of type <code>sap.m.Popover</code> is rendered. * * @protected */ ComboBox.prototype.onBeforeRenderingDropdown = function() { var oPopover = this.getPicker(), sWidth = (this.$().outerWidth() / parseFloat(library.BaseFontSize)) + "rem"; if (oPopover) { oPopover.setContentMinWidth(sWidth); } }; /** * This event handler will be called before the ComboBox Picker's List is rendered. * * @protected */ ComboBox.prototype.onBeforeRenderingList = function() { if (this.bProcessingLoadItemsEvent) { var oList = this._getList(), oFocusDomRef = this.getFocusDomRef(); if (oList) { oList.setBusy(true); } if (oFocusDomRef) { oFocusDomRef.setAttribute("aria-busy", "true"); } } }; /** * This event handler will be called after the ComboBox's Picker is rendered. * * @protected */ ComboBox.prototype.onAfterRenderingPicker = function() { var fnOnAfterRenderingPickerType = this["onAfterRendering" + this.getPickerType()]; var iInputWidth = this.getDomRef().getBoundingClientRect().width; var sPopoverMaxWidth = getComputedStyle(this.getDomRef()).getPropertyValue("--sPopoverMaxWidth"); fnOnAfterRenderingPickerType && fnOnAfterRenderingPickerType.call(this); // hide the list while scrolling to selected item, if necessary fnSelectedItemOnViewPort.call(this, false); if (iInputWidth <= parseInt(sPopoverMaxWidth) && !Device.system.phone) { this.getPicker().addStyleClass("sapMSuggestionPopoverDefaultWidth"); } else { this.getPicker().getDomRef().style.setProperty("max-width", iInputWidth + "px"); this.getPicker().addStyleClass("sapMSuggestionPopoverInputWidth"); } }; /** * This event handler will be called after the ComboBox Picker's List is rendered. * * @protected */ ComboBox.prototype.onAfterRenderingList = function() { var oSelectedItem = this.getSelectedItem(), oSelectedListItem = ListHelpers.getListItem(oSelectedItem); if (this.bProcessingLoadItemsEvent && !this.bItemsUpdated && (this.getItems().length === 0)) { return; } var oList = this._getList(), oFocusDomRef = this.getFocusDomRef(); this.highlightList(this._sInputValueBeforeOpen); if (oSelectedItem) { oList.setSelectedItem(oSelectedListItem); this.setLastFocusedListItem(oSelectedListItem); } if (oList) { oList.setBusy(false); } if (oFocusDomRef) { oFocusDomRef.removeAttribute("aria-busy"); } }; /* =========================================================== */ /* Filtering */ /* =========================================================== */ /** * Filters the items of the ComboBox, using the <code>filterItems</code> module. * * @param {string} sValue The value, to be used as a filter * @returns {Object} A result object, containing the matching items and list groups * @private */ ComboBox.prototype.filterItems = function(sValue) { return filterItems(this, this.getItems(), sValue, true, this.getFilterSecondaryValues(), this.fnFilter || inputsDefaultFilter); }; /** * Maps items of <code>sap.ui.core.Item</code> type to <code>sap.m.StandardListItem</code> items. * * @param {sap.ui.core.Item} oItem The item to be matched * @returns {sap.m.StandardListItem | sap.m.GroupHeaderListItem | null} The matched StandardListItem * @private */ ComboBox.prototype._mapItemToListItem = function (oItem) { var oListItem = ListHelpers.createListItemFromCoreItem(oItem, this.getShowSecondaryValues()); if (oItem.isA("sap.ui.core.Item")) { this.setSelectable(oItem, oItem.getEnabled()); } oListItem.addStyleClass(this.getRenderer().CSS_CLASS_COMBOBOXBASE + "NonInteractiveItem"); return oListItem; }; /* =========================================================== */ /* Event handlers */ /* =========================================================== */ /** * Handles the <code>input</code> event on the input field. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.oninput = function(oEvent) { ComboBoxBase.prototype.oninput.apply(this, arguments); this.syncPickerContent(); // notice that the input event can be buggy in some web browsers, // @see sap.m.InputBase#oninput if (oEvent.isMarked("invalid")) { return; } this._bInputFired = true; this.loadItems(function() { this.handleInputValidation(oEvent); }, { name: "input", busyIndicator: false } ); // if the loadItems event is being processed, // we need to open the dropdown list to show the busy indicator if (this.bProcessingLoadItemsEvent && (this.getPickerType() === "Dropdown")) { if (this.isOpen() && !this.getValue()) { this.close(); } else { this.open(); } } if (this.getLastFocusedListItem()) { this.getLastFocusedListItem().removeStyleClass("sapMLIBFocused"); this.setLastFocusedListItem(null); } // always focus input field when typing in it this.addStyleClass("sapMFocus"); this._getList().removeStyleClass("sapMListFocus"); // if recommendations were shown - add the icon pressed style if (this._getItemsShownWithFilter()) { this.toggleIconPressedStyle(true); } }; /** * Handles the input event on the input field. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.handleInputValidation = function (oEvent) { var aVisibleItems, aCommonStartsWithItems, oFirstVisibleItem, bCurrentlySelectedItemVisible, oSelectedItem = this.getSelectedItem(), sValue = oEvent.target.value, bEmptyValue = sValue === "", oControl = oEvent.srcControl, bToggleOpenState = (this.getPickerType() === "Dropdown"), oListItem = ListHelpers.getListItem(oSelectedItem), oFilterResults = this.filterItems(sValue); if (bEmptyValue && !this.bOpenedByKeyboardOrButton && !this.isPickerDialog()) { aVisibleItems = this.getItems(); } else { aVisibleItems = oFilterResults.items; itemsVisibilityHandler(this.getItems(), oFilterResults); } oFirstVisibleItem = aVisibleItems[0]; // first item that matches the value bCurrentlySelectedItemVisible = aVisibleItems.some(function (oItem) { return oItem.getKey() === this.getSelectedKey(); }, this); aCommonStartsWithItems = this.intersectItems(this._filterStartsWithItems(sValue, 'getText'), aVisibleItems); // In some cases, the filtered items may only be shown because of second, // third, etc term matched the typed in by the user value. However, if the ComboBox // has selectedKey already, and this key corresponds to an item, which is already not // visible after the filtering, the selection does not correspond to the users input. // In such cases: // - The selectedKey will be cleared so no "hidden" selection is left in the ComboBox // - Further validation is required from application side as the ComboBox allows input // that does not match any item from the list. if (oFirstVisibleItem && this.getSelectedKey() && !bCurrentlySelectedItemVisible) { this.setSelection(null); } const bExactMatch = aCommonStartsWithItems.some((item) => item.getText() === sValue); if (!bEmptyValue && oControl && (oControl._bDoTypeAhead || bExactMatch)) { this.handleTypeAhead(oControl, aVisibleItems, sValue); } else if (!bEmptyValue && aCommonStartsWithItems[0] && sValue === aCommonStartsWithItems[0].getText()) { this.setSelection(aCommonStartsWithItems[0]); } else { this.setSelection(null); } if (oSelectedItem !== this.getSelectedItem()) { this.fireSelectionChange({ selectedItem: this.getSelectedItem() }); oListItem = ListHelpers.getListItem(this.getSelectedItem()); } const sSelectedItemText = this.getSelectedItem()?.getText(); const slastInputValue = this.getLastValue(); if (sValue.toLowerCase() === sSelectedItemText?.toLowerCase()) { this.setValue(sSelectedItemText); sValue = sSelectedItemText; this.setLastValue(slastInputValue); } this._sInputValueBeforeOpen = sValue; if (this.isOpen()) { setTimeout(function () { this.highlightList(sValue); }.bind(this)); } if (oFirstVisibleItem) { if (bEmptyValue && !this.bOpenedByKeyboardOrButton) { this.close(); } else if (bToggleOpenState) { this.open(); scrollToItem(oListItem, this.getPicker()); } } else if (this.isOpen()) { if (bToggleOpenState && !this.bOpenedByKeyboardOrButton) { this.close(); } } else { this.clearFilter(); } }; /** * Handles the type ahead functionality on the input field. * * @param {sap.m.Input} oInput The input control * @param {sap.ui.core.Item[]} aItems The array of items * @param {string} sValue The input text value * @private */ ComboBox.prototype.handleTypeAhead = function (oInput, aItems, sValue) { var aItemTexts, bSearchBoth = this.getFilterSecondaryValues(), aMatchingItems = typeAhead(sValue, oInput, aItems, function (oItem) { aItemTexts = [oItem.getText()]; if (bSearchBoth) { aItemTexts.push(oItem.getAdditionalText()); } return aItemTexts; }); this.setSelection(aMatchingItems[0]); // always focus input field when typing in it this.addStyleClass("sapMFocus"); this._getList().removeStyleClass("sapMListFocus"); }; /** * Handles the <code>selectionChange</code> event on the list. * * @param {sap.ui.base.Event} oControlEvent The control event * @private */ ComboBox.prototype.onSelectionChange = function(oControlEvent) { var oItem = ListHelpers.getItemByListItem(this.getItems(), oControlEvent.getParameter("listItem")), mParam = this.getChangeEventParams(), bSelectedItemChanged = (oItem !== this.getSelectedItem()); oItem && this.updateDomValue(oItem.getText()); this.setSelection(oItem); this.fireSelectionChange({ selectedItem: this.getSelectedItem() }); if (bSelectedItemChanged) { mParam.itemPressed = true; this.onChange(null, mParam); } }; /** * Handles the <code>ItemPress</code> event on the list. * * @param {sap.ui.base.Event} oControlEvent The control event * @since 1.32.4 * @private */ ComboBox.prototype.onItemPress = function (oControlEvent) { var oListItem = oControlEvent.getParameter("listItem"), sText = oListItem.getTitle(), mParam = this.getChangeEventParams(), bSelectedItemChanged = (oListItem !== ListHelpers.getListItem(this.getSelectedItem())); if (oListItem.isA("sap.m.GroupHeaderListItem")) { return; } this.setLastFocusedListItem(oListItem); this.updateDomValue(sText); // if a highlighted item is pressed fire change event if (!bSelectedItemChanged) { mParam.itemPressed = true; this.onChange(null, mParam); } this._setPropertyProtected("value", sText, true); // deselect the text and move the text cursor at the endmost position if (this.getPickerType() === "Dropdown" && !this.isPlatformTablet()) { this.selectText(this.getValue().length, this.getValue().length); } this.close(); }; /** * This event handler is called before the picker popup is opened. * * @protected */ ComboBox.prototype.onBeforeOpen = function() { ComboBoxBase.prototype.onBeforeOpen.apply(this, arguments); var oSuggestionsPopover = this._getSuggestionsPopover(); var fnPickerTypeBeforeOpen = this["onBeforeOpen" + this.getPickerType()]; this.setProperty("_open", true); // the dropdown list can be opened by calling the .open() method (without // any end user interaction), in this case if items are not already loaded // and there is an {@link #loadItems} event listener attached, the items should be loaded if (this.hasLoadItemsEventListeners() && !this.bProcessingLoadItemsEvent) { this.loadItems(); } // call the hook to add additional content to the list this.addContent(); fnPickerTypeBeforeOpen && fnPickerTypeBeforeOpen.call(this); oSuggestionsPopover.resizePopup(this); }; /** * This event handler is called before the picker dialog is opened. * * @private */ ComboBox.prototype.onBeforeOpenDialog = function() { var oPickerTextField = this.getPickerTextField(); this._oSelectedItemBeforeOpen = this.getSelectedItem(); this._sValueBeforeOpen = this.getValue(); this.getSelectedItem() && itemsVisibilityHandler(this.getItems(), this.filterItems("")); oPickerTextField.setValue(this._sValueBeforeOpen); }; /** * This event handler is called after the picker popup is opened. * * @private */ ComboBox.prototype.onAfterOpen = function() { var oItem = this.getSelectedItem(), oSelectionRange = selectionRange(this.getFocusDomRef()), bTablet = this.isPlatformTablet(); this.closeValueStateMessage(); // if there is a selected item, scroll and show the list fnSelectedItemOnViewPort.call(this, true); /** * Some android devices such as Galaxy Tab 3 are not returning the correct selection of text fields */ if (!bTablet && oItem && oSelectionRange.start === oSelectionRange.end && oSelectionRange.start > 1) { setTimeout(function() { this.selectText(0, oSelectionRange.end); }.bind(this), 0); } }; /** * This event handler is called before the picker popup is closed. * * @private */ ComboBox.prototype.onBeforeClose = function() { ComboBoxBase.prototype.onBeforeClose.apply(this, arguments); var oDomRef = this.getFocusDomRef(); this.setProperty("_open", false); if (document.activeElement === oDomRef) { this.updateFocusOnClose(); } // remove the active state of the control's field this.toggleIconPressedStyle(false); }; /** * This event handler is called after the picker popup is closed. * * @private */ ComboBox.prototype.onAfterClose = function() { // clear the filter to make all items visible, // notice that to prevent flickering, the filter is cleared // after the close animation is completed this.clearFilter(); this._sInputValueBeforeOpen = ""; if (this.isPickerDialog()) { ComboBoxBase.prototype.closeValueStateMessage.apply(this, arguments); } }; /** * Handles properties' changes of items in the aggregation named <code>items</code>. * * @param {sap.ui.base.Event} oControlEvent The control event * @since 1.28 * @private */ ComboBox.prototype.onItemChange = function(oControlEvent) { var sSelectedItemId = this.getAssociation("selectedItem"), sNewValue = oControlEvent.getParameter("newValue"), sProperty = oControlEvent.getParameter("name"); // if the selected item has changed, synchronization is needed if (sSelectedItemId === oControlEvent.getParameter("id")) { switch (sProperty) { case "text": if (!this.isBound("value")) { this.setValue(sNewValue); } break; case "key": if (!this.isBound("selectedKey")) { this.setSelectedKey(sNewValue); } break; // no default } } return ComboBoxBase.prototype.onItemChange.call(this, oControlEvent, this.getShowSecondaryValues()); }; /* ----------------------------------------------------------- */ /* Keyboard handling */ /* ----------------------------------------------------------- */ /** * Handles the <code>keydown</code> event when any key is pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onkeydown = function(oEvent) { var oControl = oEvent.srcControl; ComboBoxBase.prototype.onkeydown.apply(oControl, arguments); if (!oControl.getEnabled() || !oControl.getEditable()) { return; } var mKeyCode = KeyCodes; // disable the typeahead feature for android devices due to an issue on android soft keyboard, which always returns keyCode 229 oControl._bDoTypeAhead = !Device.os.android && (oEvent.which !== mKeyCode.BACKSPACE) && (oEvent.which !== mKeyCode.DELETE); }; /** * Handles the <code>cut</code> event when the CTRL and X keys are pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.oncut = function(oEvent) { var oControl = oEvent.srcControl; ComboBoxBase.prototype.oncut.apply(oControl, arguments); oControl._bDoTypeAhead = false; }; /** * Handles the <code>sapenter</code> event when the Enter key is pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onsapenter = function(oEvent) { var oControl = oEvent.srcControl, oItem = oControl.getSelectedItem(), oSuggestionPopover = oControl._getSuggestionsPopover(), oFocusedItem = oSuggestionPopover && oSuggestionPopover.getFocusedListItem(); if (oItem && this.getFilterSecondaryValues()) { oControl.updateDomValue(oItem.getText()); } ComboBoxBase.prototype.onsapenter.apply(oControl, arguments); // in case of a non-editable or disabled combo box, the selection cannot be modified if (!oControl.getEnabled() || !oControl.getEditable()) { return; } // prevent closing of popover, when Enter is pressed on a group header if (oFocusedItem && oFocusedItem.isA("sap.m.GroupHeaderListItem")) { return; } if (oControl.isOpen() && !this.isComposingCharacter()) { oControl.close(); } }; /** * Handles the <code>sapup</code>, <code>sapdown</code>, <code>sappageup</code>, <code>sappagedown</code>, * <code>saphome</code>, <code>sapend</code> pseudo events when the Up/Down/Page Up/Page Down/Home/End key is pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ["onsapup", "onsapdown", "onsappageup", "onsappagedown", "onsaphome", "onsapend"].forEach(function(sName) { ComboBox.prototype[sName] = function (oEvent) { this.handleListNavigation(oEvent, sName); }; }); /** * Handles the list navigation * * @param {jQuery.Event} oEvent The event object. * @param {string} sName The event name. * @private */ ComboBox.prototype.handleListNavigation = function (oEvent, sName) { var oControl = oEvent.srcControl; // in case of a non-editable or disabled combo box, the selection cannot be modified if (!oControl.getEnabled() || !oControl.getEditable()) { return; } // prevent document scrolling when page up key is pressed oEvent.preventDefault(); this.loadItems(function() { this.syncPickerContent(); if (!this.isOpen()) { this.handleInlineListNavigation(sName); } else { var oSuggestionsPopover = this._getSuggestionsPopover(); oSuggestionsPopover && oSuggestionsPopover.handleListNavigation(this, oEvent, !!this.getSelectedItem()); } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); }); }; /** * Handles the list navigation, when the picker is closed. * * @param {string} sName The event name. * @private */ ComboBox.prototype.handleInlineListNavigation = function (sName) { var aItems = this.getItems(), aSelectableItems = ListHelpers.getSelectableItems(aItems), oSelectedItem = this.getSelectedItem(), iIndex; // calculates the index of the next item, depending on the pressed key switch (sName) { case "onsapdown": iIndex = aSelectableItems.indexOf(oSelectedItem) + 1; break; case "onsapup": iIndex = oSelectedItem ? aSelectableItems.indexOf(oSelectedItem) - 1 : aSelectableItems.length - 1; break; case "onsapend": iIndex = aSelectableItems.length - 1; break; case "onsaphome": iIndex = 0; break; case "onsappagedown": iIndex = Math.min(aSelectableItems.length - 1, aSelectableItems.indexOf(oSelectedItem) + 10); break; case "onsappageup": iIndex = Math.max(0, aSelectableItems.indexOf(oSelectedItem) - 10); break; } this.handleSelectionFromList(aSelectableItems[iIndex]); }; /** * Handles the list selection. * * @param {sap.ui.core.Item | sap.m.StandardListItem | sap.m.GroupHeaderListItem} oItem The item to be selected * @private */ ComboBox.prototype.handleSelectionFromList = function (oItem) { if (!oItem) { return; } var oDomRef = this.getFocusDomRef(), sTypedValue = oDomRef.value.substring(0, oDomRef.selectionStart), oSelectedItem = this.getSelectedItem(), oLastFocusedItem = this.getLastFocusedListItem(), oListItem, sItemText, iSelectionStart, bLastFocusOnGroup; // if the navigation is inline, the passed item will be a core item, // otherwise it is a list item if (oItem.isA("sap.m.StandardListItem") || oItem.isA("sap.m.GroupHeaderListItem")) { oListItem = oItem; oItem = ListHelpers.getItemByListItem(this.getItems(), oItem); } else { oListItem = ListHelpers.getListItem(oItem); } this.setSelection(oItem); this.setLastFocusedListItem(oListItem); if (oItem.isA("sap.ui.core.SeparatorItem")) { // when visual focus moves to the group header item // we should deselect and leave only the input typed in by the user this.setSelectedItem(null); this.updateDomValue(sTypedValue); this.fireSelectionChange({ selectedItem: null }); return; } if (oItem !== oSelectedItem) { sItemText = oItem.getText(); bLastFocusOnGroup = oLastFocusedItem && oLastFocusedItem.isA("sap.m.GroupHeaderListItem"); iSelectionStart = calculateSelectionStart(selectionRange(oDomRef, bLastFocusOnGroup) , sItemText, sTypedValue, bLastFocusOnGroup); this.updateDomValue(sItemText); this.fireSelectionChange({ selectedItem: oItem }); // update the selected item after the change event is fired (the selection may change) oItem = this.getSelectedItem(); this.selectText(iSelectionStart, oDomRef.value.length); } }; /** * Sets the last focused list item. * * @param {sap.m.StandardListItem | sap.m.GroupHeaderListItem} oListItem The item that is focused. * @private */ ComboBox.prototype.setLastFocusedListItem = function(oListItem) { this._oLastFocusedListItem = oListItem; }; /** * Gets the last focused list item. * * @private */ ComboBox.prototype.getLastFocusedListItem = function() { return this._oLastFocusedListItem; }; /** * Handles the <code>onsapshow</code> event when either F4 is pressed or Alt + Down arrow are pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onsapshow = function(oEvent) { var aSelectableItems, oItem, bEditable = this.getEditable(), oListItem; ComboBoxBase.prototype.onsapshow.apply(this, arguments); this.syncPickerContent(); if (!this.getValue() && bEditable) { aSelectableItems = ListHelpers.getSelectableItems(this.getItems()); oItem = aSelectableItems[0]; if (oItem) { oListItem = ListHelpers.getListItem(oItem); if (this.isOpen()) { this._getSuggestionsPopover().updateFocus(this, oListItem); this.setLastFocusedListItem(oListItem); } else { this.addStyleClass("sapMFocus"); } this.setSelection(oItem); this.updateDomValue(oItem.getText()); this.fireSelectionChange({ selectedItem: oItem }); setTimeout(function() { this.selectText(0, oItem.getText().length); }.bind(this), 0); } } }; /** * Handles when Alt + Up arrow are pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onsaphide = ComboBox.prototype.onsapshow; /** * Called when the <code>ComboBox</code> is clicked or tapped. * * @public * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.ontap = function(oEvent) { if (!this.getEnabled()) { return; } if (!this.isMobileDevice()) { this.openValueStateMessage(); } this.updateFocusOnClose(); }; ComboBox.prototype.updateFocusOnClose = function() { var oDomRef = this.getFocusDomRef(), oSuggestionsPopover = this._getSuggestionsPopover(); this.setLastFocusedListItem(null); if (oSuggestionsPopover) { oSuggestionsPopover.setValueStateActiveState(false); oSuggestionsPopover.updateFocus(this); } oDomRef.removeAttribute( "aria-activedescendant"); }; ComboBox.prototype.onmouseup = function () { if (this.getPickerType() === "Dropdown" && document.activeElement === this.getFocusDomRef() && !this.getSelectedText()) { this.selectText(0, this.getValue().length); } }; /** * Handles the <code>focusin</code> event. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onfocusin = function(oEvent) { var bDropdownPickerType = this.getPickerType() === "Dropdown"; // when having an open dialog and destroy is called // the popup is destroyed and the focus is return back to the combobox // which checks for the presence of an icon which is already destroyed if (this._bIsBeingDestroyed) { return; } // the downward-facing arrow button is receiving focus if (oEvent.target === this.getOpenArea()) { // the value state message can not be opened if click on the open area this.bOpenValueStateMessage = false; // avoid the text-editing mode popup to be open on mobile, // text-editing mode disturbs the usability experience (it blocks the UI in some devices) // note: This occurs only in some specific mobile devices if (bDropdownPickerType && !this.isPlatformTablet()) { // force the focus to stay in the input field this.focus(); } // probably the input field is receiving focus } else { // open the message popup if (!this.isOpen() && this.bOpenValueStateMessage && this.shouldValueStateMessageBeOpened()) { this.openValueStateMessage(); } this.bOpenValueStateMessage = true; } if (this.getEnabled() && (!this.isOpen() || !this.getSelectedItem() || !this._getList().hasStyleClass("sapMListFocus"))) { this.addStyleClass("sapMFocus"); } }; /** * Handles the <code>sapfocusleave</code> pseudo event. * * @param {jQuery.Event} oEvent The event object. * @private */ ComboBox.prototype.onsapfocusleave = function(oEvent) { var bTablet, oPicker, oRelatedControl, oFocusDomRef, oItem = this.getSelectedItem(); if (oItem && this.getFilterSecondaryValues()) { this.updateDomValue(oItem.getText()); } ComboBoxBase.prototype.onsapfocusleave.apply(this, arguments); if (this.isPickerDialog()) { return; } oPicker = this.getPicker(); if (!oEvent.relatedControlId || !oPicker) { return; } bTablet = this.isPlatformTablet(); oRelatedControl = Element.getElementById(oEvent.relatedControlId); oFocusDomRef = oRelatedControl && oRelatedControl.getFocusDomRef(); if (containsOrEquals(oPicker.getFocusDomRef(), oFocusDomRef) && !bTablet && !(this._getSuggestionsPopover().getValueStateActiveState())) { // force the focus to stay in the input field this.focus(); } }; /* =========================================================== */ /* API methods */ /* =========================================================== */ /** * Synchronizes the <code>selectedItem</code> association and the <code>selectedItemId</code> property. * * @protected */ ComboBox.prototype.synchronizeSelection = function() { if (this.isSelectionSynchronized()) { return; } var sKey = this.getSelectedKey(), vItem = this.getItemByKey("" + sKey); // find the first item with the given key // if there is an item that match with the "selectedKey" property and // the "selectedKey" property does not have the default value if (vItem && (sKey !== "")) { this.setAssociation("selectedItem", vItem, true); this._setPropertyProtected("selectedItemId", vItem.getId(), true); this.setValue(vItem.getText()); this._sValue = this.getValue(); } }; /** * <code>ComboBox</code> picker configuration * * @param {sap.m.Popover | sap.m.Dialog} oPicker Picker instance * @protected */ ComboBox.prototype.configPicker = function (oPicker) { var oRenderer = this.getRenderer(), CSS_CLASS = oRenderer.CSS_CLASS_COMBOBOXBASE; oPicker.setHorizontalScrolling(false) .addStyleClass(CSS_CLASS + "Picker") .addStyleClass(CSS_CLASS + "Picker-CTX") .attachBeforeOpen(this.onBeforeOpen, this) .attachAfterOpen(this.onAfterOpen, this) .attachBeforeClose(this.onBeforeClose, this) .attachAfterClose(this.onAfterClose, this) .addEventDelegate({ onBeforeRendering: this.onBeforeRenderingPicker, onAfterRendering: this.onAfterRenderingPicker }, this); }; /** * Configures the SuggestionsPopover's list. * * @param {sap.m.List} oList The list instance to be configured * @protected */ ComboBox.prototype._configureList = function (oList) { var oRenderer = this.getRenderer(); if (!oList) { return; } // apply aria role="listbox" to List control oList.applyAriaRole("listbox"); // configure the list oList.setMode(ListMode.SingleSelectMaster) .addStyleClass(oRenderer.CSS_CLASS_COMBOBOXBASE + "List") .addStyleClass(oRenderer.CSS_CLASS_COMBOBOX + "List"); // attach event handlers oList .attachSelectionChange(this.onSelectionChange, this) .attachItemPress(this.onItemPress, this); // attach event delegates oList.addEventDelegate({ onBeforeRendering: this.onBeforeRenderingList, onAfterRendering: this.onAfterRenderingList }, this); }; /** * Gets the default selected item from the aggregation named <code>items</code>. * * @returns {null} Null, as there is no default selected item * @protected */ ComboBox.prototype.getDefaultSelectedItem = function() { return null; }; ComboBox.prototype.getChangeEventParams = function() { return { itemPressed: false }; }; /** * Clears the selection. * * @protected */ ComboBox.prototype.clearSelection = function() { this.setAssociation("selectedItem", null); this.setSelectedItemId(""); this.setSelectedKey(""); }; /** * Sets the start and end positions of the current text selection. * * @param {int} iSelectionStart The index of the first selected character. * @param {int} iSelectionEnd The index of the character after the last selected character. * @returns {this} <code>this</code> to allow method chaining * @protected * @since 1.22.1 */ ComboBox.prototype.selectText = function(iSelectionStart, iSelectionEnd) { ComboBoxBase.prototype.selectText.apply(this, arguments); return this; }; /** * Clones the <code>sap.m.ComboBox</code> control. * * @param {string} [sIdSuffix] Suffix to be added to the IDs of the new control and its internal objects. * @returns {this} The cloned <code>sap.m.ComboBox</code> control. * @public * @since 1.22.1 */ ComboBox.prototype.clone = function(sIdSuffix) { var oComboBoxClone = ComboBoxBase.prototype.clone.apply(this, arguments), oList = this._getList(); // ensure that selected item is cleared, but keep key // cloning can't have a reference to an item of other ComboBox oComboBoxClone.setAssociation("selectedItem", null); if (!this.isBound("items") && oList) { oComboBoxClone.syncPickerContent(); oComboBoxClone.setSelectedIndex(this.indexOfItem(this.getSelectedItem())); } return oComboBoxClone; }; /* ----------------------------------------------------------- */ /* public metho