UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,741 lines (1,457 loc) 96.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', './Input', './Tokenizer', './Token', './ToggleButton', './List', './Popover', './GroupHeaderListItem', './library', 'sap/ui/core/EnabledPropagator', 'sap/ui/core/IconPool', 'sap/ui/core/library', 'sap/ui/Device', 'sap/ui/core/Item', 'sap/ui/core/SeparatorItem', 'sap/ui/core/ResizeHandler', './MultiComboBoxRenderer', "sap/ui/dom/containsOrEquals", "sap/ui/events/KeyCodes", "sap/base/util/deepEqual", "sap/base/assert", "sap/base/Log", "sap/ui/thirdparty/jquery", // jQuery Plugin "cursorPos" "sap/ui/dom/jquery/cursorPos", // jQuery Plugin "control" "sap/ui/dom/jquery/control" ], function( InputBase, ComboBoxTextField, ComboBoxBase, Input, Tokenizer, Token, ToggleButton, List, Popover, GroupHeaderListItem, library, EnabledPropagator, IconPool, coreLibrary, Device, Item, SeparatorItem, ResizeHandler, MultiComboBoxRenderer, containsOrEquals, KeyCodes, deepEqual, assert, Log, jQuery ) { "use strict"; // shortcut for sap.m.ListType var ListType = library.ListType; // 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; var PlacementType = library.PlacementType; /** * 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}. * <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.</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.60.39 * * @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} * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel */ 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: [] } }, associations: { /** * Provides getter and setter for the selected items from * the aggregation named items. */ selectedItems: { type: "sap.ui.core.Item", multiple: true, singularName: "selectedItem" } }, 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" }, /** * Selection state: true if item is selected, false if * item is not selected */ selected: { 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[]" } } } } }}); IconPool.insertFontFaceStyle(); EnabledPropagator.apply(MultiComboBox.prototype, [true]); /* ----------------------------------------------------------- */ /* 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) { sap.m.Tokenizer.prototype.onsapend.apply(this._oTokenizer, arguments); }; /** * Handle Home key pressed. Scroll the first token into viewport. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsaphome = function(oEvent) { sap.m.Tokenizer.prototype.onsaphome.apply(this._oTokenizer, arguments); }; /** * 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(); // If list is open then go to the first visible list item. Set this item // into the visual viewport. // If list is closed... var aItems = this.getSelectableItems(); var oItem = aItems[0]; if (oItem && this.isOpen()) { this.getListItem(oItem).focus(); return; } if (this._oTokenizer.getSelectedTokens().length) { return; } this._oTraversalItem = this._getNextTraversalItem(); if (this._oTraversalItem) { this.updateDomValue(this._oTraversalItem.getText()); this.selectText(0, this.getValue().length); } }; /** * Handle UP arrow key pressed. Set focus to input field if first list item has focus. 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(); if (this._oTokenizer.getSelectedTokens().length) { return; } this._oTraversalItem = this._getPreviousTraversalItem(); if (this._oTraversalItem) { this.updateDomValue(this._oTraversalItem.getText()); this.selectText(0, this.getValue().length); } }; /** * Handles the <code>onsapshow</code> event when either F4 is pressed or Alt + Down arrow are pressed. * * @param {jQuery.Event} oEvent The event object */ MultiComboBox.prototype.onsapshow = function(oEvent) { var oList = this.getList(), oPicker = this.getPicker(), aSelectableItems = this.getSelectableItems(), aSelectedItems = this.getSelectedItems(), oItemToFocus, oItemNavigation = oList.getItemNavigation(), iItemToFocus, oCurrentFocusedControl, sValue = this.getValue(); oCurrentFocusedControl = jQuery(document.activeElement).control()[0]; if (oCurrentFocusedControl instanceof sap.m.Token) { oItemToFocus = this._getItemByToken(oCurrentFocusedControl); } else if (sValue) { oItemToFocus = this._getItemByValue(sValue); } if (!oItemToFocus) { // we need to take the list's first selected item not the first selected item by the combobox user oItemToFocus = aSelectedItems.length ? this._getItemByListItem(this.getList().getSelectedItems()[0]) : aSelectableItems[0]; } iItemToFocus = this.getItems().indexOf(oItemToFocus); if (oItemNavigation) { oItemNavigation.setSelectedIndex(iItemToFocus); } else { this._bListItemNavigationInvalidated = true; this._iInitialItemFocus = iItemToFocus; } oPicker.setInitialFocus(oList); ComboBoxBase.prototype.onsapshow.apply(this, arguments); }; /** * Handles when Alt + Up arrow are pressed. * * @param {jQuery.Event} oEvent The event object. */ MultiComboBox.prototype.onsaphide = MultiComboBox.prototype.onsapshow; /** * 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, bPickerOpened = this.isOpen(); 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(bPickerOpened ? "" : this.getValue()); for (i = 0; i < aVisibleItems.length; i++) { if (aVisibleItems[i].getText().toUpperCase() === this.getValue().toUpperCase()) { 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 (this.getListItem(oItem).isSelected()) { this.setValue(''); } else { this.setSelection(oParam); } } } else { this._bPreventValueRemove = true; this._showWrongValueVisualEffect(); } if (oEvent) { this.close(); } }; /** * Function calculates the available space for the tokenizer * * @private * @return {String | null} CSSSize in px */ MultiComboBox.prototype._calculateSpaceForTokenizer = function () { if (this.getDomRef()) { var iWidth = this.getDomRef().offsetWidth, iArrowButtonWidth = parseInt(this.getDomRef("arrow").offsetWidth, 10), iInputWidth = parseInt(this.$().find(".sapMInputBaseInner").css("min-width"), 10) || 0, iInputPadding = parseInt(this.$().find(".sapMInputBaseInner").css("padding-right"), 10) || 0; return iWidth - (iArrowButtonWidth + iInputWidth + iInputPadding) + "px"; } else { return null; } }; /** * Handle when enter is pressed. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapenter = function(oEvent) { InputBase.prototype.onsapenter.apply(this, arguments); if (this.getValue()) { this._selectItemByKey(oEvent); } }; /** * 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 === 1) { this._selectItemByKey(oEvent); } else { this._showWrongValueVisualEffect(); } } }; /* =========================================================== */ /* Event handlers */ /* =========================================================== */ /** * Handle the focus leave event. * * @param {jQuery.Event} oEvent The event object * @private */ MultiComboBox.prototype.onsapfocusleave = function(oEvent) { var oPicker = this.getAggregation("picker"), bTablet = this.isPlatformTablet(), oControl = sap.ui.getCore().byId(oEvent.relatedControlId), oFocusDomRef = oControl && oControl.getFocusDomRef(), sOldValue = this.getValue(); // If focus target is outside of picker if (!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 focus is outside of the MultiComboBox if (!(oControl instanceof Token || oEvent.srcControl instanceof Token)) { this._oTokenizer.scrollToEnd(); } // if the focus is outside the MultiComboBox, the tokenizer should be collapsed if (!jQuery.contains(this.getDomRef(), document.activeElement)) { this._oTokenizer._useCollapsedMode(true); } } 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.getFocusDomRef(); var bPickerClosing = oPicker.oPopup.getOpenState() === OpenState.CLOSING; var bDropdownPickerType = this.getPickerType() === "Dropdown"; if (bDropdownPickerType) { bPreviousFocusInDropdown = oPickerDomRef && jQuery.contains(oPickerDomRef, oEvent.relatedTarget); } if (this.getEditable()) { this._oTokenizer._useCollapsedMode(false); this._oTokenizer.scrollToEnd(); } if (oEvent.target === this.getFocusDomRef()) { this.getEditable() && 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. !bPickerClosing && 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(); } }; /** * Handle the browser tap event on the List item. * * @param {sap.ui.base.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleItemTap = function(oEvent) { var oTappedControl = jQuery(oEvent.target).control(0); if (!oTappedControl.isA("sap.m.CheckBox") && !oTappedControl.isA("sap.m.GroupHeaderListItem")) { this._bCheckBoxClicked = false; if (this.isOpen() && !this._isListInSuggestMode()) { this.close(); } } }; /** * Handle the item press event on the List. * * @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) { this.getListItem(oItem).focus(); } } }; /** * Handle the selection change event on the List. * * @param {sap.ui.base.Event} oEvent The event object * @private */ MultiComboBox.prototype._handleSelectionLiveChange = function(oEvent) { var oListItem = oEvent.getParameter("listItem"); var bIsSelected = oEvent.getParameter("selected"); var oNewSelectedItem = this._getItemByListItem(oListItem); var oInputControl = this.isPickerDialog() ? this.getPickerTextField() : 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, id: oNewSelectedItem.getId(), key: oNewSelectedItem.getKey(), 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 (this.isOpen() && this.getPicker().oPopup.getOpenState() !== OpenState.CLOSING) { // workaround: this is needed because the List fires the "selectionChange" event during the popover is closing. // So clicking on list item description the focus should be replaced to input field. Otherwise the focus is set to // oListItem. // Scrolls an item into the visual viewport oListItem.focus(); } } else { this._bCheckBoxClicked = true; this.setValue(""); this.close(); } }; /** * Function is called on key down keyboard input * * @private * @param {jQuery.Event} oEvent The event object */ MultiComboBox.prototype.onkeydown = function(oEvent) { ComboBoxBase.prototype.onkeydown.apply(this, arguments); if (!this.getEnabled() || !this.getEditable()) { 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()) { this._oTokenizer.focus(); this._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 = (oEvent.which !== KeyCodes.BACKSPACE) && (oEvent.which !== KeyCodes.DELETE); }; /** * Handle the input 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; if (!this.getEnabled() || !this.getEditable()) { return; } // 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)); } }; /** * Filters array of items for given value * * @private */ MultiComboBox.prototype.filterItems = function (mOptions) { var fnFilter = this.fnFilter ? this.fnFilter : ComboBoxBase.DEFAULT_TEXT_FILTER; var aFilteredItems = []; var bGrouped = false; var oGroups = []; mOptions.items.forEach(function(oItem) { if (oItem.isA("sap.ui.core.SeparatorItem")) { oGroups.push({ separator: oItem }); this.getListItem(oItem).setVisible(false); bGrouped = true; return; } var bMatch = !!fnFilter(mOptions.value, oItem, "getText"); if (mOptions.value === "") { bMatch = true; if (!this.bOpenedByKeyboardOrButton && !this.isPickerDialog()) { // prevent filtering of the picker if it will be closed return; } } if (bGrouped && bMatch) { this.getListItem(oGroups[oGroups.length - 1].separator).setVisible(true); } var oListItem = this.getListItem(oItem); if (oListItem) { oListItem.setVisible(bMatch); bMatch && aFilteredItems.push(oItem); } }, this); return aFilteredItems; }; /** * Function is called on key up keyboard input * * @private * @param {jQuery.Event} oEvent The event object */ MultiComboBox.prototype.onkeyup = function(oEvent) { 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 sOldValueState = this.getValueState(); if (sOldValueState === ValueState.Error) { return; } if (this.isPickerDialog()) { this.getPickerTextField().setValueState(ValueState.Error); setTimeout(this.getPickerTextField()["setValueState"].bind(this.getPickerTextField(), sOldValueState), 1000); } else { this.setValueState(ValueState.Error); setTimeout(this["setValueState"].bind(this, sOldValueState), 1000); } this._syncInputWidth(this._oTokenizer); }; /** * Returns a modified instance type of <code>sap.m.Popover</code> used in read-only mode. * * @returns {sap.m.Popover} The Popover instance * @private */ MultiComboBox.prototype._getReadOnlyPopover = function() { if (!this._oReadOnlyPopover) { this._oReadOnlyPopover = this._createReadOnlyPopover(); } return this._oReadOnlyPopover; }; /** * Creates an instance type of <code>sap.m.Popover</code> used in read-only mode. * * @returns {sap.m.Popover} The Popover instance * @private */ MultiComboBox.prototype._createReadOnlyPopover = function() { return new Popover({ showArrow: true, placement: PlacementType.Auto, showHeader: false, contentMinWidth: "auto" }).addStyleClass("sapMMultiComboBoxReadOnlyPopover"); }; /** * Creates a picker. To be overwritten by subclasses. * * @param {string} sPickerType The picker type * @returns {sap.m.Popover | sap.m.Dialog} The picker pop-up to be used * @protected * @function */ MultiComboBox.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 pop-up (Popover or Dialog) this.setAggregation("picker", oPicker, true); var oRenderer = this.getRenderer(), CSS_CLASS_MULTICOMBOBOX = oRenderer.CSS_CLASS_MULTICOMBOBOX; // configuration 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) .addContent(this.getList()); return oPicker; }; MultiComboBox.prototype.createPickerTextField = function () { var that = this; return new Input({ // select a list item, when enter is triggered in picker text field submit: function (oEvent) { var sValue = this.getValue(); if (sValue) { that.setValue(sValue); that._selectItemByKey(); this.setValue(that._sOldInput); } } }).addEventDelegate({ // remove the type ahead when focus is not in the input onfocusout: this._handleInputFocusOut }, this); }; MultiComboBox.prototype.onBeforeRendering = function() { ComboBoxBase.prototype.onBeforeRendering.apply(this, arguments); var aItems = this.getItems(); var oList = this.getList(); if (oList) { this._synchronizeSelectedItemAndKey(aItems); // prevent closing of popup on re-rendering oList.destroyItems(); this._clearTokenizer(); this._fillList(aItems); // save focused index, and re-apply after rendering of the list if (oList.getItemNavigation()) { this._iFocusedIndex = oList.getItemNavigation().getFocusedIndex(); } // Re-apply editable state to make sure tokens are rendered in right state. this.setEditable(this.getEditable()); } this._deregisterResizeHandler(); }; /** * 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 () { this._oTokenizer.setMaxWidth(this._calculateSpaceForTokenizer()); this._syncInputWidth(this._oTokenizer); }; /** * This hook method is called before the MultiComboBox's Pop-up is rendered. * * @protected */ MultiComboBox.prototype.onBeforeRenderingPicker = function() { var fnOnBeforeRenderingPopupType = this["_onBeforeRendering" + this.getPickerType()]; if (fnOnBeforeRenderingPopupType) { fnOnBeforeRenderingPopupType.call(this); } }; /** * This hook method is called after the MultiComboBox's Pop-up is rendered. * * @protected */ MultiComboBox.prototype.onAfterRenderingPicker = function() { var fnOnAfterRenderingPopupType = this["_onAfterRendering" + this.getPickerType()]; if (fnOnAfterRenderingPopupType) { fnOnAfterRenderingPopupType.call(this); } }; /** * This event handler will be called before the MultiComboBox Popup is opened. * * @private */ MultiComboBox.prototype.onBeforeOpen = function() { var fnPickerTypeBeforeOpen = this["_onBeforeOpen" + this.getPickerType()]; // add the active state to the MultiComboBox's field this.addStyleClass(InputBase.ICON_PRESSED_CSS_CLASS); this._resetCurrentItem(); this.addContent(); this._aInitiallySelectedItems = this.getSelectedItems(); if (fnPickerTypeBeforeOpen) { fnPickerTypeBeforeOpen.call(this); } }; /** * This event handler will be called after the MultiComboBox's Pop-up is opened. * * @private */ MultiComboBox.prototype.onAfterOpen = function() { var oDomRef = this.getFocusDomRef(); oDomRef && oDomRef.setAttribute("aria-expanded", "true"); // reset the initial focus back to the input if (!this.isPlatformTablet()) { this.getPicker().setInitialFocus(this); } // 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 bUseCollapsed = !jQuery.contains(this.getDomRef(), document.activeElement) || this.isPickerDialog(), oDomRef = this.getFocusDomRef(); oDomRef && oDomRef.setAttribute("aria-expanded", "false"); // remove the active state of the MultiComboBox's field this.removeStyleClass(InputBase.ICON_PRESSED_CSS_CLASS); // 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(""); this._sOldValue = ""; if (this.isPickerDialog()) { this.getPickerTextField().setValue(""); this._getFilterSelectedButton() && this._getFilterSelectedButton().setPressed(false); } this.fireSelectionFinish({ selectedItems: this.getSelectedItems() }); this._oTokenizer._useCollapsedMode(bUseCollapsed); }; /** * 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. * */ 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); } }; /** * Decorate a Popover instance by adding some private methods. * * @param {sap.m.Popover} oPopover The popover to be decorated * @private */ MultiComboBox.prototype._decoratePopover = function(oPopover) { var that = this; oPopover.open = function() { return this.openBy(that); }; }; /** * Creates an instance type of <code>sap.m.Popover</code>. * * @returns {sap.m.Popover} The Popover instance * @private */ MultiComboBox.prototype.createDropdown = function() { var oDropdown = new Popover(this.getDropdownSettings()); oDropdown.setInitialFocus(this); this._decoratePopover(oDropdown); return oDropdown; }; MultiComboBox.prototype.createDialog = function () { var oDialog = ComboBoxBase.prototype.createDialog.apply(this, arguments), oSelectAllButton = this._createFilterSelectedButton(); oDialog.getSubHeader().addContent(oSelectAllButton); return oDialog; }; /** * Creates an instance of <code>sap.m.ToggleButton</code>. * * @returns {sap.m.ToggleButton} The Button instance * @private */ MultiComboBox.prototype._createFilterSelectedButton = function () { var sIconURI = IconPool.getIconURI("multiselect-all"), oRenderer = this.getRenderer(), that = this; return new ToggleButton({ icon: sIconURI, press: that._filterSelectedItems.bind(this) }).addStyleClass(oRenderer.CSS_CLASS_MULTICOMBOBOX + "ToggleButton"); }; MultiComboBox.prototype._getFilterSelectedButton = function () { return this.getPicker().getSubHeader().getContent()[1]; }; /** * Filters visible selected items * @param {jQuery.Event} oEvent The event object * @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 = this.getVisibleItems(), aItems = this.getItems(), aSelectedItems = this.getSelectedItems(), oLastGroupListItem = null; if (bShowSelectedOnly) { aVisibleItems.forEach(function(oItem) { bMatch = aSelectedItems.indexOf(oItem) > -1 ? true : false; oListItem = this.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 { this.filterItems({ value: sValue, items: aItems }); } }; MultiComboBox.prototype.revertSelection = function () { this.setSelectedItems(this._aInitiallySelectedItems); }; /** * Create an instance type of <code>sap.m.List</code>. * * @protected */ MultiComboBox.prototype.createList = function() { var oRenderer = this.getRenderer(); // list to use inside the picker this._oList = new List({ width: "100%", mode: ListMode.MultiSelect, includeItemInSelection: true, rememberSelections: false }).addStyleClass(oRenderer.CSS_CLASS_COMBOBOXBASE + "List") .addStyleClass(oRenderer.CSS_CLASS_MULTICOMBOBOX + "List") .attachBrowserEvent("tap", this._handleItemTap, this) .attachSelectionChange(this._handleSelectionLiveChange, this) .attachItemPress(this._handleItemPress, this); this._oList.addEventDelegate({ onAfterRendering: this.onAfterRenderingList, onfocusin: this.onFocusinList }, this); }; /** * Update and synchronize "selectedItems" association and the "selectedItems" in the List. * * @param {object} mOptions Options object * @param {sap.ui.core.Item | null} mOptions.item The item instance * @param {string} mOptions.id The item ID * @param {string} mOptions.key The item key * @param {boolean} [mOptions.suppressInvalidate] Whether invalidation should be suppressed * @param {boolean} [mOptions.listItemUpdated] Whether the item list is updated * @param {boolean} [mOptions.fireChangeEvent] Whether the change event is fired * @private */ MultiComboBox.prototype.setSelection = function(mOptions) { if (mOptions.item && this.isItemSelected(mOptions.item)) { return; } if (!mOptions.item) { return; } if (!mOptions.listItemUpdated && this.getListItem(mOptions.item)) { // set the selected item in the List this.getList().setSelectedItem(this.getListItem(mOptions.item), true); } // Fill Tokenizer var oToken = new sap.m.Token({ key: mOptions.key }); oToken.setText(mOptions.item.getText()); oToken.setTooltip(mOptions.item.getText()); mOptions.item.data(this.getRenderer().CSS_CLASS_COMBOBOXBASE + "Token", oToken); this._oTokenizer.addToken(oToken); this.$().toggleClass("sapMMultiComboBoxHasToken", this._hasTokens()); this.setValue(''); this.addAssociation("selectedItems", mOptions.item, mOptions.suppressInvalidate); var aSelectedKeys = this.getKeys(this.getSelectedItems()); this.setProperty("selectedKeys", aSelectedKeys, mOptions.suppressInvalidate); if (mOptions.fireChangeEvent) { this.fireSelectionChange({ changedItem: mOptions.item, selected: true }); } if (mOptions.fireFinishEvent) { // Fire selectionFinish also if tokens are deleted directly in input field if (!this.isOpen()) { this.fireSelectionFinish({ selectedItems: this.getSelectedItems() }); } } }; /** * Remove an item from "selectedItems" association and the "selectedItems" in the List. * * @param {object} mOptions Options object * @param {sap.ui.core.Item | null} mOptions.item The item instance * @param {string} mOptions.id The item ID * @param {string} mOptions.key The item key * @param {boolean} [mOptions.suppressInvalidate] Whether invalidation should be suppressed * @param {boolean} [mOptions.listItemUpdated] Whether the item list is updated * @param {boolean} [mOptions.fireChangeEvent] Whether the change event is fired * @private */ MultiComboBox.prototype.removeSelection = function(mOptions) { if (mOptions.item && !this.isItemSelected(mOptions.item)) { return; } if (!mOptions.item) { return; } this.removeAssociation("selectedItems", mOptions.item, mOptions.suppressInvalidate); var aSelectedKeys = this.getKeys(this.getSelectedItems()); this.setProperty("selectedKeys", aSelectedKeys, mOptions.suppressInvalidate); if (!mOptions.listItemUpdated && this.getListItem(mOptions.item)) { // set the selected item in the List this.getList().setSelectedItem(this.getListItem(mOptions.item), false); } // Synch the Tokenizer if (!mOptions.tokenUpdated) { var oToken = this._getTokenByItem(mOptions.item); mOptions.item.data(this.getRenderer().CSS_CLASS_COMBOBOXBASE + "Token", null); this._oTokenizer.removeToken(oToken); } this.$().toggleClass("sapMMultiComboBoxHasToken", this._hasTokens()); if (mOptions.fireChangeEvent) { this.fireSelectionChange({ changedItem: mOptions.item, selected: false }); } if (mOptions.fireFinishEvent) { // Fire selectionFinish also if tokens are deleted directly in input field if (!this.isOpen()) { this.fireSelectionFinish({ selectedItems: this.getSelectedItems() }); } } }; /** * Synchronize selected item and key. * * @param {array} [aItems] The items array * @private */ MultiComboBox.prototype._synchronizeSelectedItemAndKey = function(aItems) { // no items if (!aItems.length) { Log.info("Info: _synchronizeSelectedItemAndKey() the MultiComboBox control does not contain any item on ", this); return; } var aSelectedKeys = this.getSelectedKeys() || this._aCustomerKeys; var aKeyOfSelectedItems = this.getKeys(this.getSelectedItems()); // the "selectedKey" property is not synchronized if (aSelectedKeys.length) { for ( var i = 0, sKey = null, oItem = null, iIndex = null, iLength = aSelectedKeys.length; i < iLength; i++) { sKey = aSelectedKeys[i]; if (aKeyOfSelectedItems.indexOf(sKey) > -1) { if (this._aCustomerKeys.length && (iIndex = this._aCustomerKeys.indexOf(sKey)) > -1) { this._aCustomerKeys.splice(iIndex, 1); } continue; } oItem = this.getItemByKey("" + sKey); // if the "selectedKey" has no corresponding aggregated item, no // changes will apply if (oItem) { if (this._aCustomerKeys.length && (iIndex = this._aCustomerKeys.indexOf(sKey)) > -1) { this._aCustomerKeys.splice(iIndex, 1); } this.setSelection({ item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: false, suppressInvalidate: true, listItemUpdated: false }); } } return; } }; // --------------------------- End ------------------------------------ /** * Get token instance for a specific item * * @param {sap.ui.core.Item} oItem The item in question * @returns {sap.m.Token | null} Token instance, null if not found * @private */ MultiComboBox.prototype._getTokenByItem = function(oItem) { return oItem ? oItem.data(this.getRenderer().CSS_CLASS_COMBOBOXBASE + "Token") : null; }; MultiComboBox.prototype.updateItems = function (sReason) { var bKeyItemSync, aItems, // Get selected keys should be requested at that point as it // depends on getSelectedItems()- calls it internally aKeys = this.getSelectedKeys(); var oUpdateItems = ComboBoxBase.prototype.updateItems.apply(this, arguments); // It's important to request the selected items after the update, // because the sync breaks there. aItems = this.getSelectedItems(); // Check if selected keys and selected items are in sync bKeyItemSync = (aItems.length === aKeys.length) && aItems.every(function (oItem) { return oItem && oItem.getKey && aKeys.indexOf(oItem.getKey()) > -1; }); // Synchronize if sync has been broken by the update if (!bKeyItemSync) { aItems = aKeys.map(this.getItemByKey, this); this.setSelectedItems(aItems); } return oUpdateItems; }; /** * Get selected items from "aItems". * * @param {array | null} aItems Array of sap.ui.core.Item * @returns {array} The array of selected items * @private */ MultiComboBox.prototype._getSelectedItemsOf = function(aItems) { for ( var i = 0, iLength = aItems.length, aSelectedItems = []; i < iLength; i++) { if (this.getListItem(aItems[i]).isSelected()) { aSelectedItems.push(aItems[i]); } } return aSelectedItems; }; /** * Get the last selected item * @returns {sap.ui.core.Item} The selected item * @private */ MultiComboBox.prototype._getLastSelectedItem = function() { var aTokens = this._oTokenizer.getTokens(); var oToken = aTokens.length ? aTokens[aTokens.length - 1] : null; if (!oToken) { return null; } return this._getItemByToken(oToken); }; /** * Get the selected items ordered * @returns {sap.ui.core.Item[]} The ordered list of selected items * @private */ MultiComboBox.prototype._getOrderedSelectedItems = function() { var aItems = []; for (var i = 0, aTokens = this._oTokenizer.getTokens(), iLength = aTokens.length; i < iLength; i++) { aItems[i] = this._getItemByToken(aTokens[i]); } return aItems; }; /** * Get the focused item from list * @returns {sap.ui.core.Item} The focused item in the list * @private */ MultiComboBox.prototype._getFocusedListItem = function() { if (!document.activeElement) { return null; } var oFocusedElement = sap.ui.getCore().byId(document.activeElement.id); if (this.getList() && containsOrEquals(this.getList().getFocusDomRef(), oFocusedElement.getFocusDomRef())) { return oFocusedElement; } return null; }; /** * Get the focused item * @returns {sap.ui.core.Item} The focused item * @private */ MultiComboBox.prototype._getFocusedItem = function() { var oListItem = this._getFocusedListItem(); return this._getItemByListItem(oListItem); }; /** * Tests if an item is in a selected range * @param {sap.ui.core.Item} oListItem The item * @returns {boolean} True if the item is in the selected range * @private */ MultiComboBox.prototype._isRangeSelectionSet = function(oListItem) { var $ListItem = oListItem.getDomRef(); return $ListItem.indexOf(this.getRenderer().CSS_CLASS_MULTICOMBOBOX + "ItemRangeSelection") > -1 ? true : false; }; /** * Tests if there are tokens in the combo box * @returns {boolean} True if there are tokens * @private */ MultiComboBox.prototype._hasTokens = function() { return this._oTokenizer.getTokens().length > 0; }; /** * Gets the current item * @returns {sap.ui.core.Item} The current item * @private */ MultiComboBox.prototype._getCurrentItem = function() { if (!this._oCurrentItem) { return this._getFocusedItem(); } return this._oCurrentItem; }; /** * Sets the current item * @param {sap.ui.core.Item} oItem The item to be set * @private */ MultiComboBox.prototype._setCurrentItem = function(oItem) { this._oCurrentItem = oItem; }; /** * Resets the current item * @private */ MultiComboBox.prototype._resetCurrentItem = function() { this._oCurrentItem = null; }; /** * Decorate a ListItem instance by adding some delegate methods. * * @param {sap.m.StandardListItem} oListItem The item to be decorated * @private */ MultiComboBox.prototype._decorateListItem = function(oListItem) { oListItem.addDelegate({ onkeyup: function(oEvent) { var oItem = null; // If an item is selected with SPACE inside of // suggest list the list // with all entries should be opened if (oEvent.which == KeyCodes.SPACE && this.isOpen() && this._isListInSuggestMode()) { this.open(); oItem = this._getLastSelectedItem(); // Scrolls an item into the visual viewport if (oItem) { this.getListItem(oItem).focus(); } return; } }, onkeydown: function(oEvent) { var oItem = null, oItemCurrent = null; if (oEvent.shiftKey && oEvent.which == KeyCodes.ARROW_DOWN) { oItemCurrent = this._getCurrentItem(); oItem = this._getNextVisibleItemOf(oItemCurrent); } if (oEvent.shiftKey && oEvent.which == KeyCodes.ARROW_UP) { oItemCurrent = this._getCurrentItem(); oItem = this._getPreviousVisibleItemOf(oItemCurrent); } if (oEvent.shiftKey && oEvent.which === KeyCodes.SPACE) { oItemCurrent = this._getCurrentItem(); this._selectPreviousItemsOf(oItemCurrent); } if (oItem && oItem !== oItemCurrent) { if (this.getListItem(oItemCurrent).isSelected()) { this.setSelection({ item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: true, suppressInvalidate: true }); this._setCurrentItem(oItem); } else { this.removeSelection({ item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: true, suppressInvalidate: true }); this._setCurrentItem(oItem); } return; } this._resetCurrentItem(); // Handle when CTRL + A is pressed to select all // Note: at first this function should be called and // not the // ListItemBase if ((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which == KeyCodes.A) { oEvent.setMarked(); oEvent.preventDefault(); var aVisibleItems = this.getSelectableItems(); var aSelectedItems = this._getSelectedItemsOf(aVisibleItems); if (aSelectedItems.length !== aVisibleItems.length) { aVisibleItems.forEach(function(oItem) { this.setSelection({ item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: true, suppressInvalidate: true, listItemUpdated: false }); }, this); } else { aVisibleItems.forEach(function(oItem) { this.removeSelection({ item: oItem, id: oItem.getId(), key: oItem.getKey(), fireChangeEvent: true, suppressInvalidate: true, listItemUpdated: false }); }, this); } } } }, true, this); oListItem.addEventDelegate({ onsapbackspace: function(oEvent) { // Prevent the backspace key from navigating back oEvent.preventDefault(); }, onsapshow: function(oEvent) { // Handle when F4 or Alt + DOWN arrow are pressed. oEvent.setMarked(); if (this.isOpen()) { this.close(); return; } if (this.hasContent()) { this.open(); } }, onsaphide: function(oEvent) { // Handle when Alt + UP arrow are pressed. this.onsapshow(oEvent); }, onsapenter: function(oEvent) { // Handle when enter is pressed. oEvent.setMarked(); this.close(); }, onsaphome: function(oEvent) { // Handle when Pos1 is pressed. oEvent.setMarked(); // note: prevent document scrolling when Home key is pressed oEvent.preventDefault(); var aVisibleItems = this.getSelectableItems(); var oItem = aVisibleItems[0]; // Scrolls an item into the visual viewport this.getListItem(oItem).focus(); }, onsapend: function(oEvent) { // Handle when End is pressed. oEvent.setMarked(); // note: prevent document scrolling when End key is pressed oEvent.preventDefault(); var aVisibleItems = this.getSelectableItems(); var oItem = aVisibleItems[aVisibleItems.length - 1]; // Scrolls an item into the visual viewport this.getListItem(oItem).focus(); }, onsapup: function(oEvent) { // Handle when key UP is pressed. oEvent.setMarked(); // note: prevent document scrolling when arrow keys are pressed oEvent.preventDefault(); var aVisibleItems = this.getSelectableItems(); var oItemFirst = aVisibleItems[0]; var oItemCurrent = jQuery(document.activeElement).control()[0]; if (oItemCurrent === this.getListItem(oItemFirst)) { this.focus(); // Stop the propagation of event. Otherwise the l