UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,616 lines (1,371 loc) 116 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ './InputBase', './ComboBoxBase', './Tokenizer', './Token', './Popover', './CheckBox', './Toolbar', './library', 'sap/ui/core/Element', 'sap/ui/core/EnabledPropagator', 'sap/ui/core/IconPool', 'sap/ui/core/library', 'sap/ui/Device', 'sap/ui/core/Item', 'sap/ui/core/ResizeHandler', './MultiComboBoxRenderer', "sap/ui/dom/containsOrEquals", "sap/m/inputUtils/completeTextSelected", "sap/m/inputUtils/inputsDefaultFilter", "sap/m/inputUtils/typeAhead", "sap/m/inputUtils/ListHelpers", "sap/m/inputUtils/filterItems", "sap/m/inputUtils/itemsVisibilityHandler", "sap/m/inputUtils/forwardItemPropertiesToToken", "sap/m/inputUtils/getTokenByItem", "sap/ui/events/KeyCodes", "sap/base/util/deepEqual", "sap/base/assert", "sap/base/Log", "sap/ui/core/Core", 'sap/ui/core/InvisibleText', "sap/ui/thirdparty/jquery", // jQuery Plugin "cursorPos" "sap/ui/dom/jquery/cursorPos" ], function( InputBase, ComboBoxBase, Tokenizer, Token, Popover, CheckBox, Toolbar, library, Element, EnabledPropagator, IconPool, coreLibrary, Device, Item, ResizeHandler, MultiComboBoxRenderer, containsOrEquals, completeTextSelected, inputsDefaultFilter, typeAhead, ListHelpers, filterItems, itemsVisibilityHandler, forwardItemPropertiesToToken, getTokenByItem, KeyCodes, deepEqual, assert, Log, core, InvisibleText, jQuery ) { "use strict"; // shortcut for sap.m.ListMode var ListMode = library.ListMode; // shortcut for sap.ui.core.ValueState var ValueState = coreLibrary.ValueState; // shortcut for sap.ui.core.OpenState var OpenState = coreLibrary.OpenState; // shortcut for sap.m.TokenizerRenderMode var TokenizerRenderMode = library.TokenizerRenderMode; /** * Constructor for a new MultiComboBox. * * @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 MultiComboBox control provides a list box with items and a text field allowing the user to either type a value directly into the control or choose from the list of existing items. * * A drop-down list for selecting and filtering values. * <h3>Overview</h3> * The MultiComboBox control is commonly used to enable users to select one or more options from a predefined list. The control provides an editable input field to filter the list, and a dropdown arrow of available options. * The select options in the list have checkboxes that permit multi-selection. Entered values are displayed as {@link sap.m.Token tokens}. * * When an invalid character is typed into the text field of the MultiComboBox control, the value state is changed to <code>sap.ui.core.ValueState.Error</code> only for a second, as the invalid value is immediately deleted from the input field. * <h3>Structure</h3> * The MultiComboBox consists of the following elements: * <ul> * <li> Input field - displays the selected option/s as token/s. Users can type to filter the list. * <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> * <h3>Usage</h3> * <h4>When to use:</h4> * <ul> * <li>The user needs to select one or more options from a long list of options (maximum of approximately 200).</li> * </ul> * <h4>When not to use:</h4> * <ul> * <li>The user needs to choose between two options such as ON or OFF and YES or NO. In this case, consider using a {@link sap.m.Switch switch} control instead</li> * <li>You need to display more that one attribute. In this case, consider using the {@link sap.m.SelectDialog select dialog} or value help dialog instead.</li> * <li>The user needs to search on multiple attributes. In this case, consider using the {@link sap.m.SelectDialog select dialog} or value help dialog instead.</li> * <li>Your use case requires all available options to be displayed right away, without any user interaction. In this case, consider using the {@link sap.m.CheckBox checkboxes} instead.</li> * </ul> * <h3>Responsive Behavior</h3> * If there are many tokens, the control shows only the last selected tokens that fit and for the others a label N-more is provided. * In case the length of the last selected token is exceeding the width of the control, only a label N-Items is shown. In both cases, pressing on the label will show the tokens in a popup. * <u>On Phones:</u> * <ul> * <li>A new full-screen dialog opens where all items from the option list are shown.</li> * <li>You can select and deselect items from the option list.</li> * <li>With the help of a toggle button you can switch between showing all tokens and only selected ones.</li> * <li>You can filter the option list by entering a value in the input.</li> * </ul> * <u>On Tablets:</u> * <ul> * <li>The auto-complete suggestions appear below or above the input field.</li> * <li>You can review the tokens by swiping them to left or right.</li> * </ul> * <u>On Desktop:</u> * <ul> * <li>The auto-complete suggestions appear below or above the input field.</li> * <li>You can review the tokens by pressing the right or left arrows on the keyboard.</li> * <li>You can select single tokens or a range of tokens and you can copy/cut/delete them.</li> * </ul> * * @author SAP SE * @version 1.117.4 * * @constructor * @extends sap.m.ComboBoxBase * @public * @since 1.22.0 * @alias sap.m.MultiComboBox * @see {@link fiori:https://experience.sap.com/fiori-design-web/multi-combobox/ Multi-Combo Box} */ var MultiComboBox = ComboBoxBase.extend("sap.m.MultiComboBox", /** @lends sap.m.MultiComboBox.prototype */ { metadata: { library: "sap.m", designtime: "sap/m/designtime/MultiComboBox.designtime", properties: { /** * Keys of the selected items. If the key has no corresponding item, no changes will apply. If duplicate keys exists the first item matching the key is used. */ selectedKeys: { type: "string[]", group: "Data", defaultValue: [] }, /** * Defines if there are selected items or not. */ hasSelection: { type: "boolean", visibility: "hidden", defaultValue: false }, /** * Determines if the select all checkbox is visible on top of suggestions. */ showSelectAll: { type: "boolean", defaultValue: false } }, associations: { /** * Provides getter and setter for the selected items from * the aggregation named items. */ selectedItems: { type: "sap.ui.core.Item", multiple: true, singularName: "selectedItem" } }, aggregations: { /** * The tokenizer which displays the tokens */ tokenizer: {type: "sap.m.Tokenizer", multiple: false, visibility: "hidden"} }, events: { /** * Event is fired when selection of an item is changed. * Note: please do not use the "change" event inherited from sap.m.InputBase */ selectionChange: { parameters: { /** * Item which selection is changed */ changedItem: { type: "sap.ui.core.Item" }, /** * Array of items whose selection has changed. */ changedItems : {type : "sap.ui.core.Item[]"}, /** * Selection state: true if item is selected, false if * item is not selected */ selected: { type: "boolean" }, /** * Indicates whether the select all action is triggered or not. */ selectAll : {type : "boolean"} } }, /** * Event is fired when user has finished a selection of items in a list box and list box has been closed. */ selectionFinish: { parameters: { /** * The selected items which are selected after list box has been closed. */ selectedItems: { type: "sap.ui.core.Item[]" } } } }, dnd: { draggable: false, droppable: true } }, renderer: MultiComboBoxRenderer }); IconPool.insertFontFaceStyle(); EnabledPropagator.apply(MultiComboBox.prototype, [true]); /** * Clones the <code>sap.m.MultiComboBox</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.MultiComboBox</code> control. * @public */ MultiComboBox.prototype.clone = function (sIdSuffix) { var oComboBoxClone = ComboBoxBase.prototype.clone.apply(this, arguments), oList = this._getList(); if (oList) { oComboBoxClone.syncPickerContent(); } return oComboBoxClone; }; /** * Opens the control's picker popup. * * @returns {this} <code>this</code> to allow method chaining. * @protected */ MultiComboBox.prototype.open = function () { if (!this.isOpen()) { this._bPickerIsOpening = true; } this.syncPickerContent(); return ComboBoxBase.prototype.open.apply(this, arguments); }; /* ----------------------------------------------------------- */ /* Keyboard handling */ /* ----------------------------------------------------------- */ /** * Handle End key pressed. Scroll the last token into viewport. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapend = function(oEvent) { if (oEvent.isMarked("forwardFocusToParent")) { this.focus(); } }; /** * Handle Home key pressed. Scroll the first token into viewport. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsaphome = function(oEvent) { // if the caret is already moved to the start of the input text // execute tokenizer's onsaphome handler if (!this.getFocusDomRef().selectionStart && this._hasTokens()) { Tokenizer.prototype.onsaphome.apply(this.getAggregation("tokenizer"), arguments); } oEvent.setMarked(); }; /** * Handle DOWN arrow key pressed. Set focus to the first list item if the list is open. Otherwise show in input field * the description of the next traversal item. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapdown = function(oEvent) { if (!this.getEnabled() || !this.getEditable()) { return; } // mark the event for components that needs to know if the event was handled by this control oEvent.setMarked(); // note: prevent document scrolling when arrow keys are pressed oEvent.preventDefault(); this.syncPickerContent(); if (!this.isOpen()) { this._oTraversalItem = this._getNextTraversalItem(); if (this._oTraversalItem && !this.isFocusInTokenizer() && !this.isComposingCharacter()) { this.updateDomValue(this._oTraversalItem.getText()); this.selectText(0, this.getValue().length); } return; } // wait for the composition and input events to fire properly since the focus of the list item // triggers unwanted extra events when called in while composing setTimeout(this.handleDownEvent.bind(this, oEvent), 0); }; /** * Handles Up arrow key pressed. Set the focus to the input field if there are no links in * the value state message and the first list item is selected. Otherwise show in input field * description of the previous traversal item. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapup = function(oEvent) { if (!this.getEnabled() || !this.getEditable()) { return; } // mark the event for components that needs to know if the event was handled // by this control oEvent.setMarked(); // note: prevent document scrolling when arrow keys are pressed oEvent.preventDefault(); this.syncPickerContent(); if (this.isFocusInTokenizer() || this.isOpen()) { return; } this._oTraversalItem = this._getPreviousTraversalItem(); if (this._oTraversalItem) { this.updateDomValue(this._oTraversalItem.getText()); this.selectText(0, this.getValue().length); } }; /** * Handles the Down Arrow press event. * * @param {jquery.Event} oEvent The event object * @private */ MultiComboBox.prototype.handleDownEvent = function (oEvent) { if (!this.isOpen()) { return; } var oSrcControl = oEvent.srcControl, oSrcDomRef = oSrcControl && oSrcControl.getDomRef(), bFocusInInput = containsOrEquals(this.getDomRef(), oSrcDomRef), oValueStateHeader = this.getPicker().getCustomHeader(), oValueStateHeaderDom = oValueStateHeader && oValueStateHeader.getDomRef(); oEvent.setMarked(); // note: Prevent document scrolling when Down key is pressed oEvent.preventDefault(); if (bFocusInInput && this.getValueState() != ValueState.None) { this._handleFormattedTextNav(); return; } if ((bFocusInInput || containsOrEquals(oValueStateHeaderDom, oSrcDomRef)) && this.getShowSelectAll()) { this.focusSelectAll(); return; } this.focusFirstItemInList(); }; /** * Handles the End press event. * * @param {jquery.Event} oEvent The event object * @private */ MultiComboBox.prototype.handleEndEvent = function (oEvent) { oEvent.setMarked(); // Note: Prevent document scrolling when End key is pressed oEvent.preventDefault(); var aVisibleItems = ListHelpers.getVisibleItems(this.getItems()), oListItem = aVisibleItems.length && ListHelpers.getListItem(aVisibleItems[aVisibleItems.length - 1]); oListItem && oListItem.focus(); }; /** * Handles the Home press event. * * @param {jquery.Event} oEvent The event object * @private */ MultiComboBox.prototype.handleHomeEvent = function (oEvent) { oEvent.setMarked(); // note: Prevent document scrolling when Home key is pressed oEvent.preventDefault(); if (this.getValueState() !== ValueState.None) { this._handleFormattedTextNav(); oEvent.stopPropagation(true); return; } if (this.getShowSelectAll()) { this.focusSelectAll(); oEvent.stopPropagation(true); return; } this.focusFirstItemInList(); }; /** * Focuses on the first item in the list of options. * @private */ MultiComboBox.prototype.focusFirstItemInList = function () { var aVisibleItems = ListHelpers.getVisibleItems(this.getItems()), oListItem = aVisibleItems.length && ListHelpers.getListItem(aVisibleItems[0]); oListItem && oListItem.focus(); }; /** * Checks if the focused element is part of the Tokenizer. * @returns {boolean} True if the focus is inside the Tokenizer * @private */ MultiComboBox.prototype.isFocusInTokenizer = function () { return jQuery.contains(this.getAggregation("tokenizer").getFocusDomRef(), document.activeElement); }; /** * 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 */ MultiComboBox.prototype.onsapshow = function(oEvent) { oEvent.preventDefault(); this._handleItemToFocus(); ComboBoxBase.prototype.onsapshow.apply(this, arguments); }; MultiComboBox.prototype._handlePopupOpenAndItemsLoad = function () { // should sync the content before setting the initial focus and opening the picker // the picker opening is handled in the base function this._handleItemToFocus(); ComboBoxBase.prototype._handlePopupOpenAndItemsLoad.apply(this, arguments); }; /** * Generates an event delegate for keyboard navigation for <code>sap.m.FormattedText</code> value state header. * If the focus is on the formatted text value state message: * - pressing the Up arrow key will move the focus to the input, * - pressing the Down arrow key - will select the first selectable item. * * @param {object} oValueStateHeader The value state header. * @param {array} aValueStateLinks The links in <code>sap.m.FormattedText</code> value state message. * @returns {object} Delegate for navigation and focus handling for <code>sap.m.ValueStateHeader</code> containing <code>sap.m.FormattedText</code> message with links. * * @private */ MultiComboBox.prototype._valueStateNavDelegate = function(oValueStateHeader, aValueStateLinks) { return { onsapdown: this.handleDownEvent, onsapup: this.focus, onsapend: this.handleEndEvent, onfocusout: function(oEvent) { // Links should not be tabbable after the focus is moved outside of the value state header oValueStateHeader.removeStyleClass("sapMFocusable"); // Check if the element getting the focus is outside the value state header if (!oValueStateHeader.getDomRef().contains(oEvent.relatedTarget)) { aValueStateLinks.forEach(function(oLink) { oLink.getDomRef().setAttribute("tabindex", "-1"); }); } }, onsapshow: this.close, onsaphide: this.close }; }; /** * Event delegate for the last link in the <code>sap.m.FormattedText</code> value state message. * Closes the picker and the value state popup if tab key is pressed on the last value state message link in the header. * * @private */ MultiComboBox.prototype._closePickerDelegate = { onsaptabnext: function() { this.close(); // Closing with timeout as it is open that way setTimeout(function() { this.closeValueStateMessage(); }.bind(this), 0); } }; /** * Event delegate that Handles the arrow navigation of the links in the value state header * * @private */ MultiComboBox.prototype._formattedTextLinksNav = { onsapup: this.focus, onsapdown: this.handleDownEvent }; /** * Handles the focus and the navigation of the value state header * when <code>sap.m.Link</code> is present in the value state message. * * @private */ MultiComboBox.prototype._handleFormattedTextNav = function() { var oCustomHeader = this.getPicker().getCustomHeader(), aValueStateLinks = this.getValueStateLinks(), oLastValueStateLink = aValueStateLinks ? aValueStateLinks[aValueStateLinks.length - 1] : null, oFirstValueStateLink = aValueStateLinks ? aValueStateLinks[0] : null; if (!oCustomHeader.getDomRef() || oCustomHeader.getDomRef() === document.activeElement) { return; } if (!this.oValueStateNavDelegate) { this.oValueStateNavDelegate = this._valueStateNavDelegate(oCustomHeader, aValueStateLinks); oCustomHeader.addEventDelegate(this.oValueStateNavDelegate, this); } // Make the value state header focusable and focus it oCustomHeader.getDomRef().setAttribute("tabindex", "-1"); oCustomHeader.addStyleClass("sapMFocusable"); oCustomHeader.focus(); // Linka should not be part of the tab chain when the focus is out of the value state header // (on the items list or on the input) and the opposite when the header is focused. aValueStateLinks.forEach(function(oLink) { oLink.getDomRef().setAttribute("tabindex", "0"); oLink.addEventDelegate(this._formattedTextLinksNav, this); }, this); this.oMoveFocusBackToVSHeader = !this.oMoveFocusBackToVSHeader ? { onsaptabprevious: function(oEvent) { oEvent.preventDefault(); oCustomHeader.focus(); oCustomHeader.addStyleClass("sapMFocusable"); } } : this.oMoveFocusBackToVSHeader; oLastValueStateLink && oLastValueStateLink.addEventDelegate(this._closePickerDelegate, this); oFirstValueStateLink && oFirstValueStateLink.addEventDelegate(this.oMoveFocusBackToVSHeader, this); }; /** * Handles when Alt + Up arrow are pressed. * * @param {jQuery.Event} oEvent The event object. * @private */ MultiComboBox.prototype.onsaphide = function (oEvent) { this.onsapshow(oEvent); }; /** * Handles the item selection when user triggers an item selection via key press (TAB, ENTER etc.). * * @param {jQuery.Event} oEvent The key event object * @private */ MultiComboBox.prototype._selectItemByKey = function(oEvent) { var aVisibleItems, oParam, oItem, i, bItemMatched, bKeyIsValid; if (!this.getEnabled() || !this.getEditable()) { return; } // mark the event for components that needs to know if the event was handled // by this control if (oEvent) { oEvent.setMarked(); } aVisibleItems = this._getUnselectedItems(); for (i = 0; i < aVisibleItems.length; i++) { // Empty string should be valid key for sap.ui.core.Item only // as sap.ui.core.SeparatorItem with empty key is used for Grouping // while sap.ui.core.SeparatorItem without key and text is used for horizontal visible separator bKeyIsValid = !(aVisibleItems[i].getKey() === undefined || aVisibleItems[i].getKey() === null) && !aVisibleItems[i].isA("sap.ui.core.SeparatorItem"); if (aVisibleItems[i].getText().toUpperCase() === this.getValue().toUpperCase() && bKeyIsValid) { oItem = aVisibleItems[i]; bItemMatched = true; break; } } if (bItemMatched) { oParam = { item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: true, fireFinishEvent: true, suppressInvalidate: true, listItemUpdated: false }; this._bPreventValueRemove = false; if (this.getValue() === "" || (typeof this.getValue() === "string" && oItem.getText().toLowerCase().startsWith(this.getValue().toLowerCase()))) { if (ListHelpers.getListItem(oItem).isSelected()) { this.setValue(''); } else { this.setSelection(oParam); } } } else { this._bPreventValueRemove = true; } if (oEvent) { this.close(); } }; /** * Handle when enter is pressed. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapenter = function(oEvent) { var oTokenizer = this.getAggregation("tokenizer"); // intentionally skip implementation of ComboTextField.prototype.onsapenter InputBase.prototype.onsapenter.apply(this, arguments); // validate if an item is already selected this._showAlreadySelectedVisualEffect(); if (this.getValue()) { this._selectItemByKey(oEvent); } //Open popover with items if in readonly mode and has Nmore indicator if (!this.getEditable() && oTokenizer.getHiddenTokensCount() && oEvent.target === this.getFocusDomRef()) { oTokenizer._togglePopup(oTokenizer.getTokensPopup()); } }; /** * Handles tab key event. Selects an item according to given input if there is exactly one fitting item available. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsaptabnext = function(oEvent) { var sInputValue = this.getValue(); if (sInputValue) { var aSelectableItems = this._getUnselectedItemsStartingText(sInputValue); if (aSelectableItems.length) { this._selectItemByKey(oEvent); } else { this._showWrongValueVisualEffect(); } } }; MultiComboBox.prototype.onsaptabprevious = MultiComboBox.prototype.onsaptabnext; /* =========================================================== */ /* Event handlers */ /* =========================================================== */ /** * Handle the focus leave event. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapfocusleave = function(oEvent) { var bTablet = this.isPlatformTablet(), oControl = core.byId(oEvent.relatedControlId), oFocusDomRef = oControl && oControl.getFocusDomRef(), sOldValue = this.getValue(), oPicker = this.getPicker(), oTokenizer = this.getAggregation("tokenizer"); // If focus target is outside of picker and the picker is fully opened if (!this._bPickerIsOpening && (!oPicker || !oPicker.getFocusDomRef() || !oFocusDomRef || !jQuery.contains(oPicker.getFocusDomRef(), oFocusDomRef))) { this.setValue(null); // fire change event only if the value of the MCB is not empty if (sOldValue) { this.fireChangeEvent("", { value: sOldValue }); } // if the focus is outside the MultiComboBox, the tokenizer should be collapsed if (!jQuery.contains(this.getDomRef(), document.activeElement)) { oTokenizer.setRenderMode(TokenizerRenderMode.Narrow); } } if (oPicker && oFocusDomRef) { if (deepEqual(oPicker.getFocusDomRef(), oFocusDomRef) && !bTablet && !this.isPickerDialog()) { // force the focus to stay in the MultiComboBox field when scrollbar // is moving this.focus(); } } }; /** * Handle the focus in event. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onfocusin = function(oEvent) { var oPicker = this.getPicker(); var bPreviousFocusInDropdown = false; var oPickerDomRef = oPicker && oPicker.getFocusDomRef(); var sCurrentState = (oPicker && oPicker.oPopup.getOpenState()) || OpenState.CLOSED; var bPickerClosedOrClosing = sCurrentState === OpenState.CLOSING || sCurrentState === OpenState.CLOSED; var bDropdownPickerType = this.getPickerType() === "Dropdown"; var oTokenizer = this.getAggregation("tokenizer"); if (bDropdownPickerType) { bPreviousFocusInDropdown = oPickerDomRef && jQuery.contains(oPickerDomRef, oEvent.relatedTarget); } if (this.getEditable() && oEvent.target === this.getDomRef("inner")) { oTokenizer.setRenderMode(TokenizerRenderMode.Loose); } if (oEvent.target === this.getFocusDomRef()) { oTokenizer.hasOneTruncatedToken() && oTokenizer.setFirstTokenTruncated(false); this.getEnabled() && this.addStyleClass("sapMFocus"); // enable type ahead when switching focus from the dropdown to the input field // we need to check whether the focus has been triggered by the popover's closing or just a manual focusin // isOpen is still true as the closing has not finished yet. !bPickerClosedOrClosing && bPreviousFocusInDropdown && this.handleInputValidation(oEvent, false); } if (oEvent.target === this.getOpenArea() && bDropdownPickerType && !this.isPlatformTablet()) { // 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) // force the focus to stay in the input field this.focus(); } // message popup won't open when the item list is shown if (!this.isOpen() && this.shouldValueStateMessageBeOpened()) { this.openValueStateMessage(); } }; /** * Handles the <code>tap</code> event on the list's items. * * @param {sap.ui.base.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleItemTap = function(oEvent) { var oTappedControl = Element.closestTo(oEvent.target); if (!oTappedControl.isA("sap.m.CheckBox") && !oTappedControl.isA("sap.m.GroupHeaderListItem")) { this._bCheckBoxClicked = false; } }; /** * Handles the <code>press</code> event on the list's items. * * @param {sap.ui.base.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleItemPress = function(oEvent) { // If an item is selected clicking on checkbox inside of suggest list the list with all entries should be opened if (this.isOpen() && this._isListInSuggestMode() && this.getPicker().oPopup.getOpenState() !== OpenState.CLOSING) { this.clearFilter(); var oItem = this._getLastSelectedItem(); // Scrolls an item into the visual viewport if (oItem) { ListHelpers.getListItem(oItem).focus(); } } }; /** * Handles the <code>selectionChange</code> event on the List. * * @param {sap.ui.base.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleSelectionLiveChange = function(oEvent) { if (oEvent.getParameter("selectAll")) { return; } var oListItem = oEvent.getParameter("listItem"); var aListItems = oEvent.getParameter("listItems"); var oListItemToFocus = aListItems && aListItems[aListItems.length - 1] || oListItem; var oInputControl = this.isPickerDialog() ? this.getPickerTextField() : this; var bShouldFocusItem = this._getIsClick() && !!oListItemToFocus; var bIsSelected = oEvent.getParameter("selected"); var oNewSelectedItem = ListHelpers.getItemByListItem(this.getItems(), oListItem); var aNewSelectedItems; if (aListItems && aListItems.length) { aNewSelectedItems = []; aListItems.forEach(function (oNewItem) { if (oNewItem.getType() === "Active") { aNewSelectedItems.push(ListHelpers.getItemByListItem(this.getItems(), oNewItem)); } }, this); } if (oListItem.getType() === "Inactive") { // workaround: this is needed because the List fires the "selectionChange" event on inactive items return; } // pre-assertion assert(oNewSelectedItem, "The corresponding mapped item was not found on " + this); if (!oNewSelectedItem) { return; } var oParam = { item: oNewSelectedItem, items: aNewSelectedItems, id: oNewSelectedItem.getId(), key: oNewSelectedItem.getKey(), selectAll: false, fireChangeEvent: true, suppressInvalidate: true, listItemUpdated: true }; if (bIsSelected) { // update the selected item this.fireChangeEvent(oNewSelectedItem.getText()); this.setSelection(oParam); } else { this.fireChangeEvent(oNewSelectedItem.getText()); this.removeSelection(oParam); } if (this._bCheckBoxClicked) { oInputControl.setValue(this._sOldInput); if (bShouldFocusItem && this.isOpen() && this.getPicker().oPopup.getOpenState() !== OpenState.CLOSING) { oListItemToFocus.focus(); this._setIsClick(false); } } else { this._bCheckBoxClicked = true; this.setValue(""); this.close(); } }; /** * Handles the <code>keydown</code> event when any key is pressed. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onkeydown = function(oEvent) { var bEditable = this.getEditable(), oTokenizer = this.getAggregation("tokenizer"), iTokensCount = oTokenizer.getTokens().length; ComboBoxBase.prototype.onkeydown.apply(this, arguments); if (!this.getEnabled()) { return; } if ((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.I && iTokensCount) { oEvent.preventDefault(); if (bEditable) { this._togglePopover(); } else { this._handleIndicatorPress(); } return; } this._bIsPasteEvent = (oEvent.ctrlKey || oEvent.metaKey) && (oEvent.which === KeyCodes.V); // only if there is no text and tokenizer has some tokens if (this.getValue().length === 0 && (oEvent.ctrlKey || oEvent.metaKey) && (oEvent.which === KeyCodes.A) && this._hasTokens()) { oTokenizer.focus(); oTokenizer.selectAllTokens(true); oEvent.preventDefault(); } // workaround - keyup is not fired on mobile device if (this.isPickerDialog()) { this._sOldValue = this.getPickerTextField().getValue(); this._iOldCursorPos = jQuery(this.getFocusDomRef()).cursorPos(); } this._bDoTypeAhead = !Device.os.android && (oEvent.which !== KeyCodes.BACKSPACE) && (oEvent.which !== KeyCodes.DELETE); }; /** * Handles the <code>input</code> event on the control's input field. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.oninput = function(oEvent) { ComboBoxBase.prototype.oninput.apply(this, arguments); var oInput = oEvent.srcControl, bIsPickerDialog = this.isPickerDialog(), oInputField = bIsPickerDialog ? this.getPickerTextField() : this, sValueState = oInputField.getValueState(); // reset the value state if (sValueState === ValueState.Error && this._bAlreadySelected) { oInputField.setValueState(this._sInitialValueState); oInputField.setValueStateText(this._sInitialValueStateText); this._bAlreadySelected = false; } if (!this.getEnabled() || !this.getEditable()) { return; } this.syncPickerContent(); // suppress invalid value this.handleInputValidation(oEvent, this.isComposingCharacter()); if (this._bIsPasteEvent) { oInput.updateDomValue(this._sOldValue || oEvent.target.value || ""); return; } if (this.isOpen()) { // wait a tick so the setVisible call has replaced the DOM setTimeout(this.highlightList.bind(this, this._sOldInput)); } // if recommendations were shown - add the icon pressed style if (this._getItemsShownWithFilter()) { this.toggleIconPressedStyle(true); } }; /** * Filters array of items for given value. * * @param {object} mOptions Options object * @returns {sap.ui.core.Item[]} Array of filtered items * @private */ MultiComboBox.prototype.filterItems = function (mOptions) { return filterItems(this, mOptions.items, mOptions.value, true, false, this.fnFilter || inputsDefaultFilter); }; /** * Function is called on key up keyboard input * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onkeyup = function(oEvent) { ComboBoxBase.prototype.onkeyup.apply(this, arguments); if (!this.getEnabled() || !this.getEditable()) { return; } this._sOldValue = this.getValue(); this._iOldCursorPos = jQuery(this.getFocusDomRef()).cursorPos(); }; /* ----------------------------------------------------------- */ /* */ /* ----------------------------------------------------------- */ /** * Triggers the value state "Error" for 1s, and resets the state to the previous one. * * @private */ MultiComboBox.prototype._showWrongValueVisualEffect = function() { var oSuggestionsPopover = this._getSuggestionsPopover(); var sInitialValueStateText = this._sInitialValueStateText; var sInitialValueState = this._sInitialValueState; var sInvalidEntry = sInitialValueStateText || this._oRbC.getText("VALUE_STATE_ERROR"); var that = this; if (sInitialValueState === ValueState.Error) { return; } if (oSuggestionsPopover) { oSuggestionsPopover.updateValueState(ValueState.Error, sInvalidEntry, true); setTimeout(oSuggestionsPopover.updateValueState.bind(oSuggestionsPopover, that.getValueState(), sInvalidEntry, true), 1000); } if (!this.isPickerDialog()) { this.setValueState(ValueState.Error); this.setValueStateText(this.getValueStateText() || sInvalidEntry); setTimeout(this["setValueState"].bind(this, sInitialValueState || ValueState.Error), 1000); } this._syncInputWidth(this.getAggregation("tokenizer")); }; /** * Triggers the value state "Error" when the item is already selected and enter is pressed. * * @private */ MultiComboBox.prototype._showAlreadySelectedVisualEffect = function() { var sAlreadySelectedText = this._oRb.getText("VALUE_STATE_ERROR_ALREADY_SELECTED"); if (!this.getValue()) { return; } var bAlreadySelected = !!this.getSelectedItems().filter(function(oItem) { return oItem.getText().toLowerCase() === this.getValue().toLowerCase(); }, this).length; var bNewSelection = this.getItems().filter(function(oItem) { return oItem.getText().toLowerCase() === this.getValue().toLowerCase(); }, this).length; if (bAlreadySelected) { if (!this._bAlreadySelected) { this._sInitialValueState = this.getValueState(); this._sInitialValueStateText = this.getValueStateText(); } this._bAlreadySelected = true; this.setValueStateText(sAlreadySelectedText); this.setValueState("Error"); return; } else if (bNewSelection) { return; } else { this._showWrongValueVisualEffect(); } }; MultiComboBox.prototype._hasShowSelectedButton = function () { return true; }; MultiComboBox.prototype.forwardEventHandlersToSuggPopover = function (oSuggPopover) { ComboBoxBase.prototype.forwardEventHandlersToSuggPopover.apply(this, arguments); oSuggPopover.setShowSelectedPressHandler(this._filterSelectedItems.bind(this)); }; /** * <code>MultiComboBox</code> picker configuration * * @param {sap.m.Popover | sap.m.Dialog} oPicker Picker instance * @protected */ MultiComboBox.prototype.configPicker = function (oPicker) { var oRenderer = this.getRenderer(), CSS_CLASS_MULTICOMBOBOX = oRenderer.CSS_CLASS_MULTICOMBOBOX; oPicker.setHorizontalScrolling(false) .addStyleClass(oRenderer.CSS_CLASS_COMBOBOXBASE + "Picker") .addStyleClass(CSS_CLASS_MULTICOMBOBOX + "Picker") .addStyleClass(CSS_CLASS_MULTICOMBOBOX + "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 internal list and attaches it's event handlers/delegates. * * @param {sap.m.List} oList The list instance to be configured * @private * @function */ MultiComboBox.prototype._configureList = function (oList) { // overwrite the default page size of the list // in order to be consistent with the other inputs // page size is used for pageup and pagedown var iPageSize = 10; if (!oList) { return; } // apply aria role="listbox" to List control oList.applyAriaRole("listbox"); // configure the list oList.setMode(ListMode.MultiSelect); oList.setIncludeItemInSelection(true); oList.setGrowingThreshold(iPageSize); // attach event handlers oList .attachBrowserEvent("tap", this._handleItemTap, this) .attachSelectionChange(this._handleSelectionLiveChange, this) .attachItemPress(this._handleItemPress, this); // attach event delegates oList.addEventDelegate({ onAfterRendering: this.onAfterRenderingList, onfocusin: this.onFocusinList }, this); this.getShowSelectAll() && this.createSelectAllHeaderToolbar(oList); }; /** * Modifies the suggestions dialog input * @param {sap.m.Input} oInput The input * * @returns {sap.m.Input} The modified input control * @private * @ui5-restricted */ MultiComboBox.prototype._decoratePopupInput = function(oInput) { ComboBoxBase.prototype._decoratePopupInput.apply(this, arguments); if (!oInput || !oInput.isA(["sap.m.InputBase"])) { return; } oInput.attachSubmit(function (oEvent) { var sValue = oInput.getValue(); if (sValue) { this.setValue(sValue); this._selectItemByKey(); this.setValue(this._sOldInput); this.close(); } }.bind(this)); oInput.addEventDelegate({ // remove the type ahead when focus is not in the input onfocusout: this._handleInputFocusOut }, this); oInput.attachChange(this._handleInnerInputChange.bind(this)); return oInput; }; /** * Handles dialog's OK button press. * * @private */ MultiComboBox.prototype._handleOkPress = function () { ComboBoxBase.prototype._handleOkPress.apply(this, arguments); if (this.getValue()) { this._selectItemByKey(); } }; /** * Handles the picker input change. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleInnerInputChange = function (oEvent) { if (oEvent.getParameter("value") === "") { this._sOldInput = ""; this.clearFilter(); } }; /** * This hook method is called before the MultiComboBox is rendered. * * @protected */ MultiComboBox.prototype.onBeforeRendering = function() { var bEditable = this.getEditable(); var oTokenizer = this.getAggregation("tokenizer"); var aItems = this.getItems(); ComboBoxBase.prototype.onBeforeRendering.apply(this, arguments); this._bInitialSelectedKeysSettersCompleted = true; oTokenizer.setEnabled(this.getEnabled()); oTokenizer.setEditable(bEditable); this._updatePopoverBasedOnEditMode(bEditable); if (!aItems.length) { this._clearTokenizer(); } if (this._getList()) { this.syncPickerContent(true); } this.toggleSelectAllVisibility(this.getShowSelectAll()); // In case there is an old input, the picker is opened and there are items // we need to return the previous state of the filtering as syncPickerContent // will have removed it. if (this._sOldInput && aItems.length && this.isOpen()) { itemsVisibilityHandler(this.getItems(), this.filterItems({ value: this._sOldInput, items: aItems })); // wait a tick so the setVisible call has replaced the DOM setTimeout(this.highlightList.bind(this, this._sOldInput)); } this._deregisterResizeHandler(); this._synchronizeSelectedItemAndKey(); this.setProperty("hasSelection", !!this.getSelectedItems().length); if (!this._bAlreadySelected) { this._sInitialValueStateText = this.getValueStateText(); } if (this.getValueState() !== ValueState.Error) { this._sInitialValueState = this.getValueState(); } if (this.getShowClearIcon()) { this._getClearIcon().setVisible(this.shouldShowClearIcon()); } else if (this._oClearIcon) { this._getClearIcon().setVisible(false); } }; /** * Creates picker if doesn't exist yet and sync with Control items * * @param {boolean} [bForceListSync] Force MultiComboBox to SuggestionPopover sync * @protected * @returns {sap.m.Dialog|sap.m.Popover} The picker instance */ MultiComboBox.prototype.syncPickerContent = function (bForceListSync) { var aItems, oList; var oPicker = this.getPicker(); var oTokenizer = this.getAggregation("tokenizer"); if (!oPicker) { oPicker = this.createPicker(this.getPickerType()); this._updateSuggestionsPopoverValueState(); bForceListSync = true; } if (bForceListSync) { oList = this._getList(); aItems = this.getEditable() ? this.getItems() : this.getSelectedItems(); this._synchronizeSelectedItemAndKey(); var aTokens = oTokenizer.getTokens(); var iFocusedIndex = aTokens.findIndex(function (oToken) { return document.activeElement === oToken.getDomRef(); }); // prevent closing of popup on re-rendering oList.destroyItems(); this._clearTokenizer(); this._fillList(aItems); this.bShouldRestoreTokenizerFocus = iFocusedIndex > -1; this.iFocusedIndex = iFocusedIndex; // save focused index, and re-apply after rendering of the list if (oList.getItemNavigation()) { this._iFocusedIndex = oList.getItemNavigation().getFocusedIndex(); } } return oPicker; }; /** * Registers resize handler * * @private */ MultiComboBox.prototype._registerResizeHandler = function () { assert(!this._iResizeHandlerId, "Resize handler already registered"); this._iResizeHandlerId = ResizeHandler.register(this, this._onResize.bind(this)); }; /** * Deregisters resize handler * * @private */ MultiComboBox.prototype._deregisterResizeHandler = function () { if (this._iResizeHandlerId) { ResizeHandler.deregister(this._iResizeHandlerId); this._iResizeHandlerId = null; } }; /** * Handler for resizing * * @private */ MultiComboBox.prototype._onResize = function () { var oTokenizer = this.getAggregation("tokenizer"); oTokenizer.setMaxWidth(this._calculateSpaceForTokenizer()); this._syncInputWidth(oTokenizer); this._handleNMoreAccessibility(); }; /** * This hook method is called after the MultiComboBox's Pop-up is rendered. * * @protected */ MultiComboBox.prototype.onAfterRenderingPicker = function() { var fnOnAfterRenderingPopupType = this["_onAfterRendering" + this.getPickerType()]; var iInputWidth = this.getDomRef().getBoundingClientRect().width; var sPopoverMaxWidth = getComputedStyle(this.getDomRef()).getPropertyValue("--sPopoverMaxWidth"); if (fnOnAfterRenderingPopupType) { fnOnAfterRenderingPopupType.call(this); } if (iInputWidth <= parseInt(sPopoverMaxWidth) && !Device.system.phone) { this.getPicker().getDomRef().style.setProperty("max-width", "40rem"); } else { this.getPicker().getDomRef().style.setProperty("max-width", iInputWidth + "px"); } }; /** * This event handler will be called before the MultiComboBox Popup is opened. * * @private */ MultiComboBox.prototype.onBeforeOpen = function() { ComboBoxBase.prototype.onBeforeOpen.apply(this, arguments); var oSuggestionsPopover = this._getSuggestionsPopover(); var fnPickerTypeBeforeOpen = this["_onBeforeOpen" + this.getPickerType()], oDomRef = this.getFocusDomRef(); if (oDomRef) { // expose a parent/child contextual relationship to assistive technologies, // notice that the "aria-controls" attribute is set when the popover opened. oDomRef.setAttribute("aria-controls", this.getPicker().getId()); } // add the active state to the MultiComboBox's field this.addContent(); this._aInitiallySelectedItems = this.getSelectedItems(); this._synchronizeSelectedItemAndKey(); if (fnPickerTypeBeforeOpen) { fnPickerTypeBeforeOpen.call(this); } oSuggestionsPopover.resizePopup(this); }; /** * This event handler will be called after the MultiComboBox's Pop-up is opened. * * @private */ MultiComboBox.prototype.onAfterOpen = function() { var oDomRef = this.getFocusDomRef(), aValueStateLinks = this.getValueStateLinks(); oDomRef && this.getFocusDomRef().setAttribute("aria-expanded", "true"); this._bPickerIsOpening = false; // reset the initial focus back to the input if (!this.isPlatformTablet()) { this.getPicker().setInitialFocus(this); } // If there are links in the value state take the links out of // the tab chain by default. They will be tabbable only if the focus in the value state message aValueStateLinks.forEach(function(oLink) { oLink.addDelegate({ onAfterRendering: function() { if (this.getFocusDomRef()) { this.getFocusDomRef().setAttribute("tabindex", "-1"); } } }, oLink); }); // close error message when the list is open, otherwise the list can be covered by the message this.closeValueStateMessage(); }; /** * This event handler will be called before the MultiComboBox's Pop-up is closed. * */ MultiComboBox.prototype.onBeforeClose = function () { ComboBoxBase.prototype.onBeforeClose.apply(this, arguments); }; /** * This event handler will be called after the MultiComboBox's Pop-up is closed. * * @private */ MultiComboBox.prototype.onAfterClose = function() { var bUseNarrow = !jQuery.contains(this.getDomRef(), document.activeElement) || this.isPickerDialog(), oDomRef = this.getFocusDomRef(); oDomRef && this.getFocusDomRef().setAttribute("aria-expanded", "false"); // remove the active state of the MultiComboBox's field this.toggleIconPressedStyle(false); // Show all items when the list will be opened next time this.clearFilter(); // resets or not the value of the input depending on the event (enter does not clear the value) !this.isComposingCharacter() && !this._bPreventValueRemove && this.setValue(""); // clear old values this._sOldValue = ""; this._sOldInput = ""; // clear the typed in value, since SP does not clean it itself, // if no autocomplete property is present this._getSuggestionsPopover()._sTypedInValue = ""; if (this.isPickerDialog()) { // reset the value state after the dialog is closed this.getPickerTextField().setValue(""); this.getFilterSelectedButton() && this.getFilterSelectedButton().setPressed(false); } this.fireSelectionFinish({ selectedItems: this.getSelectedItems() }); this.getAggregation("tokenizer").setRenderMode(bUseNarrow ? TokenizerRenderMode.Narrow : TokenizerRenderMode.Loose); // show value state message when focus is in the input field if (this.getValueState() == ValueState.Error && document.activeElement === this.getFocusDomRef()) { this.selectText(0, this.getValue().length); } }; /** * Called before the Dialog is opened. * * @private */ MultiComboBox.prototype._onBeforeOpenDialog = function() {}; /** * This event handler will be called before the control's picker popover is opened. * * @private */ MultiComboBox.prototype._onBeforeOpenDropdown = function() { var oPopover = this.getPicker(), oDomRef = this.getDomRef(), sWidth; if (oDomRef && oPopover) { sWidth = (oDomRef.offsetWidth / parseFloat(library.BaseFontSize)) + "rem"; oPopover.setContentMinWidth(sWidth); } }; /** * Gets the filter selected toggle button for the control's picker. * * @returns {sap.m.ToggleButton} The button's instance * @private */ MultiComboBox.prototype.getFilterSelectedButton = function () { return this._getSuggestionsPopover().getFilterSelectedButton(); }; /** * Filters visible selected items * @param {jQuery.Event} oEvent The event object * @param {boolean} bForceShowSelected Should the selected items be shown * @returns {void} * @private */ MultiComboBox.prototype._filterSelectedItems = function (oEvent, bForceShowSelected) { var oSource = oEvent.oSource, oListItem, bMatch, sValue = this.getPickerTextField() ? this.getPickerTextField().getValue() : "", bShowSelectedOnly = (oSource && oSource.getPressed && oSource.getPressed()) || bForceShowSelected, aVisibleItems = ListHelpers.getVisibleItems(this.getItems()), aItems = this.getItems(), aSelectedItems = this.getSelectedItems(), oLastGroupListItem = null; if (bShowSelectedOnly) { aVisibleItems.forEach(function(oItem) { bMatch = aSelectedItems.indexOf(oItem) > -1 ? true : false; oListItem = ListHelpers.getListItem(oItem); if (!oListItem) { return; } if (oListItem.isA("sap.m.GroupHeaderListItem")) { oListItem.setVisible(false); oLastGroupListItem = oListItem; } else { oListItem.setVisible(bMatch); if (bMatch && oLastGroupListItem) { oLastGroupListItem.setVisible(true); } } }, this); } else { itemsVisibilityHandler(this.getItems(), this.filterItems({value: sValue, items: aItems})); } this.manageSelectAllCheckBoxState(); }; /** * Reverts the selection as before opening the picker. * * @private */ MultiComboBox.prototype.revertSelection = function () { this.setSelectedItems(this._aInitiallySelectedItems); }; /** * Update and synchronize "selectedItems" association and the "selectedItems" in the List. * * @param {o