UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

1,756 lines (1,501 loc) 48.5 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ // Provides control sap.m.Tokenizer. sap.ui.define([ './library', 'sap/m/Button', 'sap/m/List', 'sap/m/StandardListItem', 'sap/m/ResponsivePopover', 'sap/ui/core/Core', 'sap/ui/core/Control', 'sap/ui/core/Element', 'sap/ui/core/delegate/ScrollEnablement', 'sap/ui/Device', 'sap/ui/core/InvisibleText', 'sap/ui/core/ResizeHandler', './TokenizerRenderer', "sap/ui/dom/containsOrEquals", "sap/ui/events/KeyCodes", "sap/base/Log", "sap/ui/core/EnabledPropagator", "sap/ui/core/theming/Parameters", // jQuery Plugin "scrollLeftRTL" "sap/ui/dom/jquery/scrollLeftRTL" ], function( library, Button, List, StandardListItem, ResponsivePopover, Core, Control, Element, ScrollEnablement, Device, InvisibleText, ResizeHandler, TokenizerRenderer, containsOrEquals, KeyCodes, Log, EnabledPropagator, Parameters ) { "use strict"; var CSS_CLASS_NO_CONTENT_PADDING = "sapUiNoContentPadding"; var RenderMode = library.TokenizerRenderMode; var PlacementType = library.PlacementType; var ListMode = library.ListMode; var ButtonType = library.ButtonType; /** * Constructor for a new Tokenizer. * * @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 tokenizer is a container for {@link sap.m.Token Tokens}. It also handles all actions associated with the tokens like adding, deleting, selecting and editing. * <h3>Structure</h3> * The tokens are stored in the <code>tokens</code> aggregation. * The tokenizer can determine, by setting the <code>editable</code> property, whether the tokens in it are editable. * Still the Token itself can determine if it is <code>editable</code>. This allows you to have non-editable Tokens in an editable Tokenizer. * * <h3>Usage</h3> * <h4>When to use:</h4> * The tokenizer can only be used as part of {@link sap.m.MultiComboBox MultiComboBox},{@link sap.m.MultiInput MultiInput} or {@link sap.ui.comp.valuehelpdialog.ValueHelpDialog ValueHelpDialog} * * @extends sap.ui.core.Control * @author SAP SE * @version 1.117.4 * * @constructor * @public * @since 1.22 * @alias sap.m.Tokenizer * @see {@link fiori:https://experience.sap.com/fiori-design-web/token/ Tokenizer} */ var Tokenizer = Control.extend("sap.m.Tokenizer", /** @lends sap.m.Tokenizer.prototype */ { metadata : { library : "sap.m", properties : { /** * true if tokens shall be editable otherwise false */ editable : {type : "boolean", group : "Misc", defaultValue : true}, /** * Defines the width of the Tokenizer. */ width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : null}, /** * Defines the maximum width of the Tokenizer. */ maxWidth : {type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue : "100%"}, /** * Defines the mode that the Tokenizer will use: * <ul> * <li><code>sap.m.TokenizerRenderMode.Loose</code> mode shows all tokens, no matter the width of the Tokenizer</li> * <li><code>sap.m.TokenizerRenderMode.Narrow</code> mode forces the Tokenizer to show only as much tokens as possible in its width and add an n-More indicator</li> * </ul> */ renderMode: {type : "string", group : "Misc", defaultValue : RenderMode.Loose}, /** * Defines the count of hidden tokens if any. If this property is set to 0, the n-More indicator will not be shown. */ hiddenTokensCount: {type : "int", group : "Misc", defaultValue : 0, visibility: "hidden"} }, defaultAggregation : "tokens", aggregations : { /** * the currently displayed tokens */ tokens : {type : "sap.m.Token", multiple : true, singularName : "token"}, /** * Hidden text used for accesibility */ _tokensInfo: {type: "sap.ui.core.InvisibleText", multiple: false, visibility: "hidden"} }, associations : { /** * Association to controls / ids which describe this control (see WAI-ARIA attribute aria-describedby). */ ariaDescribedBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaDescribedBy"}, /** * Association to controls / ids which label this control (see WAI-ARIA attribute aria-labelledby). */ ariaLabelledBy: {type: "sap.ui.core.Control", multiple: true, singularName: "ariaLabelledBy"} }, events : { /** * Fired when the tokens aggregation changed (add / remove token) * @deprecated Since version 1.82, replaced by <code>tokenDelete</code> event. */ tokenChange : { deprecated: true, parameters : { /** * type of tokenChange event. * There are four TokenChange types: "added", "removed", "removedAll", "tokensChanged". * Use sap.m.Tokenizer.TokenChangeType.Added for "added", sap.m.Tokenizer.TokenChangeType.Removed for "removed", sap.m.Tokenizer.TokenChangeType.RemovedAll for "removedAll" and sap.m.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) * @deprecated Since version 1.82, replaced by <code>tokenDelete</code> event. * @since 1.46 */ tokenUpdate: { deprecated: true, allowPreventDefault : true, parameters: { /** * Type of tokenChange event. * There are two TokenUpdate types: "added", "removed" * Use sap.m.Tokenizer.TokenUpdateType.Added for "added" and sap.m.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[]"} } }, /** * Fired when a token is deleted by clicking icon, pressing backspace or delete button. * <Note:> Once the event is fired, application is responsible for removing / destroying the token from the aggregation. * @public * @since 1.82 */ tokenDelete: { parameters: { /** * The array of tokens that are removed. */ tokens: { type: "sap.m.Token[]" }, /** * Keycode of the key pressed for deletion (backspace or delete). */ keyCode: { type: "number" } } } } }, renderer: TokenizerRenderer }); var oRb = Core.getLibraryResourceBundle("sap.m"); EnabledPropagator.apply(Tokenizer.prototype, [true]); Tokenizer.prototype.init = function() { // Do not allow text selection in the Tokenizer // If called with 'false', the method prevents the // default behavior and propagation of the 'selectstart' event. // For more info - check sap.ui.core.Control.js this.allowTextSelection(false); this._oTokensWidthMap = {}; this._oIndicator = null; this._bShouldRenderTabIndex = null; this._oScroller = new ScrollEnablement(this, this.getId() + "-scrollContainer", { horizontal : true, vertical : false, nonTouchScrolling : true }); // The ratio between the font size of the token and the font size of the items used in the // n-more popover. this._fFontSizeRatio = 1.0; if (Core.getConfiguration().getAccessibility()) { var sAriaTokenizerContainToken = new InvisibleText({ text: oRb.getText("TOKENIZER_ARIA_NO_TOKENS") }); this.setAggregation("_tokensInfo", sAriaTokenizerContainToken); } // listen for delete event of tokens, it bubbles this.attachEvent("delete", function(oEvent) { var oToken = oEvent.getSource(); var aSelectedTokens = this.getSelectedTokens(); this._fireCompatibilityEvents(oToken, aSelectedTokens); this.fireEvent("tokenDelete", { tokens: [oToken] }); oEvent.cancelBubble(); }, this); }; /** * Fires deprecated events for backwards compatibility * * @private * @param {object} oToken Updated token * @param {array} aSelectedTokens Array of selected changed tokens * @deprecated As of version 1.82, replaced by <code>tokenDelete</code> event */ Tokenizer.prototype._fireCompatibilityEvents = function(oToken, aSelectedTokens) { this.fireTokenChange({ type: Tokenizer.TokenChangeType.Removed, token: oToken, tokens: aSelectedTokens.length ? aSelectedTokens : [oToken], addedTokens: [], removedTokens: aSelectedTokens.length ? aSelectedTokens : [oToken] }); this.fireTokenUpdate({ type: Tokenizer.TokenChangeType.Removed, addedTokens: [], removedTokens: aSelectedTokens.length ? aSelectedTokens : [oToken] }); }; /** * Opens or closes the token popup when N-more label is pressed. * * @private */ Tokenizer.prototype._handleNMoreIndicatorPress = function () { this._togglePopup(this.getTokensPopup()); }; /** * Getter for the list containing tokens. * * @returns {sap.m.List} The list * @private */ Tokenizer.prototype._getTokensList = function () { if (!this._oTokensList) { this._oTokensList = new List({ width: "auto", mode: ListMode.Delete }).attachDelete(this._handleListItemDelete, this); } return this._oTokensList; }; /** * Changes list mode. * * @param sMode {sap.m.ListMode} * @private */ Tokenizer.prototype._setPopoverMode = function (sMode) { var oSettings = {}, oPopover = this.getTokensPopup(); switch (sMode) { case ListMode.Delete: oSettings = { showArrow: false, placement: PlacementType.VerticalPreferredBottom }; break; default: oSettings = { showArrow: true, placement: PlacementType.Auto }; break; } oPopover.setShowArrow(oSettings.showArrow); oPopover.setPlacement(oSettings.placement); this._getTokensList().setMode(sMode); }; /** * Fills a list by creating new list items and mapping them to certain token. * * There might be a filtering function, so only certain tokens can be mapped to a ListItem. * * @param oList {sap.m.List} * @param fnFilter {function} * @private */ Tokenizer.prototype._fillTokensList = function (oList, fnFilter) { oList.destroyItems(); fnFilter = fnFilter ? fnFilter : function () { return true; }; this.getTokens() .filter(fnFilter) .forEach(function (oToken) { oList.addItem(this._mapTokenToListItem(oToken)); }, this); }; /** * Handles token deletion from the List. * * @param oEvent * @private */ Tokenizer.prototype._handleListItemDelete = function (oEvent) { var oListItem = oEvent.getParameter("listItem"); var sSelectedId = oListItem && oListItem.data("tokenId"); var oTokenToDelete; oTokenToDelete = this.getTokens().filter(function(oToken){ return (oToken.getId() === sSelectedId) && oToken.getEditable(); })[0]; if (oTokenToDelete) { this.fireTokenUpdate({ addedTokens: [], removedTokens: [oTokenToDelete], type: Tokenizer.TokenUpdateType.Removed }); this.fireTokenDelete({ tokens: [oTokenToDelete] }); this._adjustTokensVisibility(); } }; /** * Returns N-More Popover/Dialog. * * @private * @ui5-restricted sap.m.MultiInput, sap.m.MultiComboBox * @returns {sap.m.ResponsivePopover} */ Tokenizer.prototype.getTokensPopup = function () { if (this._oPopup) { return this._oPopup; } this._oPopup = new ResponsivePopover({ showArrow: false, showHeader: Device.system.phone, placement: PlacementType.Auto, offsetX: 0, offsetY: 3, horizontalScrolling: false, title: this._getDialogTitle(), content: this._getTokensList() }) .attachBeforeOpen(function () { var iWidestElement = this.getEditable() ? 120 : 32, // Paddings & Delete icons in editable mode && paddings in non-editable mode oPopup = this._oPopup, fnGetDensityMode = function () { var oParent = this.getDomRef() && this.getDomRef().parentElement; var sDensityMode = "Cozy"; if (!oParent) { return sDensityMode; } if (oParent.closest(".sapUiSizeCompact") !== null || document.body.classList.contains("sapUiSizeCompact")) { sDensityMode = "Compact"; } return sDensityMode; }.bind(this), fnGetRatioPromise = new Promise(function (resolve) { Parameters.get({ name: ["_sap_m_Tokenizer_FontSizeRatio" + fnGetDensityMode()], callback: function (sFontSizeRatio) { var fRatio = parseFloat(sFontSizeRatio); if (isNaN(fRatio)) { resolve(this._fFontSizeRatio); return; } resolve(fRatio); }.bind(this) }); }.bind(this)); if (oPopup.getContent && !oPopup.getContent().length) { oPopup.addContent(this._getTokensList()); } this._fillTokensList(this._getTokensList()); iWidestElement += Object.keys(this._oTokensWidthMap) // Object.values is not supported in IE .map(function (sKey) { return this._oTokensWidthMap[sKey]; }, this) .sort(function (a, b) { return a - b; }) // Just sort() returns odd results .pop() || 0; // Get the longest element in PX // The row below takes into consideration the ratio of the token's width to item's font size // which in turn is used to adjust the longest element's width so that there is no truncation // in the n-more popover. // width = width + (width * <<ratio converted in difference>>); fnGetRatioPromise.then(function (fRatio) { iWidestElement += Math.ceil(iWidestElement * ( 1 - fRatio )); oPopup.setContentWidth(iWidestElement + "px"); }); }, this); this.addDependent(this._oPopup); this._oPopup.addStyleClass(CSS_CLASS_NO_CONTENT_PADDING); this._oPopup.addStyleClass("sapMTokenizerTokensPopup"); if (Device.system.phone) { this._oPopup.setEndButton(new Button({ text: oRb.getText("SUGGESTIONSPOPOVER_CLOSE_BUTTON"), type: ButtonType.Emphasized, press: function () { this._oPopup.close(); }.bind(this) })); } return this._oPopup; }; Tokenizer.prototype._getDialogTitle = function () { var oResourceBundle = Core.getLibraryResourceBundle("sap.m"); var aLabeles = this.getAriaLabelledBy().map(function(sLabelID) { return Core.byId(sLabelID); }); return aLabeles.length ? aLabeles[0].getText() : oResourceBundle.getText("COMBOBOX_PICKER_TITLE"); }; /** * Toggles the popover. * * @private * @ui5-restricted sap.m.MultiInput, sap.m.MultiComboBox */ Tokenizer.prototype._togglePopup = function (oPopover) { var oOpenByDom, oDomRef = this.getDomRef(), oPopoverIsOpen = oPopover.isOpen(), bEditable = this.getEditable(); this._setPopoverMode(bEditable ? ListMode.Delete : ListMode.None); if (oPopoverIsOpen) { oPopover.close(); } else { oOpenByDom = bEditable || this.hasOneTruncatedToken() ? oDomRef : this._oIndicator[0]; oOpenByDom = oOpenByDom && oOpenByDom.className.indexOf("sapUiHidden") === -1 ? oOpenByDom : oDomRef; oPopover.openBy(oOpenByDom || oDomRef); } }; /** * Generates a StandardListItem from token. * * @param {sap.m.Token} oToken The token * @private * @returns {sap.m.StandardListItem | null} The generated ListItem */ Tokenizer.prototype._mapTokenToListItem = function (oToken) { if (!oToken) { return null; } var oListItem = new StandardListItem({ selected: true, wrapping: true, wrapCharLimit: 10000 }).data("tokenId", oToken.getId()); oListItem.setTitle(oToken.getText()); return oListItem; }; /** Gets the width of the tokenizer that will be used for the calculation for hiding * or revealing the tokens. * * @returns {number} The width of the DOM in pixels. * @private */ Tokenizer.prototype._getPixelWidth = function () { var sMaxWidth = this.getMaxWidth(), iTokenizerWidth, oDomRef = this.getDomRef(), iPaddingLeft; if (!oDomRef) { return; } // The padding needs to be exluded from the calculations later on // as it is actually not an available space. iPaddingLeft = parseInt(this.$().css("padding-left")); if (sMaxWidth.indexOf("px") === -1) { // We need to use pixel width in order to calculate the space left for the Tokens. // In standalone Tokenizer, we take the width of the Tokenizer itself. iTokenizerWidth = oDomRef.clientWidth; } else { iTokenizerWidth = parseInt(this.getMaxWidth()); } return iTokenizerWidth - iPaddingLeft; }; /** * Function determines which tokens should be displayed and adds N-more label. * * @private */ Tokenizer.prototype._adjustTokensVisibility = function() { if (!this.getDomRef()) { return; } var iTokenizerWidth = this._getPixelWidth(), aTokens = this._getVisibleTokens().reverse(), iTokensCount = aTokens.length, iLabelWidth, iFreeSpace, iCounter, iFirstTokenToHide = -1; // find the index of the first overflowing token aTokens.some(function (oToken, iIndex) { iTokenizerWidth = iTokenizerWidth - this._oTokensWidthMap[oToken.getId()]; if (iTokenizerWidth < 0) { iFirstTokenToHide = iIndex; return true; } else { iFreeSpace = iTokenizerWidth; } }, this); if (iTokensCount === 1 && iFirstTokenToHide !== -1) { this.setFirstTokenTruncated(true); return; } else if (iTokensCount === 1 && aTokens[0].getTruncated()) { this.setFirstTokenTruncated(false); } // adjust the visibility of the tokens if (iFirstTokenToHide > -1) { for (iCounter = 0; iCounter < iTokensCount; iCounter++) { if (iCounter >= iFirstTokenToHide) { aTokens[iCounter].addStyleClass("sapMHiddenToken"); } else { aTokens[iCounter].removeStyleClass("sapMHiddenToken"); } } this._handleNMoreIndicator(iTokensCount - iFirstTokenToHide); iLabelWidth = this._oIndicator.width(); // if there is not enough space after getting the actual indicator width, hide the last visible token // and update the n-more indicator if (iLabelWidth >= iFreeSpace) { iFirstTokenToHide = iFirstTokenToHide - 1; this._handleNMoreIndicator(iTokensCount - iFirstTokenToHide); aTokens[iFirstTokenToHide].addStyleClass("sapMHiddenToken"); } this._setHiddenTokensCount(iTokensCount - iFirstTokenToHide); } else { // if no token needs to be hidden, show all this._setHiddenTokensCount(0); this._showAllTokens(); } }; /** * Sets the first token truncation. * * @param {boolean} bValue The value to set * @returns {this} <code>this</code> instance for method chaining * @protected */ Tokenizer.prototype.setFirstTokenTruncated = function (bValue) { var oToken = this.getTokens()[0]; oToken && oToken.setTruncated(bValue); if (bValue) { this.addStyleClass("sapMTokenizerOneLongToken"); } else { this.removeStyleClass("sapMTokenizerOneLongToken"); this.scrollToEnd(); } return this; }; /** * Checks if the token is one and truncated. * * @returns {boolean} * @protected */ Tokenizer.prototype.hasOneTruncatedToken = function () { return this.getTokens().length === 1 && this.getTokens()[0].getTruncated(); }; /** * Renders the N-more label. * @private * * @param {number} iHiddenTokensCount The number of hidden tokens * @returns {this} this instance for method chaining */ Tokenizer.prototype._handleNMoreIndicator = function (iHiddenTokensCount) { if (!this.getDomRef()) { return this; } if (iHiddenTokensCount) { var sLabelKey = "MULTIINPUT_SHOW_MORE_TOKENS"; if (iHiddenTokensCount === this._getVisibleTokens().length) { if (iHiddenTokensCount === 1) { sLabelKey = "TOKENIZER_SHOW_ALL_ITEM"; } else { sLabelKey = "TOKENIZER_SHOW_ALL_ITEMS"; } } this._oIndicator.html(oRb.getText(sLabelKey, iHiddenTokensCount)); } return this; }; /** * Returns the visible tokens. * * @returns {array} Array of tokens * @private */ Tokenizer.prototype._getVisibleTokens = function () { return this.getTokens().filter(function (oToken) { return oToken.getVisible(); }); }; /** * Function makes all tokens visible, used for collapsed=false. * * @private */ Tokenizer.prototype._showAllTokens = function() { this._getVisibleTokens().forEach(function(oToken) { // TODO: Token should provide proper API for this oToken.removeStyleClass("sapMHiddenToken"); }); }; /** * Function returns the internally used scroll delegate. * * @public * @returns {sap.ui.core.delegate.ScrollEnablement} The scroll delegate */ Tokenizer.prototype.getScrollDelegate = function() { return this._oScroller; }; /** * Function scrolls the tokens to the end. * * @public */ Tokenizer.prototype.scrollToEnd = function() { var domRef = this.getDomRef(), bRTL = Core.getConfiguration().getRTL(), iScrollWidth, scrollDiv; if (!this.getDomRef()) { return; } scrollDiv = this.$().find(".sapMTokenizerScrollContainer")[0]; iScrollWidth = scrollDiv.scrollWidth; if (bRTL) { iScrollWidth *= -1; } domRef.scrollLeft = iScrollWidth; }; Tokenizer.prototype._registerResizeHandler = function(){ if (!this._sResizeHandlerId) { this._sResizeHandlerId = ResizeHandler.register(this.getDomRef(), this._handleResize.bind(this)); } }; Tokenizer.prototype._handleResize = function(){ this._useCollapsedMode(this.getRenderMode()); this.scrollToEnd(); }; /** * Function sets the tokenizer's width in pixels. * * @public * @param {number} nWidth The new width in pixels */ Tokenizer.prototype.setPixelWidth = function(nWidth) { if (typeof nWidth !== "number") { Log.warning("Tokenizer.setPixelWidth called with invalid parameter. Expected parameter of type number."); return; } this.setWidth(nWidth + "px"); if (this._oScroller) { this._oScroller.refresh(); } }; /** * Function scrolls the tokens to the start. * * @public * */ Tokenizer.prototype.scrollToStart = function() { var domRef = this.getDomRef(); if (!domRef) { return; } domRef.scrollLeft = 0; }; /** * Function returns the tokens' width. * * @public * * @returns {number} The complete width of all tokens */ Tokenizer.prototype.getScrollWidth = function(){ if (!this.getDomRef()) { return 0; } return this.$().children(".sapMTokenizerScrollContainer")[0].scrollWidth; }; Tokenizer.prototype.onBeforeRendering = function() { var aTokens = this.getTokens(); if (aTokens.length !== 1) { this.setFirstTokenTruncated(false); } aTokens.forEach(function(oToken, iIndex) { oToken.setProperty("editableParent", this.getEditable() && this.getEnabled()); oToken.setProperty("posinset", iIndex + 1); oToken.setProperty("setsize", aTokens.length); }, this); this._setTokensAria(); }; /** * Called after the control is rendered. * * @private */ Tokenizer.prototype.onAfterRendering = function() { var sRenderMode = this.getRenderMode(); this._oIndicator = this.$().find(".sapMTokenizerIndicator"); if (Core.isThemeApplied()) { this._storeTokensSizes(); } // refresh the render mode (loose/narrow) based on whether an indicator should be shown // to ensure that the N-more label is rendered correctly this._useCollapsedMode(sRenderMode); this._registerResizeHandler(); if (sRenderMode === RenderMode.Loose) { this.scrollToEnd(); } }; /** * Called after a new theme is applied. * * @private */ Tokenizer.prototype.onThemeChanged = function() { this._storeTokensSizes(); this._useCollapsedMode(this.getRenderMode()); }; /** * Stores sizes of the tokens for layout calculations. * * @private */ Tokenizer.prototype._storeTokensSizes = function() { var aTokens = this.getTokens(); aTokens.forEach(function(oToken){ if (oToken.getDomRef() && !oToken.$().hasClass("sapMHiddenToken") && !oToken.getTruncated()) { this._oTokensWidthMap[oToken.getId()] = oToken.$().outerWidth(true); } }, this); }; /** * Handles the setting of collapsed state. * * @param {string} sRenderMode If true collapses the tokenizer's content * @private */ Tokenizer.prototype._useCollapsedMode = function(sRenderMode) { var aTokens = this._getVisibleTokens(); if (!aTokens.length) { this._setHiddenTokensCount(0); return; } if (sRenderMode === RenderMode.Narrow) { this._adjustTokensVisibility(); } else { this._setHiddenTokensCount(0); this._showAllTokens(); } }; /** * Handle the focus leave event, deselects token. * * @param {jQuery.Event} oEvent The occuring event * @private */ Tokenizer.prototype.onsapfocusleave = function(oEvent) { // when focus goes to token, keep the select status, otherwise deselect all tokens if (document.activeElement === this.getDomRef() || !this._checkFocus()) { this._changeAllTokensSelection(false); this._oSelectionOrigin = null; } }; Tokenizer.prototype.onsapbackspace = function (oEvent) { var aSelectedTokens = this.getSelectedTokens(); var oFocussedToken = this.getTokens().filter(function (oToken) { return oToken.getFocusDomRef() === document.activeElement; })[0]; var aDeletingTokens = aSelectedTokens.length ? aSelectedTokens : [oFocussedToken]; oEvent.preventDefault(); return this.fireTokenDelete({ tokens: aDeletingTokens, keyCode: oEvent.which }); }; Tokenizer.prototype.onsapdelete = Tokenizer.prototype.onsapbackspace; /** * Handle the key down event for Ctrl+ a , Ctrl+ c and Ctrl+ x. * * @param {jQuery.Event}oEvent The occuring event * @private */ Tokenizer.prototype.onkeydown = function(oEvent) { var bSelectAll; if (!this.getEnabled()) { return; } if (oEvent.which === KeyCodes.TAB) { this._changeAllTokensSelection(false); } // ctrl/meta + c OR ctrl/meta + A if ((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.A) { //to check how many tokens are selected before Ctrl + A in Tokenizer bSelectAll = this.getSelectedTokens().length < this._getVisibleTokens().length; if (this._getVisibleTokens().length > 0) { this.focus(); this._changeAllTokensSelection(bSelectAll); oEvent.preventDefault(); oEvent.stopPropagation(); } } // ctrl/meta + c OR ctrl/meta + Insert if ((oEvent.ctrlKey || oEvent.metaKey) && (oEvent.which === KeyCodes.C || oEvent.which === KeyCodes.INSERT)) { this._copy(); } // ctr/meta + x OR Shift + Delete if (((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.X) || (oEvent.shiftKey && oEvent.which === KeyCodes.DELETE)) { if (this.getEditable()) { this._cut(); } else { this._copy(); } } }; Tokenizer.prototype._shouldPreventModifier = function (oEvent) { var bShouldPreventOnMac = Device.os.macintosh && oEvent.metaKey; var bShouldPreventOnWindows = Device.os.windows && oEvent.altKey; return bShouldPreventOnMac || bShouldPreventOnWindows; }; /** * Pseudo event for pseudo 'previous' event with modifiers (Ctrl, Alt or Shift). * * @see #onsapprevious * @param {jQuery.Event} oEvent The event object * @private */ Tokenizer.prototype.onsappreviousmodifiers = function (oEvent) { if (!this._shouldPreventModifier(oEvent)) { this.onsapprevious(oEvent); } }; /** * Pseudo event for pseudo 'next' event with modifiers (Ctrl, Alt or Shift). * * @see #onsapnext * @param {jQuery.Event} oEvent The event object * @private */ Tokenizer.prototype.onsapnextmodifiers = function (oEvent) { if (!this._shouldPreventModifier(oEvent)) { this.onsapnext(oEvent); } }; /** * Pseudo event for keyboard Home with modifiers (Ctrl, Alt or Shift). * * @see #onsaphome * @param {jQuery.Event} oEvent The event object * @private */ Tokenizer.prototype.onsaphomemodifiers = function (oEvent) { this._selectRange(false); }; /** * Pseudo event for keyboard End with modifiers (Ctrl, Alt or Shift). * * @see #onsapend * @param {jQuery.Event} oEvent The event object * @private */ Tokenizer.prototype.onsapendmodifiers = function (oEvent) { this._selectRange(true); }; /** * Sets the selection over a range of tokens. * * @param {boolean} bForwardSection True, if the selection is onward * @private */ Tokenizer.prototype._selectRange = function (bForwardSection) { var oRange = {}, oTokens = this._getVisibleTokens(), oFocusedControl = Element.closestTo(document.activeElement), iTokenIndex = oTokens.indexOf(oFocusedControl); if (!oFocusedControl || !oFocusedControl.isA("sap.m.Token")) { return; } if (bForwardSection) { oRange.start = iTokenIndex; oRange.end = oTokens.length - 1; } else { oRange.start = 0; oRange.end = iTokenIndex; } if (oRange.start < oRange.end) { for (var i = oRange.start; i <= oRange.end; i++) { oTokens[i].setSelected(true); } } }; /** * Handles the copy event. * * @private */ Tokenizer.prototype._copy = function() { this._fillClipboard("copy"); }; Tokenizer.prototype._fillClipboard = function (sShortcutName) { var aSelectedTokens = this.getSelectedTokens(); var sTokensTexts = aSelectedTokens.map(function(oToken) { return oToken.getText(); }).join("\r\n"); /* fill clipboard with tokens' texts so parent can handle creation */ var cutToClipboard = function(oEvent) { if (oEvent.clipboardData) { oEvent.clipboardData.setData('text/plain', sTokensTexts); } else { oEvent.originalEvent.clipboardData.setData('text/plain', sTokensTexts); } oEvent.preventDefault(); }; document.addEventListener(sShortcutName, cutToClipboard); document.execCommand(sShortcutName); document.removeEventListener(sShortcutName, cutToClipboard); }; /** * Handles the cut event. * * @private */ Tokenizer.prototype._cut = function() { var aSelectedTokens = this.getSelectedTokens(); this._fillClipboard("cut"); // compatibility this.fireTokenChange({ type: Tokenizer.TokenChangeType.Removed, token: aSelectedTokens, tokens: aSelectedTokens, addedTokens: [], removedTokens: aSelectedTokens }); // compatibility this.fireTokenUpdate({ type: Tokenizer.TokenChangeType.Removed, addedTokens: [], removedTokens: aSelectedTokens }); this.fireTokenDelete({ tokens: aSelectedTokens }); }; /** * Adjusts the scrollLeft so that the given token is visible from its left side. * @param {sap.m.Token} oToken The token that will be fully visible * @private */ Tokenizer.prototype._ensureTokenVisible = function(oToken) { if (!oToken || !oToken.getDomRef() || !this.getDomRef()) { return; } var iTokenizerLeftOffset = this.$().offset().left, iTokenizerWidth = this.$().width(), iTokenLeftOffset = oToken.$().offset().left, bRTL = Core.getConfiguration().getRTL(), // Margins and borders are excluded from calculations therefore we need to add them explicitly. iTokenMargin = bRTL ? parseInt(oToken.$().css("margin-left")) : parseInt(oToken.$().css("margin-right")), iTokenBorder = parseInt(oToken.$().css("border-left-width")) + parseInt(oToken.$().css("border-right-width")), iTokenWidth = oToken.$().width() + iTokenMargin + iTokenBorder, iScrollLeft = bRTL ? this.$().scrollLeftRTL() : this.$().scrollLeft(), iLeftOffset = iScrollLeft - iTokenizerLeftOffset + iTokenLeftOffset, iRightOffset = iScrollLeft + (iTokenLeftOffset - iTokenizerLeftOffset + iTokenWidth - iTokenizerWidth); if (this._getVisibleTokens().indexOf(oToken) === 0) { this.$().scrollLeft(0); return; } if (iTokenLeftOffset < iTokenizerLeftOffset) { bRTL ? this.$().scrollLeftRTL(iLeftOffset) : this.$().scrollLeft(iLeftOffset); } if (iTokenLeftOffset - iTokenizerLeftOffset + iTokenWidth > iTokenizerWidth) { bRTL ? this.$().scrollLeftRTL(iRightOffset) : this.$().scrollLeft(iRightOffset); } }; Tokenizer.prototype.ontap = function (oEvent) { var bShiftKey = oEvent.shiftKey, bCtrlKey = (oEvent.ctrlKey || oEvent.metaKey), oTargetToken = oEvent.getMark("tokenTap"), bDeleteToken = oEvent.getMark("tokenDeletePress"), aTokens = this._getVisibleTokens(), oFocusedToken, iFocusIndex, iIndex, iMinIndex, iMaxIndex; if (bDeleteToken || !oTargetToken || (!bShiftKey && bCtrlKey)) { // Ctrl this._oSelectionOrigin = null; return; } if (!bShiftKey) { // Simple click/tap // simple select, neither ctrl nor shift key was pressed, deselects other tokens this._oSelectionOrigin = oTargetToken; this._changeAllTokensSelection(false, oTargetToken, true); } // Shift oFocusedToken = oTargetToken; if (this._oSelectionOrigin) { oFocusedToken = this._oSelectionOrigin; } else { this._oSelectionOrigin = oFocusedToken; } if (oTargetToken && this.hasOneTruncatedToken()) { this._handleNMoreIndicatorPress(); return; } iFocusIndex = this.indexOfToken(oFocusedToken); iIndex = this.indexOfToken(oTargetToken); iMinIndex = Math.min(iFocusIndex, iIndex); iMaxIndex = Math.max(iFocusIndex, iIndex); aTokens.forEach(function (oToken, i) { if (i >= iMinIndex && i <= iMaxIndex) { oToken.setSelected(true); } else if (!bCtrlKey) { oToken.setSelected(false); } }); }; /** * Called when the user presses the left arrow key, focuses previous token. * @param {jQuery.Event} oEvent The event triggered by the user * @private */ Tokenizer.prototype.onsapprevious = function(oEvent) { var aTokens = this._getVisibleTokens(), iLength = aTokens.length; if (iLength === 0) { return; } var oFocusedElement = Element.closestTo(document.activeElement); // oFocusedElement could be undefined since the focus element might not correspond to an SAPUI5 Control var index = oFocusedElement ? aTokens.indexOf(oFocusedElement) : -1; if (index === 0) { oEvent.setMarked("forwardFocusToParent"); // focus is on first token - we do not handle this event and let it bubble return; } var targetToken, currentToken; if (index > 0) { targetToken = aTokens[index - 1]; this._ensureTokenVisible(targetToken); targetToken.focus(); } else { targetToken = aTokens[aTokens.length - 1]; this._ensureTokenVisible(targetToken); // Prevent default scrolling in IE when last token is focused targetToken.focus({ preventScroll: true }); } if (oEvent.shiftKey) { currentToken = aTokens[index]; targetToken.setSelected(true); currentToken.setSelected(true); } // mark the event that it is handled by the control oEvent.setMarked(); oEvent.preventDefault(); }; /** * Called when the user presses the right arrow key, focuses next token. * @param {jQuery.Event} oEvent The event triggered by the user * @private */ Tokenizer.prototype.onsapnext = function(oEvent) { var aTokens = this._getVisibleTokens(), iLength = aTokens.length; if (iLength === 0) { return; } var oFocusedElement = Element.closestTo(document.activeElement); // oFocusedElement could be undefined since the focus element might not correspond to an SAPUI5 Control var index = oFocusedElement ? aTokens.indexOf(oFocusedElement) : -1; var oNextToken = aTokens[index + 1]; this._ensureTokenVisible(oNextToken); if (index < iLength - 1) { var currentToken = aTokens[index]; oNextToken.focus(); if (oEvent.shiftKey) { oNextToken.setSelected(true); currentToken.setSelected(true); } } else { // focus is on last token - we do not handle this event and let it bubble // notify the parent that the focus should be taken over oEvent.setMarked("forwardFocusToParent"); return; } // mark the event that it is handled by the control oEvent.setMarked(); oEvent.preventDefault(); }; /** * Function adds a validation callback called before any new token gets added to the tokens aggregation. * * @public * @param {function} fValidator The validation function * @deprecated As of version 1.81, replaced by {@link MultiInput.prototype.addValidator} */ Tokenizer.prototype.addValidator = function(fValidator) { Log.warning( "[Warning]:", "You are attempting to use deprecated method 'addValidator()', please use MultiInput.prototype.addValidator instead.", this); }; /** * Function removes a validation callback. * * @public * @param {function} fValidator The validation function * @deprecated As of version 1.81, replaced by {@link MultiInput.prototype.addValidator} */ Tokenizer.prototype.removeValidator = function(fValidator) { Log.warning( "[Warning]:", "You are attempting to use deprecated method 'addValidator()', please use MultiInput.prototype.addValidator instead.", this); }; /** * Function removes all validation callbacks * * @public * @deprecated As of version 1.81, replaced by {@link MultiInput.prototype.addValidator} */ Tokenizer.prototype.removeAllValidators = function() { Log.warning( "[Warning]:", "You are attempting to use deprecated method 'addValidator()', please use MultiInput.prototype.addValidator instead.", this); }; /** * Function validates the given text and adds a new token if validation was successful. * * @public * @param {object} oParameters Parameter bag containing the following fields: * @param {string} oParameters.text The source text {sap.m.Token} * @param {object} [oParameters.token] Suggested token * @param {object} [oParameters.suggestionObject] Any object used to find the suggested token * @param {function} [oParameters.validationCallback] Callback which gets called after validation has finished * @deprecated As of version 1.81, replaced by {@link MultiInput.prototype.addValidator} */ Tokenizer.prototype.addValidateToken = function(oParameters) { Log.warning( "[Warning]:", "You are attempting to use deprecated method 'addValidator()', please use MultiInput.prototype.addValidator instead.", this); }; /** * Function parses given text, and text is separated by line break. * * @private * @param {string} sString The texts that needs to be parsed * @returns {array} Array of string after parsing */ Tokenizer.prototype._parseString = function(sString) { // for the purpose to copy from column in excel and paste in MultiInput/MultiComboBox // delimiter is line break return sString.split(/\r\n|\r|\n/g); }; /** * Checks whether the Tokenizer or one of its internal DOM elements has the focus. * @returns {object} The control that has the focus * @private */ Tokenizer.prototype._checkFocus = function() { return this.getDomRef() && containsOrEquals(this.getDomRef(), document.activeElement); }; /** * Function selects all tokens. * * @public * @param {boolean} bSelect [optional] true for selecting, false for deselecting * @returns {this} this instance for method chaining */ Tokenizer.prototype.selectAllTokens = function(bSelect) { if (bSelect === undefined) { bSelect = true; } this._changeAllTokensSelection(bSelect); return this; }; /** * Function selects/deselects all tokens and fires the correct "select" or "deselect" events. * @param {boolean} bSelect Whether the tokens should be selected * @param {sap.m.Token} oTokenToSkip [optional] this token will be skipped when changing the selection * @param {boolean} bSkipClipboardSelect [optional] selecting the hidden cli div to enable copy to clipboard will be skipped * @private */ Tokenizer.prototype._changeAllTokensSelection = function (bSelect, oTokenToSkip, bSkipClipboardSelect) { var aTokens = this._getVisibleTokens(); aTokens .filter(function (oToken) { return oToken !== oTokenToSkip; }) .forEach(function (oToken) { oToken.setSelected(bSelect); }); if (!bSkipClipboardSelect) { this._doSelect(); } return this; }; /** * Function returns all currently selected tokens. * * @public * @returns {sap.m.Token[]} Array of selected tokens or empty array */ Tokenizer.prototype.getSelectedTokens = function () { return this._getVisibleTokens() .filter(function (oToken) { return oToken.getSelected(); }); }; /** * Handle the home button, scrolls to the first token. * * @param {jQuery.Event}oEvent The occuring event * @private */ Tokenizer.prototype.onsaphome = function(oEvent) { var aAvailableTokens = this.getTokens().filter(function (oToken) { return oToken.getDomRef() && !oToken.getDomRef().classList.contains("sapMHiddenToken"); }); aAvailableTokens.length && aAvailableTokens[0].focus(); this.scrollToStart(); oEvent.preventDefault(); }; /** * Handle the end button, scrolls to the last token and focuses it. * * @param {jQuery.Event} oEvent The occuring event * @private */ Tokenizer.prototype.onsapend = function(oEvent) { var oTokens = this._getVisibleTokens(), oLastToken = oTokens[oTokens.length - 1]; // handle the event chain only if the focus is not on the last token // otherwise let the focus be handled by the parent control if (oLastToken.getDomRef() !== document.activeElement) { oLastToken.focus(); this.scrollToEnd(); oEvent.stopPropagation(); } else { // notify the parent that the focus should be taken over oEvent.setMarked("forwardFocusToParent"); } oEvent.preventDefault(); }; /** * Method for handling the state for tabindex rendering * * @param {boolean} bShouldRenderTabIndex If tabindex should be rendered * @protected */ Tokenizer.prototype.setShouldRenderTabIndex = function (bShouldRenderTabIndex) { this._bShouldRenderTabIndex = bShouldRenderTabIndex; }; /** * Flag indicating if tabindex attribute should be rendered * * @returns {boolean} True if tabindex should be rendered and false if not * @protected */ Tokenizer.prototype.getEffectiveTabIndex = function () { return this._bShouldRenderTabIndex === null ? !!this.getTokens().length : this._bShouldRenderTabIndex; }; /** * Handle the focus event on the control. * * @param {jQuery.Event} oEvent The occuring event * @protected */ Tokenizer.prototype.onclick = function (oEvent) { var bFireIndicatorHandler; if (!this.getEnabled()) { return; } bFireIndicatorHandler = !this.hasStyleClass("sapMTokenizerIndicatorDisabled") && oEvent.target.classList.contains("sapMTokenizerIndicator"); if (bFireIndicatorHandler) { this._handleNMoreIndicatorPress(); } }; /** * Handles the touch start event on the control. * * @param {jQuery.Event} oEvent The event object. */ Tokenizer.prototype.ontouchstart = function(oEvent) { // needed when the control is inside active controls oEvent.setMarked(); // Workaround for chrome bug // BCP: 1680011538 if (Device.browser.chrome && window.getSelection()) { window.getSelection().removeAllRanges(); } }; /** * Function cleans up registered event handlers. * * @private */ Tokenizer.prototype.exit = function() { this._deregisterResizeHandler(); if (this._oTokensList) { this._oTokensList.destroy(); this._oTokensList = null; } if (this._oScroller) { this._oScroller.destroy(); this._oScroller = null; } if (this._oPopup) { this._oPopup.destroy(); this._oPopup = null; } this._oTokensWidthMap = null; this._oIndicator = null; this._aTokenValidators = null; this._bShouldRenderTabIndex = null; }; /** * Function deregisters event handlers. * * @private */ Tokenizer.prototype._deregisterResizeHandler = function(){ if (this._sResizeHandlerId) { ResizeHandler.deregister(this._sResizeHandlerId); delete this._sResizeHandlerId; } }; /** * Sets accessibility information about the tokens. * * @private */ Tokenizer.prototype._setTokensAria = function() { var iTokenCount = this._getVisibleTokens().length; var oInvisibleText; var sTokenizerAria = ""; var sTranslation = ""; var oTranslationMapping = { 0: "TOKENIZER_ARIA_NO_TOKENS", 1: "TOKENIZER_ARIA_CONTAIN_ONE_TOKEN" }; if (Core.getConfiguration().getAccessibility()) { oInvisibleText = this.getAggregation("_tokensInfo"); sTranslation = oTranslationMapping[iTokenCount] ? oTranslationMapping[iTokenCount] : "TOKENIZER_ARIA_CONTAIN_SEVERAL_TOKENS"; sTokenizerAria = oRb.getText(sTranslation, iTokenCount); oInvisibleText.setText(sTokenizerAria); } }; /** * Selects the hidden clip div to enable copy to clipboad. * * @private */ Tokenizer.prototype._doSelect = function(){ if (this._checkFocus() && this._bCopyToClipboardSupport) { var oFocusRef = document.activeElement; var oSelection = window.getSelection(); oSelection.removeAllRanges(); if (this.getSelectedTokens().length) { var oRange = document.createRange(); oRange.selectNodeContents(this.getDomRef("clip")); oSelection.addRange(oRange); } if (window.clipboardData && oFocusRef.id === this.getId() + "-clip" && this.getDomRef()) { this.getDomRef().focus(); } } }; /** * Sets the count of hidden tokens that will be used for the n-More indicator. * This also determines if the n-More indicator will be shown or not. * * @param {number} iCount The number of hidden tokens * @returns {this} this instance for method chaining * @private */ Tokenizer.prototype._setHiddenTokensCount = function (iCount) { iCount = this.validateProperty("hiddenTokensCount", iCount); return this.setProperty("hiddenTokensCount", iCount); }; /** * Gets the count of hidden tokens that will be used for the n-More indicator. * If the count is 0, there is no n-More indicator shown. * * @since 1.80 * @public * @returns {number} The number of hidden tokens */ Tokenizer.prototype.getHiddenTokensCount = function () { return this.getProperty("hiddenTokensCount"); }; /** * Gets the accessibility text aggregation id. * @returns {string} Returns the InvisibleText control id * @protected */ Tokenizer.prototype.getTokensInfoId = function() { return this.getAggregation("_tokensInfo").getId(); }; /** * Handles focus management after deletion of a token by pressing backspace. * @private */ Tokenizer.prototype._handleBackspace = function(iIndex, fnFallback) { var aTokens = this.getTokens(); if (aTokens[iIndex - 1]) { return aTokens[iIndex - 1].focus(); } return fnFallback(); }; /** * Handles focus management after deletion of a token by pressing delete. * @private */ Tokenizer.prototype._handleDelete = function (iIndex, fnFallback) { var aTokens = this.getTokens(); if (aTokens[iIndex + 1]) { return aTokens[iIndex + 1].focus(); } return fnFallback(); }; /** * Forwards focus to the last token or calls callback if no tokens are left. * * @private * @ui5-restricted sap.m.MultiComboBox, sap.m.MultiInput */ Tokenizer.prototype.focusToken = function (iIndex, oOptions, fnFallback) { var aTokens = this.getTokens(); var bKeyboard = oOptions.keyCode; var bBackspace = oOptions.keyCode === KeyCodes.BACKSPACE; if (aTokens.length === 0) { return; } if (!bKeyboard) { return; } if (bBackspace) { return this._handleBackspace(iIndex, fnFallback); } return this._handleDelete(iIndex, fnFallback); }; Tokenizer.TokenChangeType = { Added : "added", Removed : "removed", RemovedAll : "removedAll", TokensChanged : "tokensChanged" }; Tokenizer.TokenUpdateType = { Added : "added", Removed : "removed" }; return Tokenizer; });