@openui5/sap.m
Version:
OpenUI5 UI Library sap.m
1,743 lines (1,483 loc) • 66.7 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 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/base/i18n/Localization",
'sap/m/Button',
'sap/m/List',
'sap/m/StandardListItem',
'sap/m/ResponsivePopover',
"sap/ui/core/Core",
"sap/ui/core/ControlBehavior",
'sap/ui/core/Control',
'sap/ui/core/Element',
"sap/ui/core/Lib",
'sap/ui/core/delegate/ScrollEnablement',
"sap/ui/base/ManagedObjectObserver",
'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/Theming",
"sap/ui/core/theming/Parameters",
// jQuery Plugin "scrollLeftRTL"
"sap/ui/dom/jquery/scrollLeftRTL"
],
function(
library,
Localization,
Button,
List,
StandardListItem,
ResponsivePopover,
Core,
ControlBehavior,
Control,
Element,
Library,
ScrollEnablement,
ManagedObjectObserver,
Device,
InvisibleText,
ResizeHandler,
TokenizerRenderer,
containsOrEquals,
KeyCodes,
Log,
Theming,
Parameters
) {
"use strict";
var CSS_CLASS_NO_CONTENT_PADDING = "sapUiNoContentPadding";
var CSS_CLASS_TOKENIZER_POPUP = "sapMTokenizerTokensPopup";
var RenderMode = library.TokenizerRenderMode;
var PlacementType = library.PlacementType;
var ListMode = library.ListMode;
var ListType = library.ListType;
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 Тokenizer is a container for {@link sap.m.Token Tokens}. The tokenizer supports keyboard navigation and token selection. It also handles all actions associated with the tokens like adding, deleting, selecting and editing.
* <h3>Structure</h3>
* The tokenizer consists of two parts:
* - Tokens - displays the available tokens.
* - N-more indicator - contains the number of the remaining tokens that cannot be displayed due to the limited space.
* 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.
*
* @extends sap.ui.core.Control
* @implements sap.ui.core.ISemanticFormContent
*
* @borrows sap.ui.core.ISemanticFormContent.getFormFormattedValue as #getFormFormattedValue
* @borrows sap.ui.core.ISemanticFormContent.getFormValueProperty as #getFormValueProperty
* @borrows sap.ui.core.ISemanticFormContent.getFormObservingProperties as #getFormObservingProperties
* @borrows sap.ui.core.ISemanticFormContent.getFormRenderAsControl as #getFormRenderAsControl
* @author SAP SE
* @version 1.146.0
*
* @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 : {
interfaces : [
"sap.ui.core.IFormContent",
"sap.ui.core.ISemanticFormContent"
],
library : "sap.m",
properties : {
/**
* true if tokens shall be editable otherwise false
*/
editable : {type : "boolean", group : "Misc", defaultValue : true},
/**
* Defines if the Tokenizer is enabled
*/
enabled : {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>
*
* <b>Note</b>: Have in mind that the <code>renderMode</code> property is used internally by the Tokenizer and controls that use the Tokenizer. Therefore, modifying this property may alter the expected behavior of the control.
*/
renderMode: {type : "string", group : "Misc", defaultValue : RenderMode.Narrow},
/**
* 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"},
/**
* The ID of the opener of the tokens popup.
*/
opener: { type: "string", multiple: false, visibility: "hidden" },
/**
* The name property to be used in the HTML code for the tokenizer (e.g. for HTML forms that send data to the server via submit).
* @since 1.142.0
*/
name: { type: "string", group: "Misc", defaultValue: "" },
/**
* Determines whether the <code>Tokenizer</code> is in display only state.
*
* When set to <code>true</code>, the <code>Tokenizer</code> is not editable.
* This setting is used for forms in review mode.
*
* @since 1.142.0
*/
displayOnly : {type : "boolean", group : "Behavior", defaultValue : false},
/**
* Changed when tokens are changed. The value for sap.ui.core.ISemanticFormContent interface.
* Contains a comma-separated list of all token texts for form processing.
* @private
*/
_semanticFormValue: {type: "string", group: "Behavior", defaultValue: "", visibility: "hidden"},
/**
* Defines whether tokens are displayed on multiple lines.
* @experimental since 1.142
*/
multiLine: {type: "boolean", group: "Misc", defaultValue: false},
/**
* Defines whether "Clear All" button is present. Ensure `multiLine` is enabled, otherwise `showClearAll` will have no effect.
* @experimental since 1.142
*/
showClearAll: {type: "boolean", group: "Misc", defaultValue: false}
},
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" }
}
},
/**
* Fired when the render mode of the Tokenizer changes between <code>Narrow</code> (collapsed) and <code>Loose</code> (expanded).
* @public
* @since 1.133
*/
renderModeChange: {
parameters: {
/**
* The render mode of the Tokenizer.
*/
renderMode: { type: "string" }
}
}
}
},
renderer: TokenizerRenderer
});
var oRb = Library.getResourceBundleFor("sap.m");
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._bInForm = false;
this._bShouldRenderTabIndex = null;
this._iPopoverIndexToFocusAfterDelete = 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 (ControlBehavior.isAccessibilityEnabled()) {
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();
var aTokens = this._getVisibleTokens();
var iTokenIndex = aTokens.indexOf(oToken);
// Mark that we're in a deletion operation
this._bDeletionInProgress = true;
// Prevent _bFocusFirstToken from interfering with deletion focus
this._bFocusFirstToken = false;
this._fireCompatibilityEvents(oToken, aSelectedTokens);
this.fireEvent("tokenDelete", {
tokens: [oToken]
});
this._applyFocusAfterDeletion(iTokenIndex, aTokens.length, "click");
oEvent.cancelBubble();
}, this);
this._bThemeApplied = false;
this._handleThemeApplied = () => {
this._bThemeApplied = true;
Theming.detachApplied(this._handleThemeApplied);
};
Theming.attachApplied(this._handleThemeApplied);
this._observeTokens();
};
/**
* Calculates the proper index for focusing after deletion.
* For backspace: focus goes to previous token.
* For delete and click: focus goes to next token (or previous if at the end).
*
* @private
* @param {number} iDeletedIndex Index of the deleted item
* @param {number} iTotalLength Total length of items
* @param {string} sDeletionType Type of deletion: "click", "backspace", or "delete"
* @returns {number} Index to focus after deletion
*/
Tokenizer.prototype._calculateFocusIndexAfterDeletion = function(iDeletedIndex, iTotalLength, sDeletionType) {
// For Backspace: focus goes to previous token
if (sDeletionType === "backspace") {
return iDeletedIndex > 0 ? iDeletedIndex - 1 : 0;
}
// For Delete and Click: focus goes to next token (or previous if at the end)
if (iDeletedIndex < iTotalLength - 1) {
return iDeletedIndex; // Focus next item at same position
}
return iDeletedIndex - 1; // At the end - focus previous item
};
/**
* Ensures the focus index is within valid bounds of the items array.
*
* @private
* @param {number} iIndex The calculated index
* @param {array} aItems The array of tokens (sap.m.Token) or list items (sap.m.StandardListItem)
* @returns {object|null} The token or list item to focus, or null if array is empty
*/
Tokenizer.prototype._getItemToFocusWithinBounds = function(iIndex, aItems) {
if (aItems.length === 0) {
return null;
}
var iIndexToFocus = iIndex;
// Ensure index is within bounds
if (iIndexToFocus < 0) {
iIndexToFocus = 0;
} else if (iIndexToFocus >= aItems.length) {
iIndexToFocus = aItems.length - 1;
}
return aItems[iIndexToFocus];
};
/**
* Calculates and applies focus to the correct token after deletion.
* Uses setTimeout to ensure DOM is updated before focusing.
*
* @private
* @param {number} iDeletedIndex Index of the deleted token
* @param {number} iTotalLength Total number of tokens before deletion
* @param {string} sDeletionType Type of deletion: "click", "backspace", or "delete"
*/
Tokenizer.prototype._applyFocusAfterDeletion = function(iDeletedIndex, iTotalLength, sDeletionType) {
setTimeout(function() {
// Calculate the focus index
var iCalculatedIndex = this._calculateFocusIndexAfterDeletion(iDeletedIndex, iTotalLength, sDeletionType);
var aRemainingTokens = this._getVisibleTokens();
var oTokenToFocus = this._getItemToFocusWithinBounds(iCalculatedIndex, aRemainingTokens);
if (oTokenToFocus && oTokenToFocus.getDomRef()) {
oTokenToFocus.focus();
}
// Clear the deletion flag
this._bDeletionInProgress = false;
}.bind(this), 0);
};
/**
* 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._bIsOpenedByNMoreIndicator = true;
this._togglePopup(this.getTokensPopup(), this._getEffectiveOpener());
};
Tokenizer.prototype._getEffectiveOpener = function() {
return document.getElementById(this.getProperty("opener")) || this.getDomRef();
};
/**
* 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)
.attachItemPress(this._handleListItemPress, this);
}
return this._oTokensList;
};
Tokenizer.prototype.getFormFormattedValue = function () {
this._bInForm = true;
const sTokens = this.getTokens()
.map(function (oToken) { return oToken.getText(); })
.join(", ");
// show en-dash in form display mode when empty
if (!sTokens) {
return "\u2013";
}
return sTokens;
};
Tokenizer.prototype.getFormValueProperty = function () {
return "_semanticFormValue";
};
Tokenizer.prototype.getFormObservingProperties = function() {
return ["_semanticFormValue"];
};
Tokenizer.prototype.getFormRenderAsControl = function () {
// if there are no tokens, we render the tokenizer as en-dash and not as a control
if (this.getDisplayOnly() && !this.getTokens().length) {
return false;
} else {
return this.getDisplayOnly();
}
};
/**
* ISemanticFormContent interface works only with properties. The state of Tokenizer is kept as Tokens.
* Update _semanticFormValue property so it'd match Tokenizer's state, but as a string which could be reused.
*
* @private
*/
Tokenizer.prototype.updateFormValueProperty = function () {
this.setProperty("_semanticFormValue", this.getFormFormattedValue(), true);
};
/**
* Changes list mode.
*
* @param sMode {sap.m.ListMode}
* @private
*/
Tokenizer.prototype._setPopoverMode = function (sMode) {
var oPopover = this.getTokensPopup();
var oSettings = {
showArrow: true,
placement: PlacementType.VerticalPreferredBottom
};
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) {
var iIndexToFocus = this._iPopoverIndexToFocusAfterDelete;
var bShouldRestoreFocus = iIndexToFocus !== undefined && iIndexToFocus !== null && this.getTokensPopup().isOpen();
// Clear the stored index before rebuilding
if (bShouldRestoreFocus) {
this._iPopoverIndexToFocusAfterDelete = null;
}
oList.destroyItems();
fnFilter = fnFilter ? fnFilter : function () { return true; };
this.getTokens()
.filter(fnFilter)
.forEach(function (oToken) {
oList.addItem(this._mapTokenToListItem(oToken));
}, this);
// Restore focus to the appropriate list item after deletion
if (bShouldRestoreFocus) {
var fnRestoreFocus = function() {
oList.removeEventDelegate(this._oFocusRestoreDelegate);
delete this._oFocusRestoreDelegate;
var aListItems = oList.getItems();
var oItemToFocus = this._getItemToFocusWithinBounds(iIndexToFocus, aListItems);
if (oItemToFocus && oItemToFocus.getDomRef()) {
oItemToFocus.focus();
}
}.bind(this);
// Remove any existing delegate
if (this._oFocusRestoreDelegate) {
oList.removeEventDelegate(this._oFocusRestoreDelegate);
}
// Use event delegate to hook into the List's rendering cycle
this._oFocusRestoreDelegate = {
onAfterRendering: fnRestoreFocus
};
oList.addEventDelegate(this._oFocusRestoreDelegate);
}
};
/**
* 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;
var oTokensList = this._getTokensList();
var aListItems = oTokensList.getItems();
var iDeletedIndex = aListItems.indexOf(oListItem);
oTokenToDelete = this.getTokens().filter(function(oToken){
return (oToken.getId() === sSelectedId) && oToken.getEditable();
})[0];
if (oTokenToDelete) {
// Calculate focus index using helper function
this._iPopoverIndexToFocusAfterDelete = this._calculateFocusIndexAfterDeletion(iDeletedIndex, aListItems.length, "click");
this.fireTokenUpdate({
addedTokens: [],
removedTokens: [oTokenToDelete],
type: Tokenizer.TokenUpdateType.Removed
});
this.fireTokenDelete({
tokens: [oTokenToDelete]
});
this._adjustTokensVisibility();
}
};
/**
* Handles token press from the List.
*
* @param {sap.ui.base.Event} oEvent object
* @private
*/
Tokenizer.prototype._handleListItemPress = function (oEvent) {
var oListItem = oEvent.getParameter("listItem");
var sSelectedId = oListItem && oListItem.data("tokenId");
var oPressedToken = this.getTokens().filter(function(oToken){
return (oToken.getId() === sSelectedId);
})[0];
if (oPressedToken) {
oPressedToken.firePress();
}
};
Tokenizer.prototype.focusFirstTokenItem = function () {
const oTokenList = this._getTokensList();
const aTokenListItems = oTokenList.getItems();
if (aTokenListItems.length) {
aTokenListItems[0].focus();
}
};
Tokenizer.prototype._handleTokenizerAfterOpen = function (oEvent) {
this.focusFirstTokenItem();
this.setRenderMode(RenderMode.Loose);
this.fireRenderModeChange({
renderMode: RenderMode.Loose
});
};
Tokenizer.prototype.addPopupClasses = function(oPopup) {
if (oPopup.hasStyleClass(CSS_CLASS_TOKENIZER_POPUP)) {
return;
}
oPopup.addStyleClass(CSS_CLASS_TOKENIZER_POPUP);
oPopup.addStyleClass(CSS_CLASS_NO_CONTENT_PADDING);
};
/**
* Returns N-More Popover/Dialog.
*
* @private
* @ui5-restricted sap.m.MultiInput, sap.m.MultiComboBox
* @returns {sap.m.ResponsivePopover} The popover containing the tokens
*/
Tokenizer.prototype.getTokensPopup = function () {
var oTokenList = this._getTokensList();
if (this._oPopup) {
return this._oPopup;
}
this._oPopup = new ResponsivePopover({
showArrow: true,
showCloseButton: 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(oTokenList);
}
this._fillTokensList(oTokenList);
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)
.attachAfterClose(this.afterPopupClose, this)
.attachAfterOpen(this._handleTokenizerAfterOpen, this);
this.addDependent(this._oPopup);
this.addPopupClasses(this._oPopup);
if (Device.system.phone) {
this._oPopup.setBeginButton(new Button({
text: oRb.getText("SUGGESTIONSPOPOVER_CLOSE_BUTTON"),
type: ButtonType.Emphasized,
press: function () {
this._oPopup.close();
}.bind(this)
}));
this._oPopup.setEndButton(new Button({
text: oRb.getText("TOKENIZER_CANCEL_BUTTON"),
press: function () {
this._oPopup.close();
}.bind(this)
}));
}
return this._oPopup;
};
/**
* Function to execute after the n-more popover is closed.
*
* @protected
*/
Tokenizer.prototype.afterPopupClose = function () {
if (!this.checkFocus()) {
this.setRenderMode(RenderMode.Narrow);
this.fireRenderModeChange({
renderMode: "Narrow"
});
}
};
Tokenizer.prototype._getDialogTitle = function () {
var oResourceBundle = Library.getResourceBundleFor("sap.m");
var aLabeles = this.getAriaLabelledBy().map(function(sLabelID) {
return Element.getElementById(sLabelID);
});
return aLabeles.length ? aLabeles[0].getText?.() : oResourceBundle.getText("TOKENIZER_MOBILE_DIALOG_TITLE");
};
/**
* Toggles the popover.
*
* @private
* @ui5-restricted sap.m.MultiInput, sap.m.MultiComboBox
*/
Tokenizer.prototype._togglePopup = function () {
var oOpenBy = this._getEffectiveOpener(),
oPopover = this.getTokensPopup(),
oPopoverIsOpen = oPopover.isOpen(),
bEditable = this.getEditable();
this._setPopoverMode(bEditable ? ListMode.Delete : ListMode.None);
if (oPopoverIsOpen) {
oPopover.close();
} else {
oPopover.openBy(oOpenBy);
}
};
/**
* 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,
type: ListType.Active,
wrapCharLimit: 10000
}).data("tokenId", oToken.getId());
var fnOnSapShowHide = function (oEvent) {
this._togglePopup(this.getTokensPopup());
if (this.getTokens().length && this._bIsOpenedByNMoreIndicator) {
this.getTokens()[0].focus();
this._bIsOpenedByNMoreIndicator = false;
}
};
oListItem.setTitle(oToken.getText());
oListItem.addDelegate({
onkeydown: function (oEvent) {
oEvent.preventDefault();
if (!((oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.I) && oEvent.which !== KeyCodes.ESCAPE) {
return;
}
this._togglePopup(this.getTokensPopup());
if (this.getTokens().length && this._bIsOpenedByNMoreIndicator) {
this.getTokens()[0].focus();
this._bIsOpenedByNMoreIndicator = false;
}
},
onsapshow: fnOnSapShowHide,
onsaphide: fnOnSapShowHide
}, this);
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("inner") || 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(),
iTokensCount = aTokens.length,
iLabelWidth, iFreeSpace,
iCounter, iFirstTokenToHide = -1;
if (this.getMultiLine()) {
aTokens.forEach(function (oToken) {
const iTokenWidth = iTokenizerWidth - this._oTokensWidthMap[oToken.getId()];
if (iTokenWidth <= 0) {
oToken.setTruncated(true);
}
}, this);
return;
}
// 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;
return false;
}
}, 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");
}
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) {
let 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 = Localization.getRTL(),
bIsFirstTokenFocused = this.getTokens()[0] && this.getTokens()[0].getDomRef() && this.getTokens()[0].getDomRef() === document.activeElement,
iScrollWidth,
scrollDiv;
if (!this.getDomRef() || bIsFirstTokenFocused) {
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, this._handleResize.bind(this));
}
};
Tokenizer.prototype._handleResize = function(){
this._useCollapsedMode(this.getRenderMode());
};
/**
* 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.getMultiLine()) {
this.setFirstTokenTruncated(false);
}
aTokens.forEach(function(oToken, iIndex) {
oToken.setProperty("editableParent", this.getEditable());
oToken.setProperty("enabledParent", this.getEnabled());
oToken.setProperty("posinset", iIndex + 1);
oToken.setProperty("setsize", aTokens.length);
}, this);
this._setTokensAria();
if (this.getTokensPopup() && this.getTokensPopup().isOpen() ) {
const nTokensLength = this._getTokensList().getItems().length;
// If tokens were deleted or added - update the popover list
if (aTokens.length !== nTokensLength){
this._fillTokensList(this._getTokensList());
}
}
};
/**
* Called after the control is rendered.
*
* @private
*/
Tokenizer.prototype.onAfterRendering = function() {
var sRenderMode = this.getRenderMode();
this._oIndicator = this.$().find(".sapMTokenizerIndicator");
if (this._bThemeApplied) {
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 (!this.getEnabled() && this.getTokens().length) {
this.getTokens().forEach(function(oToken) {
if (!oToken.getDomRef()) {
return;
}
oToken.getDomRef().setAttribute("tabindex", "-1");
});
}
var oFocusableToken = this._getTokenToFocus();
if (oFocusableToken && oFocusableToken.getDomRef() && this.getEffectiveTabIndex()) {
oFocusableToken.getDomRef().setAttribute("tabindex", "0");
}
if (this._bFocusFirstToken && !this._bDeletionInProgress) {
this.scrollToStart();
this._bFocusFirstToken = false;
return;
}
if (sRenderMode === RenderMode.Loose && this._nMoreIndicatorPressed) {
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);
};
/**
* Returns the first selected visible token or if there is none - the first visible token.
*
* @private
*/
Tokenizer.prototype._getTokenToFocus = function() {
var aVisibleTokens = this._getVisibleTokens();
return aVisibleTokens.find((oToken) => oToken.getSelected()) || aVisibleTokens[0];
};
/**
* 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.Loose) {
this._setHiddenTokensCount(0);
this._showAllTokens();
return;
}
this._adjustTokensVisibility();
};
/**
* 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._oSelectionOrigin = null;
}
if (!this.checkFocus() && !this._bDeletionInProgress) {
this._bFocusFirstToken = true;
this.setRenderMode(RenderMode.Narrow);
this.fireRenderModeChange({
renderMode: "Narrow"
});
}
};
/**
* Handles keyboard deletion with unified focus calculation.
*
* @private
* @param {jQuery.Event} oEvent The keyboard event
* @param {string} sDeletionType Type of deletion: "backspace" or "delete"
*/
Tokenizer.prototype._handleKeyboardDeletion = function(oEvent, sDeletionType) {
var oFocusedToken = this.getTokens().filter(function (oToken) {
return oToken.getFocusDomRef() === document.activeElement;
})[0];
if (!oFocusedToken) {
return;
}
var aDeletingTokens = this.getSelectedTokens().length ? this.getSelectedTokens() : [oFocusedToken];
var aTokens = this._getVisibleTokens();
// This ensures focus goes to the correct position
var iDeletingIndex = Math.min.apply(null, aDeletingTokens.map(function(oToken) {
return aTokens.indexOf(oToken);
}));
// Mark that we're in a deletion operation
this._bDeletionInProgress = true;
// Prevent _bFocusFirstToken from interfering with deletion focus
this._bFocusFirstToken = false;
oEvent.preventDefault();
this.fireTokenDelete({
tokens: aDeletingTokens,
keyCode: oEvent.which
});
this._applyFocusAfterDeletion(iDeletingIndex, aTokens.length, sDeletionType);
};
Tokenizer.prototype.onsapbackspace = function (oEvent) {
return this._handleKeyboardDeletion(oEvent, "backspace");
};
Tokenizer.prototype.onsapdelete = function (oEvent) {
return this._handleKeyboardDeletion(oEvent, "delete");
};
/**
* 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;
var bShouldOpenPopover = (oEvent.ctrlKey || oEvent.metaKey) && oEvent.which === KeyCodes.I;
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();
}
}
if (bShouldOpenPopover) {
oEvent.preventDefault();
oEvent.stopPropagation();
this._togglePopup(this.getTokensPopup());
return;
}
};
Tokenizer.prototype.onsaphide = function(oEvent) {
this._togglePopup(this.getTokensPopup());
};
Tokenizer.prototype.onsapshow = Tokenizer.prototype.onsaphide;
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);
}
};
Tokenizer.prototype._observeTokens = function () {
var oTokensObserver = new ManagedObjectObserver(function(oChange) {
var sMutation = oChange.mutation;
var oItem = oChange.child;
switch (sMutation) {
case "insert":
// invalidate tokens to recalculate the sizes
oItem.attachEvent("_change", this.invalidate, this);
break;
case "remove":
oItem.detachEvent("_change", this.invalidate, this);
break;
default:
break;
}
this.updateFormValueProperty();
this.invalidate();
}.bind(this));
oTokensObserver.observe(this, {
aggregations: ["tokens"]
});
};
/**
* 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);
this.scrollToStart();
};
/**
* Pseudo event for keyboard Page Up with modifiers (Ctrl, Alt or Shift).
*
* @see #onsaphome
* @param {jQuery.Event} oEvent The event object
* @private
*/
Tokenizer.prototype.onsappageupmodifiers = function (oEvent) {
this._selectRange(false);
this.scrollToStart();
};
/**
* 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);
this.scrollToEnd();
};
/**
* Pseudo event for keyboard Page Down with modifiers (Ctrl, Alt or Shift).
*
* @see #onsapend
* @param {jQuery.Event} oEvent The event object
* @private
*/
Tokenizer.prototype.onsappagedownmodifiers = function (oEvent) {
this._selectRange(true);
this.scrollToEnd();
};
/**
* 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");
// Async clipboard API (works in secure contexts - HTTPS/localhost)
if (navigator.clipboard?.writeText && window.isSecureContext) {
navigator.clipboard.writeText(sTokensTexts);
return;
}
/* 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 = Localization.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.onfocusin = function (oEvent) {
this.setRenderMode(RenderMode.Loose);
this.fireRenderModeChange({
renderMode: "Loose"
});
const oFirstToken = this.getTokens()[0];
// Don't set _bFocusFirstToken if we're handling a deletion
if (!this._bDeletionInProgress) {
this._bFocusFirstToken = oEvent.srcControl === oFirstToken;
}
if (!this._bFocusFirstToken && !this._bTokenToBeDeleted) {
this._ensureTokenVisible(oEvent.srcControl);
}
};
Tokenizer.prototype.onmousedown = function (oEvent) {
this._bTokenToBeDeleted = oEvent.target.matches(".sapMTokenIcon, .sapMTokenIcon *");
};
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;
// Close popover if it's open and user clicks on a token in tokenizer
if (oTargetToken && this._oPopup && this._oPopup.isOpen()) {
this._oPopup.close();
}
if (bDeleteToken || !oTargetToken || (!bShiftKey && bCtrlKey)) { // Ctrl
this._oSelectionOrigin = null;
return;
}
if (!bShiftKey) { // Simple click/tap
// simple select, neither ctr