UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,792 lines (1,541 loc) 54 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. */ // Provides control sap.m.MultiInput. sap.ui.define([ './Input', './Tokenizer', './Token', './library', 'sap/ui/core/EnabledPropagator', 'sap/ui/base/ManagedObjectMetadata', 'sap/ui/Device', './Popover', './Button', './ToggleButton', './List', './Title', './Bar', './Toolbar', 'sap/ui/core/ResizeHandler', 'sap/ui/core/IconPool', './MultiInputRenderer', "sap/ui/dom/containsOrEquals", "sap/ui/events/KeyCodes", "sap/ui/thirdparty/jquery", // jQuery Plugin "cursorPos" "sap/ui/dom/jquery/cursorPos", // jQuery Plugin "control" "sap/ui/dom/jquery/control" ], function( Input, Tokenizer, Token, library, EnabledPropagator, ManagedObjectMetadata, Device, Popover, Button, ToggleButton, List, Title, Bar, Toolbar, ResizeHandler, IconPool, MultiInputRenderer, containsOrEquals, KeyCodes, jQuery ) { "use strict"; var PlacementType = library.PlacementType, ListMode = library.ListMode; /** * Constructor for a new MultiInput. * * @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 * <h3>Overview</h3> * A multi-input field allows the user to enter multiple values, which are displayed as {@link sap.m.Token tokens}. * You can enable auto-complete suggestions or value help to help the user choose the correct entry. You can define * validator functions to define what token values are accepted. * <b>Notes:</b> * <ul> * <li> New valid tokens are created, when the user presses Enter, selects a value from the suggestions drop-down, or when the focus leaves the field.</li> * <li> When multiple values are copied and pasted in the field, separate tokens are created for each of them.</li> * <li> When a single value is copied and pasted in the field, it is shown as a text value, as further editing might be required before it is converted into a token.</li> * <li> Provide meaningful labels for all input fields. Do not use the placeholder as a replacement for the label.</li> * </ul> * <h3>Usage</h3> * <h4>When to use:</h4> * <ul> * <li> You need to provide the value help option to help users select or search multiple business objects.</li> * <li> The dataset to choose from is expected to increase over time (for example, to more than 200 values).</li> * </ul> * <h4>When not to use:</h4> * <ul> * <li> When you need to select only one value.</li> * <li> When you want the user to select from a predefined set of options. Use {@link sap.m.MultiComboBox} 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 <i>N-more</i> is provided. * In case the length of the last selected token is exceeding the width of the control, only a label <i>N-Items</i> is shown. * In both cases, pressing on the label will show the tokens in a popup. * <u>On Phones:</u> * <ul> * <li> Only the last entered token is displayed.</li> * <li> A new full-screen dialog opens where the auto-complete suggestions can be selected.</li> * <li> You can review the tokens by tapping on the token or the input field.</li> * </ul> * <u> On Tablets:</u> * <ul> * <li> The auto-complete suggestions appear below or above the multi-input field.</li> * <li> You can review the tokens by swiping them to the left or right.</li> * </ul> * <u>On Desktop:</u> * <ul> * <li> The auto-complete suggestions appear below or above the multi-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> * @extends sap.m.Input * * @author SAP SE * @version 1.60.39 * * @constructor * @public * @alias sap.m.MultiInput * @see {@link fiori:https://experience.sap.com/fiori-design-web/multiinput/ Multi-Input Field} * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel */ var MultiInput = Input.extend("sap.m.MultiInput", /** @lends sap.m.MultiInput.prototype */ { metadata: { library: "sap.m", designtime: "sap/m/designtime/MultiInput.designtime", properties: { /** * If set to true, the MultiInput will be displayed in multi-line display mode. * In multi-line display mode, all tokens can be fully viewed and easily edited in the MultiInput. * The default value is false. * <b>Note:</b> This property does not take effect on smartphones or when the editable property is set to false. * <b>Caution:</b> Do not enable multi-line mode in tables and forms. * @deprecated Since version 1.58. Replaced with N-more/N-items labels, which work in all cases. * @since 1.28 */ enableMultiLineMode: {type: "boolean", group: "Behavior", defaultValue: false}, /** * The max number of tokens that is allowed in MultiInput. * @since 1.36 */ maxTokens: {type: "int", group: "Behavior"} }, aggregations: { /** * The currently displayed tokens */ tokens: {type: "sap.m.Token", multiple: true, singularName: "token"}, /** * The tokenizer which displays the tokens */ tokenizer: {type: "sap.m.Tokenizer", multiple: false, visibility: "hidden"} }, events: { /** * Fired when the tokens aggregation changed (add / remove token) * @deprecated Since version 1.46. * Please use the new event tokenUpdate. */ tokenChange: { parameters: { /** * Type of tokenChange event. * There are four TokenChange types: "added", "removed", "removedAll", "tokensChanged". * Use Tokenizer.TokenChangeType.Added for "added", Tokenizer.TokenChangeType.Removed for "removed", Tokenizer.TokenChangeType.RemovedAll for "removedAll" and Tokenizer.TokenChangeType.TokensChanged for "tokensChanged". */ type: {type: "string"}, /** * The added token or removed token. * This parameter is used when tokenChange type is "added" or "removed". */ token: {type: "sap.m.Token"}, /** * The array of removed tokens. * This parameter is used when tokenChange type is "removedAll". */ tokens: {type: "sap.m.Token[]"}, /** * The array of tokens that are added. * This parameter is used when tokenChange type is "tokenChanged". */ addedTokens: {type: "sap.m.Token[]"}, /** * The array of tokens that are removed. * This parameter is used when tokenChange type is "tokenChanged". */ removedTokens: {type: "sap.m.Token[]"} } }, /** * Fired when the tokens aggregation changed due to a user interaction (add / remove token) */ tokenUpdate: { allowPreventDefault : true, parameters: { /** * Type of tokenChange event. * There are two TokenUpdate types: "added", "removed" * Use Tokenizer.TokenUpdateType.Added for "added" and Tokenizer.TokenUpdateType.Removed for "removed". */ type: {type: "string"}, /** * The array of tokens that are added. * This parameter is used when tokenUpdate type is "added". */ addedTokens: {type: "sap.m.Token[]"}, /** * The array of tokens that are removed. * This parameter is used when tokenUpdate type is "removed". */ removedTokens: {type: "sap.m.Token[]"} } } } } }); EnabledPropagator.apply(MultiInput.prototype, [true]); var oRb = sap.ui.getCore().getLibraryResourceBundle("sap.m"); // ** // * This file defines behavior for the control, // */ MultiInput.prototype.init = function () { this._bShowListWithTokens = false; Input.prototype.init.call(this); this._bIsValidating = false; this._tokenizer = new Tokenizer(); this._tokenizer._setAdjustable(true); this.setAggregation("tokenizer", this._tokenizer); this._tokenizer.attachTokenChange(this._onTokenChange, this); this._tokenizer.attachTokenUpdate(this._onTokenUpdate, this); this._tokenizer._handleNMoreIndicatorPress(this._handleIndicatorPress.bind(this)); this._tokenizer.addEventDelegate({ onThemeChanged: this._handleInnerVisibility.bind(this) }, this); this.setShowValueHelp(true); this.setShowSuggestion(true); this.attachSuggestionItemSelected(this._onSuggestionItemSelected, this); this.attachLiveChange(this._onLiveChange, this); this.attachValueHelpRequest(function () { // Register the click on value help. this._bValueHelpOpen = true; }, this); this._getValueHelpIcon().setProperty("visible", true, true); this._modifySuggestionPicker(); }; /** * Called on control termination * * @private */ MultiInput.prototype.exit = function () { Input.prototype.exit.call(this); if (this._oSelectedItemPicker) { this._oSelectedItemPicker.destroy(); this._oSelectedItemPicker = null; } if (this._getReadOnlyPopover()) { var oReadOnlyPopover = this._getReadOnlyPopover(); oReadOnlyPopover.destroy(); oReadOnlyPopover = null; } this._deregisterResizeHandler(); }; /** * Called after the control is rendered. * * @private */ MultiInput.prototype.onAfterRendering = function () { this._tokenizer.scrollToEnd(); this._registerResizeHandler(); this._tokenizer.setMaxWidth(this._calculateSpaceForTokenizer()); this._handleInnerVisibility(); this._syncInputWidth(this._tokenizer); Input.prototype.onAfterRendering.apply(this, arguments); }; MultiInput.prototype._handleInnerVisibility = function () { if (this._oReadOnlyPopover && !this.getEditable()) { var bHideInnerInput = this._tokenizer._hasMoreIndicator(); this[bHideInnerInput ? "_setValueInvisible" : "_setValueVisible"].call(this); } }; /** * Event handler for user input. * * @private * @param {jQuery.Event} oEvent User input. */ MultiInput.prototype.oninput = function(oEvent) { Input.prototype.oninput.call(this, oEvent); this._manageListsVisibility(false); this._getSelectedItemsPicker().close(); }; /** * Registers resize handler * * @private */ MultiInput.prototype._registerResizeHandler = function () { if (!this._iResizeHandlerId) { this._iResizeHandlerId = ResizeHandler.register(this, this._onResize.bind(this)); } }; /** * Deregisters resize handler * * @private */ MultiInput.prototype._deregisterResizeHandler = function () { if (this._iResizeHandlerId) { ResizeHandler.deregister(this._iResizeHandlerId); this._iResizeHandlerId = null; } }; /** * Handler for resizing * * @private */ MultiInput.prototype._onResize = function () { this._deregisterResizeHandler(); this._tokenizer.setMaxWidth(this._calculateSpaceForTokenizer()); this._handleInnerVisibility(); this._syncInputWidth(this._tokenizer); this._registerResizeHandler(); }; MultiInput.prototype._onTokenChange = function (args) { this.fireTokenChange(args.getParameters()); this.invalidate(); // check if active element is part of MultiInput var bFocusOnMultiInput = containsOrEquals(this.getDomRef(), document.activeElement); if (args.getParameter("type") === "tokensChanged" && args.getParameter("removedTokens").length > 0 && bFocusOnMultiInput) { this.focus(); } if (args.getParameter("type") === "removed") { this._tokenizer._useCollapsedMode(false); this.focus(); } }; MultiInput.prototype._onTokenUpdate = function (args) { var eventResult = this.fireTokenUpdate(args.getParameters()); if (!eventResult) { args.preventDefault(); } else { this.invalidate(); } // on mobile the list with the tokens should be updated and shown if (this._bUseDialog) { this._fillList(); this._manageListsVisibility(true/*show list with tokens*/); } }; MultiInput.prototype._onSuggestionItemSelected = function (eventArgs) { var item = null, token = null, that = this, iOldLength = this._tokenizer.getTokens().length; //length of tokens before validating // Tokenizer is "full" or ValueHelp is open. if (this.getMaxTokens() && iOldLength >= this.getMaxTokens() || this._bValueHelpOpen) { return; } if (this._hasTabularSuggestions()) { item = eventArgs.getParameter("selectedRow"); } else { item = eventArgs.getParameter("selectedItem"); if (item) { token = new Token({ text: item.getText(), key: item.getKey() }); } } if (item) { var text = this.getValue(); this._tokenizer._addValidateToken({ text: text, token: token, suggestionObject: item, validationCallback: function (validated) { if (validated) { that.setValue(""); } } }); } //dialog opens if (this._bUseDialog) { var iNewLength = this._tokenizer.getTokens().length; if (iOldLength < iNewLength) { this.setValue(""); } if (this._getSuggestionsList() instanceof sap.m.Table) { // CSN# 1421140/2014: hide the table for empty/initial results to not show the table columns this._getSuggestionsList().addStyleClass("sapMInputSuggestionTableHidden"); } else { this._getSuggestionsList().destroyItems(); } var oScroll = this._oSuggestionPopup.getScrollDelegate(); if (oScroll) { oScroll.scrollTo(0, 0, 0); } this._oSuggPopover._oPopupInput.focus(); } }; MultiInput.prototype._onLiveChange = function (eventArgs) { this._tokenizer._removeSelectedTokens(); }; /** * Set value in input field invisible. * * @since 1.38 * @private */ MultiInput.prototype._setValueInvisible = function () { this.$("inner").css("opacity", "0"); }; /** * Show value in input field * * @since 1.38 * @private */ MultiInput.prototype._setValueVisible = function () { this.$("inner").css("opacity", "1"); }; /** * Function calculates the available space for the tokenizer * * @private * @return {String | null} CSSSize in px */ MultiInput.prototype._calculateSpaceForTokenizer = function () { if (this.getDomRef()) { var iWidth = this.getDomRef().offsetWidth, iValueHelpButtonWidth = this.getDomRef("vhi") ? parseInt(this.getDomRef("vhi").offsetWidth, 10) : 0, iInputWidth = parseInt(this.$().find(".sapMInputBaseInner").css("min-width"), 10) || 0; return iWidth - (iValueHelpButtonWidth + iInputWidth) + "px"; } else { return null; } }; /** * Setter for property <code>enableMultiLineMode</code>. * @param {boolean} bMultiLineMode Property value * @returns {sap.m.MultiInput} Pointer to the control instance for chaining * @since 1.28 * @public * @deprecated Since version 1.58. */ MultiInput.prototype.setEnableMultiLineMode = function (bMultiLineMode) { // the multiline functionality is deprecated // the method is left for backwards compatibility return this.setProperty("enableMultiLineMode", bMultiLineMode, true); }; MultiInput.prototype.onmousedown = function (e) { if (e.target == this.getDomRef('content')) { e.preventDefault(); e.stopPropagation(); } }; MultiInput.prototype._openMultiLineOnDesktop = function() { // the multiline functionality is deprecated // the method is left for backwards compatibility }; /** * Expand multi-line MultiInput in multi-line mode * * @since 1.28 * @public * @deprecated Since version 1.58. */ MultiInput.prototype.openMultiLine = function () { // the multiline functionality is deprecated // the method is left for backwards compatibility }; /** * Close multi-line MultiInput in multi-line mode * * @since 1.28 * @public * @deprecated Since version 1.58. */ MultiInput.prototype.closeMultiLine = function () { // the multiline functionality is deprecated // the method is left for backwards compatibility }; /** * Returns the sap.ui.core.ScrollEnablement delegate which is used with this control. * @returns {sap.ui.core.ScrollEnablement} The scroll delegate * @private */ MultiInput.prototype.getScrollDelegate = function () { return this._tokenizer._oScroller; }; /** * Called before the control is rendered. * * @private */ MultiInput.prototype.onBeforeRendering = function () { var oTokenizer = this.getAggregation("tokenizer"); if (oTokenizer) { oTokenizer.toggleStyleClass("sapMTokenizerEmpty", oTokenizer.getTokens().length === 0); } Input.prototype.onBeforeRendering.apply(this, arguments); this._deregisterResizeHandler(); }; /** * Function adds a validation callback called before any new token gets added to the tokens aggregation * * @param {function} fValidator The validation callback * @public */ MultiInput.prototype.addValidator = function (fValidator) { this._tokenizer.addValidator(fValidator); }; /** * Function removes a validation callback * * @param {function} fValidator The validation callback to be removed * @public */ MultiInput.prototype.removeValidator = function (fValidator) { this._tokenizer.removeValidator(fValidator); }; /** * Function removes all validation callbacks * * @public */ MultiInput.prototype.removeAllValidators = function () { this._tokenizer.removeAllValidators(); }; /** * Called when the user presses the down arrow key * @param {jQuery.Event} oEvent The event triggered by the user * @private */ MultiInput.prototype.onsapnext = function (oEvent) { if (oEvent.isMarked()) { return; } // find focused element var oFocusedElement = jQuery(document.activeElement).control()[0]; if (!oFocusedElement) { // we cannot rule out that the focused element does not correspond to an SAPUI5 control in which case oFocusedElement // is undefined return; } if (this._tokenizer === oFocusedElement || this._tokenizer.$().find(oFocusedElement.$()).length > 0) { // focus is on the tokenizer or on some descendant of the tokenizer and the event was not handled -> // we therefore handle the event and focus the input element this._scrollAndFocus(); } }; /** * Function is called on keyboard backspace, if cursor is in front of a token, token gets selected and deleted * * @private * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.onsapbackspace = function (oEvent) { if (this.getCursorPosition() > 0 || !this.getEditable() || this.getValue().length > 0) { // deleting characters, not return; } Tokenizer.prototype.onsapbackspace.apply(this._tokenizer, arguments); oEvent.preventDefault(); oEvent.stopPropagation(); }; /** * Function is called on delete keyboard input, deletes selected tokens * * @private * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.onsapdelete = function (oEvent) { if (!this.getEditable()) { return; } if (this.getValue() && !this._completeTextIsSelected()) { // do not return if everything is selected return; } Tokenizer.prototype.onsapdelete.apply(this._tokenizer, arguments); }; /** * Handle the key down event for Ctrl + A * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onkeydown = function (oEvent) { if (oEvent.which === KeyCodes.TAB) { this._tokenizer._changeAllTokensSelection(false); } // ctrl/meta + A - Select all Tokens if ((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.A) { if (this._tokenizer.getTokens().length > 0) { this._tokenizer.focus(); this._tokenizer._changeAllTokensSelection(true); oEvent.preventDefault(); } } // ctrl/meta + c OR ctrl/meta + Insert - Copy all selected Tokens if ((oEvent.ctrlKey || oEvent.metaKey) && (oEvent.which === KeyCodes.C || oEvent.which === KeyCodes.INSERT)) { this._tokenizer._copy(); } // ctr/meta + x OR Shift + Delete - Cut all selected Tokens if editable if (((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.X) || (oEvent.shiftKey && oEvent.which === KeyCodes.DELETE)) { if (this.getEditable()) { this._tokenizer._cut(); } else { this._tokenizer._copy(); } } }; /** * Handle the paste event * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onpaste = function (oEvent) { var sOriginalText, i, aValidTokens = [], aAddedTokens = []; if (this.getValueHelpOnly()) { // BCP: 1670448929 return; } // for the purpose to copy from column in excel and paste in MultiInput/MultiComboBox if (window.clipboardData) { /* TODO remove after 1.62 version */ //IE sOriginalText = window.clipboardData.getData("Text"); } else { // Chrome, Firefox, Safari sOriginalText = oEvent.originalEvent.clipboardData.getData('text/plain'); } var aSeparatedText = this._tokenizer._parseString(sOriginalText); // if only one piece of text was pasted, we can assume that the user wants to alter it before it is converted into a token // in this case we leave it as plain text input if (aSeparatedText.length <= 1) { return; } setTimeout(function () { if (aSeparatedText) { if (this.fireEvent("_validateOnPaste", {texts: aSeparatedText}, true)) { var lastInvalidText = ""; for (i = 0; i < aSeparatedText.length; i++) { if (aSeparatedText[i]) { // pasting from excel can produce empty strings in the array, we don't have to handle empty strings var oToken = this._convertTextToToken(aSeparatedText[i], true); if (oToken) { aValidTokens.push(oToken); } else { lastInvalidText = aSeparatedText[i]; } } } this.updateDomValue(lastInvalidText); for (i = 0; i < aValidTokens.length; i++) { if (this._tokenizer._addUniqueToken(aValidTokens[i])) { aAddedTokens.push(aValidTokens[i]); } } if (aAddedTokens.length > 0) { this.fireTokenUpdate({ addedTokens: aAddedTokens, removedTokens: [], type: Tokenizer.TokenUpdateType.Added }); } } if (aAddedTokens.length) { this.cancelPendingSuggest(); } } }.bind(this), 0); }; MultiInput.prototype._convertTextToToken = function (text, bCopiedToken) { var result = null, item = null, token = null, iOldLength = this._tokenizer.getTokens().length; if (!this.getEditable()) { return null; } text = text.trim(); if (!text) { return null; } if ( this._getIsSuggestionPopupOpen() || bCopiedToken) { // only take item from suggestion list if popup is open // or token is pasted (otherwise pasting multiple tokens at once does not work) if (this._hasTabularSuggestions()) { //if there is suggestion table, select the correct item, to avoid selecting the wrong item but with same text. item = this._oSuggestionTable._oSelectedItem; } else { // impossible to enter other text item = this._getSuggestionItem(text); } } if (item && item.getText && item.getKey) { token = new Token({ text : item.getText(), key : item.getKey() }); } var that = this; result = this._tokenizer._validateToken({ text: text, token: token, suggestionObject: item, validationCallback: function (validated) { that._bIsValidating = false; if (validated) { that.setValue(""); if (that._bUseDialog && that._isMultiLineMode && that._oSuggestionTable.getItems().length === 0) { var iNewLength = that._tokenizer.getTokens().length; if (iOldLength < iNewLength) { that._oSuggPopover._oPopupInput.setValue(""); } that._setAllTokenVisible(); } } } }); return result; }; /** * Handle the backspace button, gives backspace to tokenizer if text cursor was on first character * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onsapprevious = function (oEvent) { if (this._getIsSuggestionPopupOpen()) { return; } if (this.getCursorPosition() === 0) { if (oEvent.srcControl === this) { Tokenizer.prototype.onsapprevious.apply(this._tokenizer, arguments); // we need this otherwise navigating with the left arrow key will trigger a scroll of the Tokens oEvent.preventDefault(); } } }; /** * Function scrolls the tokens to the end and focuses the input field. * * @private */ MultiInput.prototype._scrollAndFocus = function () { this._tokenizer.scrollToEnd(); // we set the focus back via jQuery instead of this.focus() since the latter on phones lead to unwanted opening of the // suggest popup this.$().find("input").focus(); }; /** * Handle the home button, gives control to tokenizer to move to first token * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onsaphome = function (oEvent) { if (this._tokenizer._checkFocus()) { Tokenizer.prototype.onsaphome.apply(this._tokenizer, arguments); } }; /** * Handle the end button, gives control to tokenizer to move to last token * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onsapend = function (oEvent) { if (this._tokenizer._checkFocus()) { Tokenizer.prototype.onsapend.apply(this._tokenizer, arguments); oEvent.preventDefault(); } }; /** * Function is called on keyboard enter, if possible, adds entered text as new token * * @private * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.onsapenter = function (oEvent) { if (Input.prototype.onsapenter) { Input.prototype.onsapenter.apply(this, arguments); } var bValidateFreeText = true; if (this._oSuggestionPopup && this._oSuggestionPopup.isOpen()) { if (this._hasTabularSuggestions()) { bValidateFreeText = !this._oSuggestionTable.getSelectedItem(); } else { bValidateFreeText = !this._getSuggestionsList().getSelectedItem(); } } if (bValidateFreeText) { this._validateCurrentText(); } this.focus(); }; /** * Checks whether the MultiInput or one of its internal DOM elements has the focus. * @returns {boolean} True if the input or its children elements have focus * @private */ MultiInput.prototype._checkFocus = function () { return this.getDomRef() && containsOrEquals(this.getDomRef(), document.activeElement); }; /** * Event handler called when control is losing the focus, checks if token validation is necessary * * @param {jQuery.Event} oEvent The event object * @private */ MultiInput.prototype.onsapfocusleave = function (oEvent) { var oPopup = this._oSuggestionPopup, oSelectedItemsPopup = this._oSelectedItemPicker, bNewFocusIsInSuggestionPopup = false, bNewFocusIsInTokenizer = false, bNewFocusIsInMultiInput = this._checkFocus(), oRelatedControlDomRef, bFocusIsInSelectedItemPopup, bNewFocusIsInReadOnlyPopover; if (oPopup instanceof sap.m.Popover) { if (oEvent.relatedControlId) { oRelatedControlDomRef = sap.ui.getCore().byId(oEvent.relatedControlId).getFocusDomRef(); bNewFocusIsInSuggestionPopup = containsOrEquals(oPopup.getFocusDomRef(), oRelatedControlDomRef); bNewFocusIsInTokenizer = containsOrEquals(this._tokenizer.getFocusDomRef(), oRelatedControlDomRef); bNewFocusIsInReadOnlyPopover = containsOrEquals(this._oReadOnlyPopover && this._oReadOnlyPopover.getFocusDomRef(), oRelatedControlDomRef); if (oSelectedItemsPopup) { bFocusIsInSelectedItemPopup = containsOrEquals(oSelectedItemsPopup.getFocusDomRef(), oRelatedControlDomRef); } } } // setContainerSize of multi-line mode in the end if (!bNewFocusIsInTokenizer && !bNewFocusIsInSuggestionPopup) { this._tokenizer.scrollToEnd(); } Input.prototype.onsapfocusleave.apply(this, arguments); // an asynchronous validation is running, no need to trigger validation again // OR the ValueHelp is triggered. either ways- no need for validation if (this._bIsValidating || this._bValueHelpOpen) { return; } if (!this._bUseDialog // Validation occurs if we are not on phone && !bNewFocusIsInSuggestionPopup // AND the focus is not in the suggestion popup && oEvent.relatedControlId !== this.getId() // AND the focus is not in the input field && oEvent.relatedControlId !== this._tokenizer.getId()) { // AND the focus is not on the tokenizer this._validateCurrentText(true); } if (!this._bUseDialog // not phone && this.getEditable()) { // control is editable if (bNewFocusIsInMultiInput || bNewFocusIsInSuggestionPopup) { return; } } if (!bFocusIsInSelectedItemPopup && !bNewFocusIsInTokenizer) { this._tokenizer._useCollapsedMode(true); // hide the text value only if the indicator is visible this._tokenizer._getIndicatorVisibility() && this._setValueInvisible(); } if (this._oReadOnlyPopover && this._oReadOnlyPopover.isOpen() && !bNewFocusIsInTokenizer && !bNewFocusIsInReadOnlyPopover) { this._oReadOnlyPopover.close(); } }; MultiInput.prototype._onDialogClose = function () { this._validateCurrentText(); this.setAggregation("tokenizer", this._tokenizer); this._tokenizer.setReverseTokens(false); this._tokenizer.invalidate(); }; /** * When tap on text field, deselect all tokens * @public * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.ontap = function (oEvent) { //deselect tokens when focus is on text field if (document.activeElement === this._$input[0] || document.activeElement === this._tokenizer.getDomRef()) { this._tokenizer.selectAllTokens(false); } if (oEvent && oEvent.isMarked("tokenDeletePress")) { return; } Input.prototype.ontap.apply(this, arguments); }; MultiInput.prototype._onclick = function (oEvent) { }; /** * Focus is on MultiInput * @public * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.onfocusin = function (oEvent) { this._bValueHelpOpen = false; //This means the ValueHelp is closed and the focus is back. So, reset that var if (oEvent.target === this.getFocusDomRef()) { Input.prototype.onfocusin.apply(this, arguments); } if (this.getEditable() && (!oEvent.target.classList.contains("sapMInputValHelp") && !oEvent.target.classList.contains("sapMInputValHelpInner"))) { if (this._oSuggestionPopup && this._oSuggestionPopup.isOpen()) { return; } this._tokenizer._useCollapsedMode(false); this._setValueVisible(); this._tokenizer.scrollToEnd(); } }; /** * When press ESC, deselect all tokens and all texts * @public * @param {jQuery.Event} oEvent The event object */ MultiInput.prototype.onsapescape = function (oEvent) { //deselect everything this._tokenizer.selectAllTokens(false); this.selectText(0, 0); Input.prototype.onsapescape.apply(this, arguments); }; /** * Function tries to turn current text into a token * @param {boolean} bExactMatch Whether an exact match should happen * @private */ MultiInput.prototype._validateCurrentText = function (bExactMatch) { var text = this.getValue(); if (!text || !this.getEditable()) { return; } text = text.trim(); if (!text) { return; } var item = null; if (bExactMatch || this._getIsSuggestionPopupOpen()) { // only take item from suggestion list if popup is open, otherwise it can be if (this._hasTabularSuggestions()) { //if there is suggestion table, select the correct item, to avoid selecting the wrong item but with same text. item = this._oSuggestionTable._oSelectedItem; } else { // impossible to enter other text item = this._getSuggestionItem(text, bExactMatch); } } var token = null; if (item && item.getText && item.getKey) { token = new Token({ text: item.getText(), key: item.getKey() }); } var that = this; // if maxTokens limit is not set or the added tokens are less than the limit if (!this.getMaxTokens() || this.getTokens().length < this.getMaxTokens()) { this._bIsValidating = true; this._tokenizer._addValidateToken({ text: text, token: token, suggestionObject: item, validationCallback: function (validated) { that._bIsValidating = false; if (validated) { that.setValue(""); } } }); } }; /** * Functions returns the current input field's cursor position * * @private * @return {int} The cursor position */ MultiInput.prototype.getCursorPosition = function () { return this._$input.cursorPos(); }; /** * Functions returns true if the input's text is completely selected * * @private * @return {boolean} true if text is selected, otherwise false, */ MultiInput.prototype._completeTextIsSelected = function () { var input = this._$input[0]; if (input.selectionStart !== 0) { return false; } if (input.selectionEnd !== this.getValue().length) { return false; } return true; }; /** * Functions returns true if the suggestion popup is currently open * @returns {boolean} Whether the suggestion popup is open * @private */ MultiInput.prototype._getIsSuggestionPopupOpen = function () { return this._oSuggPopover && this._oSuggPopover._oPopover.isOpen(); }; MultiInput.prototype.setEditable = function (bEditable) { bEditable = this.validateProperty("editable", bEditable); var oTokensList = this._getTokensList(); if (bEditable === this.getEditable()) { return this; } if (Input.prototype.setEditable) { Input.prototype.setEditable.apply(this, arguments); } this._tokenizer.setEditable(bEditable); if (bEditable) { if (this._bUseDialog) { this._oSuggPopover._oPopover.addContent(oTokensList); } else { this._getSelectedItemsPicker().addContent(oTokensList); } oTokensList.setMode(ListMode.MultiSelect); } else { oTokensList.setMode(ListMode.None); this._getReadOnlyPopover().addContent(oTokensList); } return this; }; /** * Function returns an item which's text starts with the given text within the given items array * * @private * @param {string} sText The given starting text * @param {array} aItems The item array * @param {boolean} bExactMatch Whether the match should be exact * @param {function} fGetText Function to extract text from a single item * @return {object} A found item or null */ MultiInput.prototype._findItem = function (sText, aItems, bExactMatch, fGetText) { if (!sText) { return; } if (!(aItems && aItems.length)) { return; } sText = sText.toLowerCase(); var length = aItems.length; for (var i = 0; i < length; i++) { var item = aItems[i]; var compareText = fGetText(item); if (!compareText) { continue; } compareText = compareText.toLowerCase(); if (compareText === sText) { return item; } if (!bExactMatch && compareText.indexOf(sText) === 0) { return item; } } }; /** * Function searches for an item with the given text within the suggestion items * * @private * @param {string} sText The search text * @param {boolean} bExactMatch If true, only items will be returned which exactly matches the text * @return {sap.ui.core.Item} A found item or null */ MultiInput.prototype._getSuggestionItem = function (sText, bExactMatch) { var items = null; var item = null; if (this._hasTabularSuggestions()) { items = this.getSuggestionRows(); item = this._findItem(sText, items, bExactMatch, function (oRow) { var cells = oRow.getCells(); var foundText = null; if (cells) { var i; for (i = 0; i < cells.length; i++) { if (cells[i].getText) { foundText = cells[i].getText(); break; } } } return foundText; }); } else { items = this.getSuggestionItems(); item = this._findItem(sText, items, bExactMatch, function (item) { return item.getText(); }); } return item; }; MultiInput.getMetadata().forwardAggregation( "tokens", { getter: function(){ return this._tokenizer; }, aggregation: "tokens", forwardBinding: true } ); /** * Function overwrites clone function to add tokens to MultiInput * * @public * @return {sap.ui.core.Element} reference to the newly created clone */ MultiInput.prototype.clone = function () { var oClone, oTokenizerClone; this.detachSuggestionItemSelected(this._onSuggestionItemSelected, this); this.detachLiveChange(this._onLiveChange, this); this._tokenizer.detachTokenChange(this._onTokenChange, this); this._tokenizer.detachTokenUpdate(this._onTokenUpdate, this); oClone = Input.prototype.clone.apply(this, arguments); oClone.destroyAggregation("tokenizer"); oClone._tokenizer = null; oTokenizerClone = this._tokenizer.clone(); oClone._tokenizer = oTokenizerClone; oClone.setAggregation("tokenizer", oTokenizerClone, true); this._tokenizer.attachTokenChange(this._onTokenChange, this); this._tokenizer.attachTokenUpdate(this._onTokenUpdate, this); oClone._tokenizer.attachTokenChange(oClone._onTokenChange, oClone); oClone._tokenizer.attachTokenUpdate(oClone._onTokenUpdate, oClone); oClone._tokenizer._handleNMoreIndicatorPress(oClone._handleIndicatorPress.bind(oClone)); this.attachSuggestionItemSelected(this._onSuggestionItemSelected, this); this.attachLiveChange(this._onLiveChange, this); return oClone; }; /** * Function returns domref which acts as reference point for the opening suggestion menu * * @public * @returns {domRef} The domref at which to open the suggestion menu */ MultiInput.prototype.getPopupAnchorDomRef = function () { return this.getDomRef("content"); }; /** * Function sets an array of tokens, existing tokens will get overridden * * @param {sap.m.Token[]} aTokens The new token set * @public * @returns {sap.m.MultiInput} Pointer to the control instance for chaining */ MultiInput.prototype.setTokens = function (aTokens) { var oValidatedToken, aValidatedTokens = [], i; if (Array.isArray(aTokens)) { for (i = 0; i < aTokens.length; i++) { oValidatedToken = this.validateAggregation("tokens", aTokens[i], true); ManagedObjectMetadata.addAPIParentInfoBegin(aTokens[i], this, "tokens"); aValidatedTokens.push(oValidatedToken); } this._tokenizer.setTokens(aValidatedTokens); for (i = 0; i < aTokens.length; i++) { ManagedObjectMetadata.addAPIParentInfoEnd(aTokens[i]); } } else { throw new Error("\"" + aTokens + "\" is of type " + typeof aTokens + ", expected array for aggregation tokens of " + this); } return this; }; MultiInput.TokenChangeType = { Added: "added", Removed: "removed", RemovedAll: "removedAll", TokensChanged: "tokensChanged" }; MultiInput.WaitForAsyncValidation = "sap.m.Tokenizer.WaitForAsyncValidation"; /** * Get the reference element which the message popup should dock to * * @return {DOMRef} Dom Element which the message popup should dock to * @protected * @function */ MultiInput.prototype.getDomRefForValueStateMessage = MultiInput.prototype.getPopupAnchorDomRef; /** * Updates the inner input field. * * @protected */ MultiInput.prototype.updateInputField = function(sNewValue) { Input.prototype.updateInputField.call(this, sNewValue); this.setDOMValue(''); }; /** * @see sap.ui.core.Control#getAccessibilityInfo * @returns {object} The accessibility object * @protected */ MultiInput.prototype.getAccessibilityInfo = function () { var sText = this.getTokens().map(function (oToken) { return oToken.getText(); }).join(" "); var oInfo = Input.prototype.getAccessibilityInfo.apply(this, arguments); oInfo.type = oRb.getText("ACC_CTR_TYPE_MULTIINPUT"); oInfo.description = ((oInfo.description || "") + " " + sText).trim(); return oInfo; }; /** * Modifies the picker provided from sap.m.Input when on mobile * * @private */ MultiInput.prototype._modifySuggestionPicker = function () { var that = this; // on mobile the Input's suggestionList is used for displaying // any suggestions or tokens related information if (!this._bUseDialog) { return; } this._bShowSelectedButton = this._createFilterSelectedButton(); this._oSuggPopover._oPopover.addContent(this._getTokensList()); this._oSuggPopover._oPopover .attachBeforeOpen(function(){ that._manageListsVisibility(that._bShowListWithTokens); that._fillList(); that._updatePickerHeaderTitle(); }) .attachAfterClose(function(){ that._tokenizer._useCollapsedMode(true); that._bShowListWithTokens = false; }); this._oSuggPopover._oPopover.getCustomHeader().removeAllContentMiddle(); this._oSuggPopover._oPopover.destroyCustomHeader(true); this._oSuggPopover._oPopover.setCustomHeader(new Bar({ contentMiddle: [new Title()], contentRight: new Button({ icon: IconPool.getIconURI("decline"), press: function() { that._oSuggPopover._oPopover.close(); } }) })); this._oSuggPopover._oPopover.setSubHeader(new Toolbar({ content : [ this._oSuggPopover._oPopupInput, this._bShowSelectedButton ]} )); this._oSuggPopover._oPopupInput.onsapenter = function (oEvent) { that._validateCurrentText(); that._setValueInvisible(); // Fire through the MultiInput Popup's input value and save it that.onChange(oEvent, null, this.getValue()); }; this._oSuggPopover._oPopupInput.attachLiveChange(function(){ if (that._bShowListWithTokens) { // filter inside tokens that._filterTokens(this.getValue()); } that._manageListsVisibility(that._bShowListWithTokens); }); }; /** * Creates an instance of <code>sap.m.ToggleButton</code>. * * @returns {sap.m.ToggleButton} The Button instance * @private */ MultiInput.prototype._createFilterSelectedButton = function () { var sIconURI = IconPool.getIconURI("multiselect-all"), that = this; return new ToggleButton({ icon: sIconURI, press: function (oEvent) { that._bShowListWithTokens = oEvent.getSource().getPressed(); that._manageListsVisibility(that._bShowListWithTokens); } }); }; /** * This event handler will be called before the control's picker popover is opened. * * @private */ MultiInput.prototype._onBeforeOpenTokensPicker = function() { var oPopover = this._getSelectedItemsPicker(), oDomRef = this.getDomRef(), sWidth; this._setValueInvisible(); this._fillList(); if (oDomRef && oPopover) { sWidth = (oDomRef.offsetWidth / parseFloat(library.BaseFontSize)) + "rem"; oPopover.setContentMinWidth(sWidth); } }; /** * This event handler will be called after the MultiComboBox's Pop-up is closed. * * @private */ MultiInput.prototype._onAfterCloseTokensPicker = function() { if (this._oSuggPopover && !this.getValue()) { this._tokenizer._useCollapsedMode(true); this._setValueInvisible(); } }; /** * Gets the picker header title. * * @returns {sap.m.Title | null} The title instance of the Picker * @protected */ MultiInput.prototype.getDialogTitle = function() { var oPicker = this._oSuggPopover._oPopover, oHeader = oPicker && oPicker.getCustomHeader(); if (oHeader) { return oHeader.getContentMiddle()[0]; } return null; }; /** * Modifies the title of the picker's header provided from sap.m.Input * * @private */ MultiInput.prototype._updatePickerHeaderTitle = function() { var oLabel, aLabels; aLabels = this.getLabels(); if (aLabels.length) { oLabel = aLabels[0]; if (oLabel && (typeof oLabel.getText === "function")) { this.getDialogTitle().setText(oLabel.getText()); } } else { this.getDialogTitle().setText(oRb.getText("COMBOBOX_PICKER_TITLE")); } }; /** * Handles the opening of a device specific picker * * @returns {sap.m.MultiInput} Pointer to the control instance for chaining * @private */ MultiInput.prototype._openSelectedItemsPicker = function () { // on mobile reuse the input's suggestion popup if (this._bUseDialog) { this._oSuggPopover._oPopover.open(); } else { // on desktop create separate popover for tokens var oPicker = this._getSelectedItemsPicker(); if (oPicker) { oPicker.open(); } } this._manageListsVisibility(true); this._setValueVisible(); return this; }; /** * Getter for the list containing tokens * * @returns {sap.m.List} The list * @private */ MultiInput.prototype._getTokensList = function() { if (!this._oSelectedItemsList) { this._oSelectedItemsList = this._createTokensList(); } return this._oSelectedItemsList; }; /** * Getter for the suggestion list provided from sap.m.Input * * @returns {sap.m.List} The suggestion list * @private */ MultiInput.prototype._getSuggestionsList = function() { return this._oSuggPopover && this._oSuggPopover._oList; }; /** * Creates a list for items generated from token * * @returns {sap.m.List} The list * @private */ MultiInput.prototype._createTokensList = function() { return new List({ width: "auto", mode: ListMode.MultiSelect, includeItemInSelection: true, rememberSelections: false }).attachBrowserEvent("tap", this._handleItemTap, this) .attachSelectionChange(this._handleSelectionLiveChange, this); }; /** * Filters the items inside the token's list * * @param {String} sValue The filtering value * @private */ MultiInput.prototype._filterTokens = function (sValue) { this._getTokensList().getItems().forEach(function(oItem){ if (oItem.getTitle().toLowerCase().indexOf(sValue) > -1) { oItem.setVisible(true); } else { oItem.setVisible(false); } }); }; /** * Manages the visibility of the suggestion list and the selected items list * * @param {boolean} bShowListWithTokens True if the selected items list should be shown * @private */ MultiInput.prototype._manageListsVisibility = function (bShowListWithTokens) { this._getTokensList().setVisible(bShowListWithTokens); this._getSuggestionsList() && this._getSuggestionsList().setVisible(!bShowListWithTokens); if (this._bUseDialog) { this._bShowSelectedButton.setPressed(bShowListWithTokens); } }; /** * Generates a StandardListItem from token * * @param {sap.m.Token} oToken The token * @private * @returns {sap.m.StandardListItem | null} The generated ListItem */ MultiInput.prototype._mapTokenToListItem = function (oToken) { if (!oToken) { return null; } var oListItem = new sap.m.StandardListItem({ selected: true, title: oToken.getText() }); oListItem.data("key", oToken.getKey()); oListItem.data("text", oToken.getText()); oListItem.data("tokenId", oToken.getId()); return oListItem; }; /** * Updates the content of the list with tokens * * @private */ MultiInput.prototype._fillList = function() { var aTokens = this.getTokens(), oListItem; if (!aTokens) { return; } this._getTokensList().removeAllItems(); for ( var i = 0, aItemsLength = aTokens.length; i < aItemsLength; i++) { var oToken = aTokens[i], oListItem = this._mapTokenToListItem(oToken); // add the mapped item type of sap.m.StandardListItem to the list this._getTokensList().addItem(oListItem); } }; /** * Handler for the press event on the N-more label * * @private */ MultiInput.prototype._handleIndicatorPress = function() { this._bShowListWithTokens = true; if (this.getEditable()) { this._openSelectedItemsPicker(); } else { this._fillList(); this._getReadOnlyPopover().openBy(this._tokenizer._oIndicator[0]); } }; /** * Called when the user taps on a list item * @param {jQuery.Event} oEvent The event triggered by the user * @private */ MultiInput.prototype._handleItemTap = function (oEvent) { if (jQuery(oEvent.target).hasClass("sapMCbMark")) { return; } if (this._bUseDialog) { this._oSuggPopover._oPopover.close(); } else if (this._oReadOnlyPopover && this._oReadOnlyPopover.isOpen()) { this._oReadOnlyPopover.close(); } else { this._getSelectedItemsPicker().close(); } }; /** * Called when the user selects or deselects a list item from the token's popover * @param {jQuery.Event} oEvent The event triggered by the user * @private */ MultiInput.prototype._handleSelectionLiveChange = function(oEvent) { var oListItem = oEvent.getParameter("listItem"), bIsSelected = oEvent.getParameter("selected"); this._syncTokensWithSelection(oListItem, bIsSelected); }; /** * Synchronizes the tokens with the selected items in the token's popover * * @param {sap.m.StandardListItem} oItemData The target list item * @param {boolean} bSelected True if the item is selected * @private */ MultiInput.prototype._syncTokensWithSelection = function(oItemData, bSelected) { if (bSelected) { var oToken = new Token({ text: oItemData.data("text"), key: oItemData.data("key") }); oItemData.data("tokenId", oToken.getId()); this.addToken(oToken); this.fireTokenUpdate({ addedTokens: [oToken], removedTokens: [], type: Tokenizer.TokenUpdateType.Added }); } else { var sSelectedId = oItemData.data("tokenId"); this.getTokens().some(function(oToken){ if (oToken.getId() === sSelectedId) { this._tokenizer._onTokenDelete(oToken); return true; } }.bind(this)); } }; /** * Returns a modified instance type of <code>sap.m.Popover</code>. * * @returns {sap.m.Popover} The Popover instance * @private */ MultiInput.prototype._getSelectedItemsPicker = function() { if (this._oSelectedItemPicker) { return this._oSelectedItemPicker; } this._oSelectedItemPicker = this._createDropdown(); if (!this._bUseDialog) { // configuration this._oSelectedItemPicker.setHorizontalScrolling(false) .attachBeforeOpen(t