UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,654 lines (1,359 loc) 56.8 kB
/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2022 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ './InputBase', './ComboBoxTextField', './ComboBoxBase', './Popover', './SelectList', './library', 'sap/ui/Device', 'sap/ui/core/Item', './ComboBoxRenderer', "sap/ui/dom/containsOrEquals", "sap/ui/events/KeyCodes", "sap/base/security/encodeXML" ], function( InputBase, ComboBoxTextField, ComboBoxBase, Popover, SelectList, library, Device, Item, ComboBoxRenderer, containsOrEquals, KeyCodes, encodeXML ) { "use strict"; /** * 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.</li> * </ul> * By setting the <code>showSecondaryValues</code> property, the combo box can display an additional value for each option (if there is one). * <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> * <h3>Responsive Behavior</h3> * <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.60.39 * * @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} * @ui5-metamodel This control will also be described in the UI5 (legacy) design time meta model. */ var ComboBox = ComboBoxBase.extend("sap.m.ComboBox", /** @lends sap.m.ComboBox.prototype */ { metadata: { 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 } }, 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> * </ul> * * In addition, this event is also fired when an item in the list is selected. */ 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" } } } } } }); /* =========================================================== */ /* Private methods */ /* =========================================================== */ function fnHandleKeyboardNavigation(oControl, oItem) { if (!oItem) { return; } var oDomRef = oControl.getFocusDomRef(), iSelectionStart = oDomRef.selectionStart, iSelectionEnd = oDomRef.selectionEnd, bIsTextSelected = iSelectionStart !== iSelectionEnd, sTypedValue = oDomRef.value.substring(0, oDomRef.selectionStart), oSelectedItem = this.getSelectedItem(); this.setSelection(oItem); if (oItem !== oSelectedItem) { oControl.updateDomValue(oItem.getText()); this.fireSelectionChange({ selectedItem: oItem }); // update the selected item after the change event is fired (the selection may change) oItem = this.getSelectedItem(); if (!((typeof sTypedValue == "string" && sTypedValue != "" ? oItem.getText().toLowerCase().startsWith(sTypedValue.toLowerCase()) : false)) || !bIsTextSelected) { iSelectionStart = 0; } oControl.selectText(iSelectionStart, oDomRef.value.length); } if (this.isOpen()) { this.$().removeClass("sapMFocus"); this.getList().addStyleClass("sapMSelectListFocus"); } else { this.$().addClass("sapMFocus"); } this.scrollToItem(oItem); } function fnSelectTextIfFocused(iStart, iEnd) { if (document.activeElement === this.getFocusDomRef()) { this.selectText(iStart, iEnd); } } function fnSelectedItemOnViewPort(bIsListHidden) { var oItem = this.getSelectedItem(), oItemDomRef = oItem && oItem.getDomRef(), oItemOffsetTop = oItem && oItemDomRef.offsetTop, oItemOffsetHeight = oItem && 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"); } } } /** * Handles the virtual focus of items. * * @param {sap.ui.core.Item | null} vItem The item that should be focused * @private * @since 1.32 */ ComboBox.prototype._handleAriaActiveDescendant = function(vItem) { var oDomRef = this.getFocusDomRef(), sActivedescendant = "aria-activedescendant"; if (oDomRef) { // the aria-activedescendant attribute is set when the list is rendered if (vItem && vItem.getDomRef() && this.isOpen()) { oDomRef.setAttribute(sActivedescendant, vItem.getId()); } else { oDomRef.removeAttribute(sActivedescendant); } } }; ComboBox.prototype._getSelectedItemText = function(vItem) { vItem = vItem || this.getSelectedItem(); if (!vItem) { vItem = this.getDefaultSelectedItem(); } if (vItem) { return vItem.getText(); } return ""; }; ComboBox.getMetadata().forwardAggregation( "items", { getter: ComboBox.prototype.getList, aggregation: "items" } ); ComboBox.prototype._setItemVisibility = function(oItem, bVisible) { var $OItem = oItem && oItem.$(), CSS_CLASS = "sapMSelectListItemBaseInvisible"; if (bVisible) { oItem.bVisible = true; $OItem.length && $OItem.removeClass(CSS_CLASS); } else { oItem.bVisible = false; $OItem.length && $OItem.addClass(CSS_CLASS); } }; /** * Handles highlighting of items after filtering. * * @param {string} sValue The value of the item * @private * @since 1.48 */ ComboBox.prototype._highlightList = function(sValue) { var aItems = this.getVisibleItems(); var aListItemsText = []; var aListItemAdditionalText = []; var oItemAdditionalTextRef, oItemTextRef; aItems.forEach(function (oItem) { var oItemDomRef = oItem.getDomRef(); if (oItemDomRef === null) { return; } oItemAdditionalTextRef = oItemDomRef.children[1]; oItemTextRef = Array.prototype.filter.call(oItemDomRef.children, function(oChildRef) { return oChildRef.tagName.toLowerCase() !== "b"; })[0] || oItemDomRef; // store a DOM and an additional text to be matched if (oItemAdditionalTextRef && oItem.getAdditionalText) { aListItemAdditionalText.push({ ref: oItemAdditionalTextRef, text: oItem.getAdditionalText() }); } // store a DOM and a text to be matched oItemTextRef && aListItemsText.push({ ref: oItemTextRef, text: oItem.getText() }); }); this.highLightList(sValue, aListItemsText); this.highLightList(sValue, aListItemAdditionalText); }; /** * 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); } }; /** * Creates an instance of <code>sap.m.Popover</code>. * * @returns {sap.m.Popover} The popover instance * @private */ ComboBox.prototype.createDropdown = function() { var that = this; var oDropdown = new Popover(this.getDropdownSettings()); oDropdown.setInitialFocus(this); oDropdown.open = function() { return this.openBy(that); }; return oDropdown; }; /** * Creates an instance of <code>sap.m.ComboBoxTextField</code>. * * @returns {sap.m.ComboBoxTextField} The TextField instance * @private */ ComboBox.prototype.createPickerTextField = function() { var oTextField = new ComboBoxTextField({ width: "100%", showValueStateMessage: false, showButton: false }).addEventDelegate({ onsapenter: function() { this.updateDomValue(oTextField.getValue()); this.onChange(); } }, this); return oTextField; }; 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 the items of the ComboBox * * @param {object} mOptions Settings for filtering * @private * @returns {sap.ui.core.item[]} Array of filtered items */ ComboBox.prototype.filterItems = function(mOptions) { var aItems = this.getItems(), aFilteredItems = [], aFilteredItemsByText = [], bFilterAdditionalText = mOptions.properties.indexOf("additionalText") > -1, fnFilter = this.fnFilter || ComboBoxBase.DEFAULT_TEXT_FILTER; this._oFirstItemTextMatched = null; aItems.forEach(function (oItem) { var bMatchedByText = fnFilter.call(this, mOptions.value, oItem, "getText"); var bMatchedByAdditionalText = fnFilter.call(this, mOptions.value, oItem, "getAdditionalText"); if (bMatchedByText) { aFilteredItemsByText.push(oItem); aFilteredItems.push(oItem); } else if (bMatchedByAdditionalText && bFilterAdditionalText) { aFilteredItems.push(oItem); } }); aItems.forEach(function (oItem) { var bItemMached = aFilteredItems.indexOf(oItem) > -1; var bItemTextMached = aFilteredItemsByText.indexOf(oItem) > -1; if (!this._oFirstItemTextMatched && bItemTextMached) { this._oFirstItemTextMatched = oItem; } this._setItemVisibility(oItem, bItemMached); }, this); return aFilteredItems; }; /** * 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; }; ComboBox.prototype._getFilters = function () { return this.getFilterSecondaryValues() ? ["text", "additionalText"] : ["text"]; }; /* =========================================================== */ /* Lifecycle methods */ /* =========================================================== */ 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; // the first item with matching text property if such exists this._oFirstItemTextMatched = null; // indicated if the ComboBox is already focused this.bIsFocused = false; if (Device.system.phone) { this.attachEvent("_change", this.onPropertyChange, this); } }; ComboBox.prototype.onBeforeRendering = function() { ComboBoxBase.prototype.onBeforeRendering.apply(this, arguments); this.synchronizeSelection(); }; ComboBox.prototype.exit = function () { ComboBoxBase.prototype.exit.apply(this, arguments); this._oSelectedItemBeforeOpen = null; this._oFirstItemTextMatched = null; }; ComboBox.prototype.onBeforeRenderingPicker = function() { var fnOnBeforeRenderingPickerType = this["onBeforeRendering" + this.getPickerType()]; fnOnBeforeRenderingPickerType && fnOnBeforeRenderingPickerType.call(this); }; ComboBox.prototype.onBeforeRenderingDropdown = function() { var oPopover = this.getPicker(), sWidth = (this.$().outerWidth() / parseFloat(library.BaseFontSize)) + "rem"; if (oPopover) { oPopover.setContentMinWidth(sWidth); } }; 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"); } } }; ComboBox.prototype.onAfterRenderingPicker = function() { var fnOnAfterRenderingPickerType = this["onAfterRendering" + this.getPickerType()]; fnOnAfterRenderingPickerType && fnOnAfterRenderingPickerType.call(this); // hide the list while scrolling to selected item, if necessary fnSelectedItemOnViewPort.call(this, false); }; ComboBox.prototype.onAfterRenderingList = function() { if (this.bProcessingLoadItemsEvent && (this.getItems().length === 0)) { return; } var oList = this.getList(), oFocusDomRef = this.getFocusDomRef(); this._highlightList(this._sInputValueBeforeOpen); if (oList) { oList.setBusy(false); } if (oFocusDomRef) { oFocusDomRef.removeAttribute("aria-busy"); } }; /* =========================================================== */ /* 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); // notice that the input event can be buggy in some web browsers, // @see sap.m.InputBase#oninput if (oEvent.isMarked("invalid")) { return; } this.$().addClass("sapMFocus"); this.loadItems(function() { this.handleInputValidation(oEvent, this.isComposingCharacter()); }, { 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")) { this.open(); } // always focus input field when typing in it this.$().addClass("sapMFocus"); this.getList().removeStyleClass("sapMSelectListFocus"); }; /** * Handles the input event on the input field. * * @param {jQuery.Event} oEvent The event object. * @param {Boolean} bCompositionEvent True if the control is in composing state * @private */ ComboBox.prototype.handleInputValidation = function (oEvent, bCompositionEvent) { var oSelectedItem = this.getSelectedItem(), sValue = oEvent.target.value, bEmptyValue = sValue === "", oControl = oEvent.srcControl, aVisibleItems, bToggleOpenState = (this.getPickerType() === "Dropdown"); if (bEmptyValue && !this.bOpenedByKeyboardOrButton && !this.isPickerDialog()) { aVisibleItems = this.getItems(); } else { aVisibleItems = this.filterItems({ properties: this._getFilters(), value: sValue }); } var bItemsVisible = !!aVisibleItems.length; var oFirstVisibleItem = aVisibleItems[0]; // first item that matches the value var bCurrentlySelectedItemVisible = aVisibleItems.some(function (oItem) { return oItem.getKey() === this.getSelectedKey(); }, this); // 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 (bItemsVisible && this.getSelectedKey() && !bCurrentlySelectedItemVisible) { this.setProperty('selectedKey', null, false); } if (!bEmptyValue && oFirstVisibleItem && oFirstVisibleItem.getEnabled()) { this.handleTypeAhead(oControl, aVisibleItems, sValue, bCompositionEvent); } if (bEmptyValue || !bItemsVisible || (!oControl._bDoTypeAhead && (this._getSelectedItemText() !== sValue))) { this.setSelection(null); if (oSelectedItem !== this.getSelectedItem()) { this.fireSelectionChange({ selectedItem: this.getSelectedItem() }); } } this._sInputValueBeforeOpen = sValue; if (this.isOpen()) { this._highlightList(sValue); } if (bItemsVisible) { if (bEmptyValue && !this.bOpenedByKeyboardOrButton) { this.close(); } else if (bToggleOpenState) { this.open(); this.scrollToItem(this.getSelectedItem()); } } 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.ComboBoxTextField} oInput The input control * @param {sap.ui.core.Item[]} aItems The array of items * @param {string} sValue The input text value * @param {Boolean} bCompositionEvent True if the control is in composing state * @private */ ComboBox.prototype.handleTypeAhead = function (oInput, aItems, sValue, bCompositionEvent) { // filtered items intersercted with starts with items by text var aCommonStartsWithItems = this.intersectItems(this._filterStartsWithItems(sValue, 'getText'), aItems); var bSearchBoth = this.getFilterSecondaryValues(); var bDesktopPlatform = Device.system.desktop; var oSelectedItem = this.getSelectedItem(); if (oInput._bDoTypeAhead) { var aCommonAdditionalTextItems = this.intersectItems(this._filterStartsWithItems(sValue, 'getAdditionalText'), aItems); if (bSearchBoth && !aCommonStartsWithItems[0] && aCommonAdditionalTextItems[0]) { !bCompositionEvent && oInput.updateDomValue(aCommonAdditionalTextItems[0].getAdditionalText()); this.setSelection(aCommonAdditionalTextItems[0]); } else if (aCommonStartsWithItems[0]) { !bCompositionEvent && oInput.updateDomValue(aCommonStartsWithItems[0].getText()); this.setSelection(aCommonStartsWithItems[0]); } } else { this.setSelection(aCommonStartsWithItems[0]); } if (oSelectedItem !== this.getSelectedItem()) { this.fireSelectionChange({ selectedItem: this.getSelectedItem() }); } if (oInput._bDoTypeAhead) { if (bDesktopPlatform) { fnSelectTextIfFocused.call(oInput, sValue.length, oInput.getValue().length); } else { // timeout required for an Android and Windows Phone bug setTimeout(fnSelectTextIfFocused.bind(oInput, sValue.length, oInput.getValue().length), 0); } } // always focus input field when typing in it this.$().addClass("sapMFocus"); this.getList().removeStyleClass("sapMSelectListFocus"); }; /** * Handles the <code>selectionChange</code> event on the list. * * @param {sap.ui.base.Event} oControlEvent The control event */ ComboBox.prototype.onSelectionChange = function(oControlEvent) { var oItem = oControlEvent.getParameter("selectedItem"), mParam = this.getChangeEventParams(), bSelectedItemChanged = (oItem !== this.getSelectedItem()); 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 */ ComboBox.prototype.onItemPress = function (oControlEvent) { var oItem = oControlEvent.getParameter("item"), sText = oItem.getText(), mParam = this.getChangeEventParams(), bSelectedItemChanged = (oItem !== this.getSelectedItem()); this.updateDomValue(sText); // if a highlighted item is pressed fire change event if (!bSelectedItemChanged) { mParam.itemPressed = true; this.onChange(null, mParam); } this.setProperty("value", oItem.getText(), true); // deselect the text and move the text cursor at the endmost position if (this.getPickerType() === "Dropdown" && !this.isPlatformTablet()) { this.selectText.bind(this, this.getValue().length, this.getValue().length); } this.close(); }; /** * This event handler is called before the picker popup is opened. * * @protected */ ComboBox.prototype.onBeforeOpen = function() { var fnPickerTypeBeforeOpen = this["onBeforeOpen" + this.getPickerType()], oDomRef = this.getFocusDomRef(); // 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(); } // add the active state to the control field this.addStyleClass(InputBase.ICON_PRESSED_CSS_CLASS); if (oDomRef) { // expose a parent/child contextual relationship to assistive technologies, // notice that the "aria-owns" attribute is set when the list is visible and in view oDomRef.setAttribute("aria-owns", this.getList().getId()); } // call the hook to add additional content to the list this.addContent(); fnPickerTypeBeforeOpen && fnPickerTypeBeforeOpen.call(this); }; ComboBox.prototype.onBeforeOpenDialog = function() { var oPickerTextField = this.getPickerTextField(); this._oSelectedItemBeforeOpen = this.getSelectedItem(); this._sValueBeforeOpen = this.getValue(); if (this.getSelectedItem()) { this.filterItems({ properties: this._getFilters(), value: "" }); } oPickerTextField.setValue(this._sValueBeforeOpen); }; /** * This event handler is called after the picker popup is opened. * */ ComboBox.prototype.onAfterOpen = function() { var oDomRef = this.getFocusDomRef(), oItem = this.getSelectedItem(); if (oDomRef) { oDomRef.setAttribute("aria-expanded", "true"); // notice that the "aria-activedescendant" attribute is set when the currently active descendant is // visible and in view oItem && oDomRef.setAttribute("aria-activedescendant", oItem.getId()); } // if there is a selected item, scroll and show the list fnSelectedItemOnViewPort.call(this, true); }; /** * This event handler is called before the picker popup is closed. * */ ComboBox.prototype.onBeforeClose = function() { ComboBoxBase.prototype.onBeforeClose.apply(this, arguments); var oDomRef = this.getFocusDomRef(); if (oDomRef) { // notice that the "aria-owns" attribute is removed when the list is not visible and in view oDomRef.removeAttribute("aria-owns"); // the "aria-activedescendant" attribute is removed when the currently active descendant is not visible oDomRef.removeAttribute("aria-activedescendant"); } // remove the active state of the control's field this.removeStyleClass(InputBase.ICON_PRESSED_CSS_CLASS); }; /** * This event handler is called after the picker popup is closed. * */ ComboBox.prototype.onAfterClose = function() { var oDomRef = this.getFocusDomRef(); if (oDomRef) { oDomRef.setAttribute("aria-expanded", "false"); } // 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 the focus is back to the input after closing the picker, // the value state message should be reopen if (this.shouldValueStateMessageBeOpened() && (document.activeElement === oDomRef)) { this.openValueStateMessage(); } }; /** * Handles properties' changes of items in the aggregation named <code>items</code>. * * @param {sap.ui.base.Event} oControlEvent The control event * @since 1.28 */ 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 } } }; /* ----------------------------------------------------------- */ /* Keyboard handling */ /* ----------------------------------------------------------- */ /** * Handles the <code>keydown</code> event when any key is pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onkeydown = function(oEvent) { var oControl = oEvent.srcControl; ComboBoxBase.prototype.onkeydown.apply(oControl, arguments); if (!oControl.getEnabled() || !oControl.getEditable()) { return; } var mKeyCode = KeyCodes; oControl._bDoTypeAhead = (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. */ 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. */ ComboBox.prototype.onsapenter = function(oEvent) { var oControl = oEvent.srcControl, oItem = oControl.getSelectedItem(); 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; } if (oControl.isOpen() && !this.isComposingCharacter()) { oControl.close(); } }; /** * Handles the <code>sapdown</code> pseudo event when the Down arrow key is pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsapdown = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when arrow keys are pressed oEvent.preventDefault(); this.loadItems(function navigateToNextSelectableItem() { var aSelectableItems = this.getSelectableItems(), oNextSelectableItem; if (this.$().hasClass("sapMFocus") && this.isOpen()) { oNextSelectableItem = aSelectableItems[0]; } else { oNextSelectableItem = aSelectableItems[aSelectableItems.indexOf(this.getSelectedItem()) + 1]; } fnHandleKeyboardNavigation.call(this, oControl, oNextSelectableItem); }); }; /** * Handles the <code>sapup</code> pseudo event when the Up arrow key is pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsapup = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when arrow keys are pressed oEvent.preventDefault(); this.loadItems(function navigateToPrevSelectableItem() { var aSelectableItems = this.getSelectableItems(); var oPrevSelectableItem = aSelectableItems[aSelectableItems.indexOf(this.getSelectedItem()) - 1]; fnHandleKeyboardNavigation.call(this, oControl, oPrevSelectableItem); }); }; /** * Handles the <code>saphome</code> pseudo event when the Home key is pressed. * * The first selectable item is selected and the input field is updated accordingly. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsaphome = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when Home key is pressed oEvent.preventDefault(); this.loadItems(function navigateToFirstSelectableItem() { var oFirstSelectableItem = this.getSelectableItems()[0]; fnHandleKeyboardNavigation.call(this, oControl, oFirstSelectableItem); }); }; /** * Handles the <code>sapend</code> pseudo event when the End key is pressed. * * The last selectable item is selected and the input field is updated accordingly. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsapend = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when End key is pressed oEvent.preventDefault(); this.loadItems(function navigateToLastSelectableItem() { var oLastSelectableItem = this.findLastEnabledItem(this.getSelectableItems()); fnHandleKeyboardNavigation.call(this, oControl, oLastSelectableItem); }); }; /** * Handles the <code>sappagedown</code> pseudo event when the Page Down key is pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsappagedown = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when page down key is pressed oEvent.preventDefault(); this.loadItems(function() { var aSelectableItems = this.getSelectableItems(), iIndex = aSelectableItems.indexOf(this.getSelectedItem()) + 10, oItem; // constrain the index iIndex = (iIndex > aSelectableItems.length - 1) ? aSelectableItems.length - 1 : Math.max(0, iIndex); oItem = aSelectableItems[iIndex]; fnHandleKeyboardNavigation.call(this, oControl, oItem); }); }; /** * Handles the <code>sappageup</code> pseudo event when the Page Up key is pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsappageup = function(oEvent) { 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; } // mark the event for components that needs to know if the event was handled oEvent.setMarked(); // prevent document scrolling when page up key is pressed oEvent.preventDefault(); this.loadItems(function() { var aSelectableItems = this.getSelectableItems(), iIndex = aSelectableItems.indexOf(this.getSelectedItem()) - 10, oItem; // constrain the index iIndex = (iIndex > aSelectableItems.length - 1) ? aSelectableItems.length - 1 : Math.max(0, iIndex); oItem = aSelectableItems[iIndex]; fnHandleKeyboardNavigation.call(this, oControl, oItem); }); }; /** * Handles the <code>onsapshow</code> event when either F4 is pressed or Alt + Down arrow are pressed. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsapshow = function(oEvent) { var aSelectableItems, oItem; ComboBoxBase.prototype.onsapshow.apply(this, arguments); if (!this.getValue()) { aSelectableItems = this.getSelectableItems(); oItem = aSelectableItems[0]; if (oItem) { 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. */ ComboBox.prototype.onsaphide = ComboBox.prototype.onsapshow; /** * Handles the <code>focusin</code> event. * * @param {jQuery.Event} oEvent The event object. */ 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 { // 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) { setTimeout(function() { if (document.activeElement === this.getFocusDomRef() && !this.bIsFocused && !this.bFocusoutDueRendering && !this.getSelectedText()) { this.selectText(0, this.getValue().length); } this.bIsFocused = true; }.bind(this), 0); } // open the message popup if (!this.isOpen() && this.bOpenValueStateMessage && this.shouldValueStateMessageBeOpened()) { this.openValueStateMessage(); } this.bOpenValueStateMessage = true; } if (!this.isOpen() || !this.getSelectedItem() || !this.getList().hasStyleClass("sapMSelectListFocus")) { this.$().addClass("sapMFocus"); } }; /** * Handles the <code>sapfocusleave</code> pseudo event. * * @param {jQuery.Event} oEvent The event object. */ ComboBox.prototype.onsapfocusleave = function(oEvent) { this.bIsFocused = false; 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.getAggregation("picker"); if (!oEvent.relatedControlId || !oPicker) { return; } bTablet = this.isPlatformTablet(); oRelatedControl = sap.ui.getCore().byId(oEvent.relatedControlId); oFocusDomRef = oRelatedControl && oRelatedControl.getFocusDomRef(); if (containsOrEquals(oPicker.getFocusDomRef(), oFocusDomRef) && !bTablet) { // force the focus to stay in the input field this.focus(); } }; /* =========================================================== */ /* API methods */ /* =========================================================== */ /** * 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 */ ComboBox.prototype.setSelection = function(vItem) { var oList = this.getList(), sKey; if (oList) { oList.setSelection(vItem); } this.setAssociation("selectedItem", vItem, true); this.setProperty("selectedItemId", (vItem instanceof Item) ? vItem.getId() : vItem, true); if (typeof vItem === "string") { vItem = sap.ui.getCore().byId(vItem); } sKey = vItem ? vItem.getKey() : ""; this.setProperty("selectedKey", sKey, true); this._handleAriaActiveDescendant(vItem); }; /** * Determines whether the <code>selectedItem</code> association and <code>selectedKey</code> * property are synchronized. * * @returns {boolean} Whether the selection is synchronized * @since 1.24.0 */ ComboBox.prototype.isSelectionSynchronized = function() { var vItem = this.getSelectedItem(); return this.getSelectedKey() === (vItem && vItem.getKey()); }; /** * 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.setProperty("selectedItemId", vItem.getId(), true); // sets the value if it has not changed if (this._sValue === this.getValue()) { this.setValue(vItem.getText()); this._sValue = this.getValue(); } } }; /** * Indicates whether the list is filtered. * * @returns {boolean} True if the list is filtered * @since 1.26.0 */ ComboBox.prototype.isFiltered = function() { var oList = this.getList(); return oList && (oList.getVisibleItems().length !== oList.getItems().length); }; /** * Indicates whether an item is visible or not. * * To be overwritten by subclasses. * * @param {sap.ui.core.Item} oItem The item to be checked * @returns {boolean} Whether the item is visible. * @since 1.32.0 */ ComboBox.prototype.isItemVisible = function(oItem) { return oItem && (oItem.bVisible === undefined || oItem.bVisible); }; /** * Creates a picker popup container where the selection should take place. * * To be overwritten by subclasses. * * @param {string} sPickerType The type of the picker * @returns {sap.m.Popover | sap.m.Dialog} The picker popup to be used. * @protected */ ComboBox.prototype.createPicker = function(sPickerType) { var oPicker = this.getAggregation("picker"); if (oPicker) { return oPicker; } oPicker = this["create" + sPickerType](); // define a parent-child relationship between the control's and the picker popup this.setAggregation("picker", oPicker, true); var CSS_CLASS = this.getRenderer().CSS_CLASS_COMBOBOXBASE; // configuration 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) .addContent(this.createList()); return oPicker; }; /** * Creates an instance of <code>sap.m.SelectList</code>. * * @returns {sap.m.SelectList} The SelectList instance */ ComboBox.prototype.createList = function() { var oRenderer = this.getRenderer(); this._oList = new SelectList({ width: "100%", busyIndicatorDelay: 0 }).addStyleClass(oRenderer.CSS_CLASS_COMBOBOXBASE + "List") .addStyleClass(oRenderer.CSS_CLASS_COMBOBOX + "List") .addEventDelegate({ onBeforeRendering: this.onBeforeRenderingList, onAfterRendering: this.onAfterRenderingList }, this) .attachSelectionChange(this.onSelectionChange, this) .attachItemPress(this.onItemPress, this); return this._oList; }; /** * 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 * @since 1.24.0 */ ComboBox.prototype.isItemSelected = function(vItem) { return vItem && (vItem.getId() === this.getAssociation("selectedItem")); }; /** * 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.setSelection(null); }; /** * 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 {sap.m.ComboBox} <code>this</code> to allow method chaining * @protected * @since 1.22.1 */ ComboBox.prototype.selectText = function(iSelectionStart, iSelectionEnd) { ComboBoxBase.prototype.selectText.apply(this, arguments); this.textSelectionStart = iSelectionStart; this.textSelectionEnd = iSelectionEnd; return this; }; ComboBox.prototype.addAggregation = function(sAggregationName, oObject, bSuppressInvalidate) { if (sAggregationName === "items" && !bSuppressInvalidate && !this.isInvalidateSuppressed()) { this.invalidate(oObject); } return ComboBoxBase.prototype.addAggregation.apply(this, arguments); }; 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 SelectList.prototype.setAssociation.apply(oList, arguments); } return ComboBoxBase.prototype.setAssociation.apply(this, arguments); }; ComboBox.prototype.setProperty = function(sPropertyName, oValue, bSuppressInvalidate) { var oList = this.getList(); if (/selectedKey|selectedItemId/.test(sPropertyName)) { // propagate the value of the "selectedKey" or "selectedItemId" properties to the list oList && SelectList.prototype.setProperty.apply(oList, arguments); } return ComboBoxBase.prototype.setProperty.apply(this, arguments); }; ComboBox.prototype.removeAllAssociation = function(sAssociationName, bSuppressInvalidate) { var oList = this.getList(); if (oList && (sAssociationName === "selectedItem")) { SelectList.prototype.removeAllAssociation.apply(oList, arguments); } return ComboBoxBase.prototype.removeAllAssociation.apply(this, arguments); }; ComboBox.prototype.clone = function(sIdSuffix) { var oComboBoxClone = ComboBoxBase.prototype.clone.apply(this, arguments), oList = this.getList(); if (!this.isBound("items") && oList) { oComboBoxClone.setSelectedIndex(this.indexOfItem(this.getSelectedItem())); } return oComboBoxClone; }; /* ----------------------------------------------------------- */ /* public methods */ /* ----------------------------------------------------------- */ /** * Opens the control's picker popup. * * @returns {sap.m.ComboBoxBase} <code>this</code> to allow method chaining. * @protected */ ComboBox.prototype.open = function() { var oList = this.getList(); ComboBoxBase.prototype.open.call(this); if (this.getSelectedItem()) { oList.addStyleClass("sapMSelectListFocus"); this.$().removeClass("sapMFocus"); } return this; }; /** * Closes the control's picker popup and focus input field. * * @returns {sap.m.ComboBox} <code>this</code> to allow method chaining. * @public */ ComboBox.prototype.close = function() { var oList = this.getList(); ComboBoxBase.prototype.close.call(this); this.$().addClass("sapMFocus"); //Remove focusing class from the list oList && oList.removeStyleClass("sapMSelectListFocus"); return this; }; ComboBox.prototype.findAggregatedObjects = function() { var oList = this.getList(); if (oList) { // notice that currently there is only one aggregation return SelectList.prototype.findAggregatedObjects.apply(oList, arguments); } return []; }; ComboBox.prototype.setShowSecondaryValues = function(bAdditionalText) { this.setProperty("showSecondaryValues", bAdditionalText, true); var oList = this.getList(); if (oList) { oList.s