UNPKG

devextreme

Version:

JavaScript/TypeScript Component Suite for Responsive Web Development

1,320 lines (1,319 loc) • 57.2 kB
/** * DevExtreme (esm/__internal/ui/tag_box.js) * Version: 26.1.4 * Build date: Fri Jul 24 2026 * * Copyright (c) 2012 - 2026 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { name as clickEvent } from "../../common/core/events/click"; import eventsEngine from "../../common/core/events/core/events_engine"; import { addNamespace, isCommandKeyPressed, normalizeKeyName } from "../../common/core/events/utils"; import messageLocalization from "../../common/core/localization/message"; import { normalizeLoadResult } from "../../common/data/data_source/utils"; import registerComponent from "../../core/component_registrator"; import devices from "../../core/devices"; import { getPublicElement } from "../../core/element"; import { data as elementData } from "../../core/element_data"; import Guid from "../../core/guid"; import $ from "../../core/renderer"; import { BindableTemplate } from "../../core/templates/bindable_template"; import { getIntersection, removeDuplicates } from "../../core/utils/array"; import { ensureDefined, equalByValue } from "../../core/utils/common"; import { Deferred, when } from "../../core/utils/deferred"; import { createTextElementHiddenCopy } from "../../core/utils/dom"; import { extend } from "../../core/utils/extend"; import { SelectionFilterCreator as FilterCreator } from "../../core/utils/selection_filter"; import { getHeight, getOuterWidth } from "../../core/utils/size"; import { isDefined, isObject, isString } from "../../core/utils/type"; import { hasWindow } from "../../core/utils/window"; import errors from "../../ui/widget/ui.errors"; import SelectBox from "../ui/select_box"; import caret from "../ui/text_box/utils.caret"; import { allowScroll } from "../ui/text_box/utils.scroll"; function xor(a, b) { return (a || b) && !(a && b) } const TAGBOX_TAG_DATA_KEY = "dxTagData"; const TAGBOX_TAG_DISPLAY_VALUE = "dxTagDisplayValue"; const TAGBOX_CLASS = "dx-tagbox"; const TAGBOX_TAG_CONTAINER_CLASS = "dx-tag-container"; const TAGBOX_TAG_CLASS = "dx-tag"; const TAGBOX_MULTI_TAG_CLASS = "dx-tagbox-multi-tag"; const TAGBOX_TAG_REMOVE_BUTTON_CLASS = "dx-tag-remove-button"; const TAGBOX_ONLY_SELECT_CLASS = "dx-tagbox-only-select"; const TAGBOX_SINGLE_LINE_CLASS = "dx-tagbox-single-line"; const TAGBOX_POPUP_WRAPPER_CLASS = "dx-tagbox-popup-wrapper"; const TAGBOX_TAG_CONTENT_CLASS = "dx-tag-content"; const TAGBOX_DEFAULT_FIELD_TEMPLATE_CLASS = "dx-tagbox-default-template"; const TAGBOX_CUSTOM_FIELD_TEMPLATE_CLASS = "dx-tagbox-custom-template"; const TEXTEDITOR_INPUT_CONTAINER_CLASS = "dx-texteditor-input-container"; const TAGBOX_MOUSE_WHEEL_DELTA_MULTIPLIER = -.3; class TagBox extends SelectBox { _supportedKeys() { const parent = super._supportedKeys(); const sendToList = options => { var _this$_list; if (!options) { return } null === (_this$_list = this._list) || void 0 === _this$_list || _this$_list._keyboardHandler(options) }; const { rtlEnabled: rtlEnabled } = this.option(); return Object.assign({}, parent, { backspace: e => { var _this$_tagElements; if (!this._isCaretAtTheStart()) { return } this._processKeyboardEvent(e); this._isTagRemoved = true; const $tagToDelete = this._$focusedTag || (null === (_this$_tagElements = this._tagElements()) || void 0 === _this$_tagElements ? void 0 : _this$_tagElements.last()); if (this._$focusedTag) { this._moveTagFocus("prev", true) } if (!$tagToDelete || 0 === $tagToDelete.length) { return } this._preserveFocusedTag = true; this._removeTagElement($tagToDelete); delete this._preserveFocusedTag }, upArrow: (e, options) => e.altKey || !this._list ? parent.upArrow.call(this, e) : sendToList(options), downArrow: (e, options) => e.altKey || !this._list ? parent.downArrow.call(this, e) : sendToList(options), del: e => { if (!this._$focusedTag || !this._isCaretAtTheStart()) { return } this._processKeyboardEvent(e); this._isTagRemoved = true; const $tagToDelete = this._$focusedTag; this._moveTagFocus("next", true); this._preserveFocusedTag = true; this._removeTagElement($tagToDelete); delete this._preserveFocusedTag }, enter: (e, options) => { var _this$_list2; const { opened: opened, acceptCustomValue: acceptCustomValue } = this.option(); const isListItemFocused = null !== (null === (_this$_list2 = this._list) || void 0 === _this$_list2 ? void 0 : _this$_list2.option("focusedElement")) && opened; const isCustomItem = acceptCustomValue && !isListItemFocused; if (isCustomItem) { e.preventDefault(); if ("" !== this._searchValue()) { this._customItemAddedHandler(e) } return } if (opened) { this._saveValueChangeEvent(e); sendToList(options); e.preventDefault() } }, space: (e, options) => { const { opened: opened } = this.option(); const isInputActive = this._shouldRenderSearchEvent(); if (opened && !isInputActive) { this._saveValueChangeEvent(e); sendToList(options); e.preventDefault() } }, leftArrow: e => { if (!this._isCaretAtTheStart() || this._isEmpty() || this._isEditable() && rtlEnabled && !this._$focusedTag) { return } e.preventDefault(); const direction = rtlEnabled ? "next" : "prev"; this._moveTagFocus(direction); const { multiline: multiline } = this.option(); if (!multiline) { this._scrollContainer(direction) } }, rightArrow: e => { if (!this._isCaretAtTheStart() || this._isEmpty() || this._isEditable() && !rtlEnabled && !this._$focusedTag) { return } e.preventDefault(); const direction = rtlEnabled ? "prev" : "next"; this._moveTagFocus(direction); const { multiline: multiline } = this.option(); if (!multiline) { this._scrollContainer(direction) } } }) } _processKeyboardEvent(e) { e.preventDefault(); e.stopPropagation(); this._saveValueChangeEvent(e) } _isEmpty() { return 0 === this._getValue().length } _updateTagsContainer($element) { if (!$element) { return } this._$tagsContainer = $element.addClass("dx-tag-container") } _allowSelectItemByTab() { return false } _isCaretAtTheStart() { const position = caret(this._input()); return 0 === (null === position || void 0 === position ? void 0 : position.start) && 0 === position.end } _updateInputAriaActiveDescendant(id) { this.setAria("activedescendant", id, this._input()) } _moveTagFocus(direction, clearOnBoundary) { if (!this._$focusedTag) { var _this$_$focusedTag; const tagElements = this._tagElements(); this._$focusedTag = "next" === direction ? null === tagElements || void 0 === tagElements ? void 0 : tagElements.first() : null === tagElements || void 0 === tagElements ? void 0 : tagElements.last(); this._toggleFocusClass(true, this._$focusedTag); this._updateInputAriaActiveDescendant(null === (_this$_$focusedTag = this._$focusedTag) || void 0 === _this$_$focusedTag ? void 0 : _this$_$focusedTag.attr("id")); return } const $nextFocusedTag = this._$focusedTag[direction](".dx-tag"); if ($nextFocusedTag.length > 0) { this._replaceFocusedTag($nextFocusedTag); this._updateInputAriaActiveDescendant($nextFocusedTag.attr("id")) } else if (clearOnBoundary || "next" === direction && this._isEditable()) { this._clearTagFocus(); this._updateInputAriaActiveDescendant() } } _replaceFocusedTag($nextFocusedTag) { this._toggleFocusClass(false, this._$focusedTag); this._$focusedTag = $nextFocusedTag; this._toggleFocusClass(true, this._$focusedTag) } _clearTagFocus() { if (!this._$focusedTag) { return } this._toggleFocusClass(false, this._$focusedTag); this._updateInputAriaActiveDescendant(); this._$focusedTag = void 0 } _focusClassTarget($element) { if (null !== $element && void 0 !== $element && $element.length && $element[0] !== this._focusTarget()[0]) { return $element } return super._focusClassTarget() } _getLabelContainer() { return this._$tagsContainer ?? super._getLabelContainer() } _getFieldElement() { return this._input() } _scrollContainer(direction) { const { multiline: multiline } = this.option(); if (multiline || !hasWindow()) { return } if (!this._$tagsContainer) { return } const scrollPosition = this._getScrollPosition(direction); this._$tagsContainer.scrollLeft(scrollPosition) } _getScrollPosition(direction) { if ("start" === direction || "end" === direction) { return this._getBorderPosition(direction) } return this._$focusedTag ? this._getFocusedTagPosition(direction) : this._getBorderPosition("end") } _getBorderPosition(direction) { if (!this._$tagsContainer) { return 0 } const { rtlEnabled: rtlEnabled } = this.option(); const isScrollLeft = xor("end" === direction, Boolean(rtlEnabled)); const scrollSign = rtlEnabled ? -1 : 1; const containerScrollWidth = this._$tagsContainer.get(0).scrollWidth; return xor(isScrollLeft, !rtlEnabled) ? 0 : scrollSign * (containerScrollWidth - getOuterWidth(this._$tagsContainer)) } _getFocusedTagPosition(direction) { var _this$_$focusedTag2; if (!this._$tagsContainer) { return 0 } const { rtlEnabled: rtlEnabled } = this.option(); const isScrollLeft = xor("next" === direction, Boolean(rtlEnabled)); let { left: scrollOffset = 0 } = (null === (_this$_$focusedTag2 = this._$focusedTag) || void 0 === _this$_$focusedTag2 ? void 0 : _this$_$focusedTag2.position()) || {}; let scrollLeft = this._$tagsContainer.scrollLeft(); if (isScrollLeft) { scrollOffset += getOuterWidth(this._$focusedTag, true) - getOuterWidth(this._$tagsContainer) } if (xor(isScrollLeft, scrollOffset < 0)) { scrollLeft += scrollOffset } return scrollLeft } _setNextValue() {} _getDefaultOptions() { return extend(super._getDefaultOptions(), { value: [], showDropDownButton: false, maxFilterQueryLength: 1500, tagTemplate: "tag", selectAllText: messageLocalization.format("dxList-selectAll"), hideSelectedItems: false, selectedItems: [], selectAllMode: "page", onSelectAllValueChanged: void 0, maxDisplayedTags: void 0, showMultiTagOnly: true, onMultiTagPreparing: void 0, multiline: true, useSubmitBehavior: true }) } _init() { super._init(); this._selectedItems = []; this._initSelectAllValueChangedAction() } _initActions() { super._initActions(); this._initMultiTagPreparingAction() } _initMultiTagPreparingAction() { this._multiTagPreparingAction = this._createActionByOption("onMultiTagPreparing", { beforeExecute: e => { this._multiTagPreparingHandler(e.args[0]) }, excludeValidators: ["disabled", "readOnly"] }) } _multiTagPreparingHandler(args) { const { length: selectedCount } = this._getValue(); const { showMultiTagOnly: showMultiTagOnly, maxDisplayedTags: maxDisplayedTags } = this.option(); if (!showMultiTagOnly) { args.text = messageLocalization.getFormatter("dxTagBox-moreSelected")(selectedCount - maxDisplayedTags + 1) } else { args.text = messageLocalization.getFormatter("dxTagBox-selected")(selectedCount) } } _initDynamicTemplates() { super._initDynamicTemplates(); this._templateManager.addDefaultTemplates({ tag: new BindableTemplate(($container, data) => { const $tagContent = $("<div>").addClass("dx-tag-content"); $("<span>").text(data.text ?? data).appendTo($tagContent); $("<div>").addClass("dx-tag-remove-button").appendTo($tagContent); $container.append($tagContent) }, ["text"], this.option("integrationOptions.watchMethod"), { text: this._displayGetter }) }) } _toggleSubmitElement(enabled) { if (enabled) { this._renderSubmitElement(); this._setSubmitValue() } else { if (this._$submitElement) { this._$submitElement.remove() } delete this._$submitElement } } _renderSubmitElement() { const { useSubmitBehavior: useSubmitBehavior } = this.option(); if (!useSubmitBehavior) { return } this._$submitElement = $("<select>").attr({ multiple: "multiple", "aria-label": "Selected items" }).css("display", "none").appendTo(this.$element()) } _setSubmitValue() { const { useSubmitBehavior: useSubmitBehavior } = this.option(); if (!useSubmitBehavior) { return } const value = this._getValue(); const $options = value.map(item => { const useDisplayText = this._shouldUseDisplayValue(item); return $("<option>").val(useDisplayText ? this._displayGetter(item) : item).attr("selected", "selected") }); this._getSubmitElement().empty().append($options) } _initMarkup() { this._tagElementsCache = $(); const { multiline: multiline, searchEnabled: searchEnabled, acceptCustomValue: acceptCustomValue } = this.option(); const isSingleLineMode = !multiline; this.$element().addClass("dx-tagbox").toggleClass("dx-tagbox-only-select", !(searchEnabled || acceptCustomValue)).toggleClass("dx-tagbox-single-line", isSingleLineMode); const elementAria = { role: "application", roledescription: messageLocalization.format("dxTagBox-ariaRoleDescription") }; this.setAria(elementAria, this.$element()); this._initTagTemplate(); super._initMarkup() } _getNewLabelId(actualId, newId, shouldRemove) { if (!actualId) { return newId } if (shouldRemove) { if (actualId === newId) { return } return actualId.split(" ").filter(id => id !== newId).join(" ") } return `${actualId} ${newId}` } _updateElementAria(id, shouldRemove) { const shouldClearLabel = !id; if (shouldClearLabel) { this.setAria("labelledby", void 0, this.$element()); return } const labelId = this.$element().attr("aria-labelledby"); const newLabelId = this._getNewLabelId(labelId, id, shouldRemove); this.setAria("labelledby", newLabelId, this.$element()) } _render() { super._render(); this._renderTagRemoveAction(); this._renderSingleLineScroll(); this._scrollContainer("start") } _initTagTemplate() { this._tagTemplate = this._getTemplateByOption("tagTemplate") } _renderField() { const { fieldTemplate: fieldTemplate } = this.option(); const isDefaultFieldTemplate = !isDefined(fieldTemplate); this.$element().toggleClass("dx-tagbox-default-template", isDefaultFieldTemplate).toggleClass("dx-tagbox-custom-template", !isDefaultFieldTemplate); super._renderField() } _renderTagRemoveAction() { const tagRemoveAction = this._createAction(this._removeTagHandler.bind(this)); const eventName = addNamespace(clickEvent, "dxTagBoxTagRemove"); eventsEngine.off(this._$tagsContainer, eventName); eventsEngine.on(this._$tagsContainer, eventName, ".dx-tag-remove-button", event => { tagRemoveAction({ event: event }) }) } _renderSingleLineScroll() { const mouseWheelEvent = addNamespace("dxmousewheel", this.NAME); const $element = this.$element(); const { multiline: multiline } = this.option(); eventsEngine.off($element, mouseWheelEvent); if ("desktop" !== devices.real().deviceType) { if (this._$tagsContainer) { this._$tagsContainer.css("overflowX", multiline ? "" : "auto") } return } if (multiline) { return } eventsEngine.on($element, mouseWheelEvent, this._tagContainerMouseWheelHandler.bind(this)) } _tagContainerMouseWheelHandler(e) { if (!this._$tagsContainer) { return } const scrollLeft = this._$tagsContainer.scrollLeft(); const delta = -.3 * e.delta; if (!isCommandKeyPressed(e) && allowScroll(this._$tagsContainer, delta, true)) { this._$tagsContainer.scrollLeft(scrollLeft + delta); return false } return } _renderEvents() { super._renderEvents(); const input = this._input(); const namespace = addNamespace("keydown", this.NAME); eventsEngine.on(input, namespace, e => { const keyName = normalizeKeyName(e) ?? ""; if (!this._isControlKey(keyName) && this._isEditable()) { this._clearTagFocus() } }) } _popupWrapperClass() { return `${super._popupWrapperClass()} dx-tagbox-popup-wrapper` } _renderInput() { super._renderInput(); this._renderPreventBlurOnInputClick() } _renderPreventBlurOnInputClick() { const eventName = addNamespace("mousedown", "dxTagBox"); eventsEngine.off(this._inputWrapper(), eventName); eventsEngine.on(this._inputWrapper(), eventName, e => { if (e.target !== this._input()[0] && this._isFocused()) { e.preventDefault() } }) } _renderInputValueImpl() { return this._renderMultiSelect() } _loadInputValue() { return when() } _clearTextValue() { this._input().val(""); this._toggleEmptinessEventHandler(); this.option("text", "") } _focusInHandler(e) { if (!this._preventNestedFocusEvent(e)) { this._scrollContainer("end") } super._focusInHandler(e) } _renderInputValue() { this.option("displayValue", this._searchValue()); return super._renderInputValue(...arguments) } _restoreInputText(saveEditingValue) { if (!saveEditingValue) { this._clearTextValue() } } _focusOutHandler(e) { if (!this._preventNestedFocusEvent(e)) { this._clearTagFocus(); this._scrollContainer("start") } super._focusOutHandler(e) } _initSelectAllValueChangedAction() { this._selectAllValueChangeAction = this._createActionByOption("onSelectAllValueChanged") } _renderList() { super._renderList(); this._setListDataSourceFilter() } _canListHaveFocus() { const { applyValueMode: applyValueMode } = this.option(); return "useButtons" === applyValueMode } _listConfig() { const { showSelectionControls: showSelectionControls, maxFilterQueryLength: maxFilterQueryLength, selectAllText: selectAllText, selectAllMode: selectAllMode } = this.option(); const selectionMode = showSelectionControls ? "all" : "multiple"; return extend(super._listConfig(), { maxFilterLengthInRequest: maxFilterQueryLength, selectionMode: selectionMode, selectAllText: selectAllText, onSelectAllValueChanged: _ref => { var _this$_selectAllValue; let { value: value } = _ref; null === (_this$_selectAllValue = this._selectAllValueChangeAction) || void 0 === _this$_selectAllValue || _this$_selectAllValue.call(this, { value: value }) }, selectAllMode: selectAllMode, selectedItems: this._selectedItems, onFocusedItemChanged: null }) } _renderMultiSelect() { const d = Deferred(); this._updateTagsContainer(this._$textEditorInputContainer); this._renderInputSize(); this._renderTags().done(d.resolve).fail(d.reject); return d.promise() } _listItemClickHandler(e) { const { showSelectionControls: showSelectionControls, applyValueMode: applyValueMode } = this.option(); if (!showSelectionControls) { this._clearTextValue() } if ("useButtons" === applyValueMode) { return } super._listItemClickHandler(e); this._saveValueChangeEvent(void 0) } _shouldClearFilter() { const shouldClearFilter = super._shouldClearFilter(); const { showSelectionControls: showSelectionControls } = this.option(); return !showSelectionControls && Boolean(shouldClearFilter) } _renderInputSize() { const $input = this._input(); const value = $input.val(); const isEmptyInput = isString(value) && value; let width = ""; let size = ""; const { searchEnabled: searchEnabled, acceptCustomValue: acceptCustomValue } = this.option(); const canTypeText = searchEnabled || acceptCustomValue; if (isEmptyInput && canTypeText) { const $calculationElement = createTextElementHiddenCopy($input, value, { includePaddings: true }); $calculationElement.insertAfter($input); width = getOuterWidth($calculationElement) + 5; $calculationElement.remove() } else if (!value) { size = 1 } $input.css("width", width); $input.attr("size", size) } _renderInputSubstitution() { super._renderInputSubstitution(); this._updateWidgetHeight() } _getValue() { const { value: value } = this.option(); return value || [] } _multiTagRequired() { const values = this._getValue(); const { maxDisplayedTags: maxDisplayedTags } = this.option(); return isDefined(maxDisplayedTags) && values.length > maxDisplayedTags } _renderMultiTag($input) { var _this$_multiTagPrepar, _this$_tagTemplate; const tagId = `dx-${new Guid}`; const $tag = $("<div>").attr("id", tagId).addClass("dx-tag").addClass("dx-tagbox-multi-tag"); const { selectedItems: selectedItems } = this.option(); const args = { multiTagElement: getPublicElement($tag), selectedItems: selectedItems ?? [] }; null === (_this$_multiTagPrepar = this._multiTagPreparingAction) || void 0 === _this$_multiTagPrepar || _this$_multiTagPrepar.call(this, args); if (args.cancel) { return false } $tag.data("dxTagData", args.text); $tag.insertBefore($input); null === (_this$_tagTemplate = this._tagTemplate) || void 0 === _this$_tagTemplate || _this$_tagTemplate.render({ model: args.text, container: getPublicElement($tag) }); this._setTagAria($tag, args.text); this._updateElementAria(tagId); return $tag } _getFilter(creator) { const dataSourceFilter = this._dataController.filter(); const { valueExpr: valueExpr, maxFilterQueryLength: maxFilterQueryLength } = this.option(); const filterExpr = creator.getCombinedFilter(valueExpr, dataSourceFilter); const filterQueryLength = encodeURI(JSON.stringify(filterExpr)).length; if (filterQueryLength <= (maxFilterQueryLength ?? 1 / 0)) { return filterExpr } errors.log("W1019", maxFilterQueryLength); return } _getFilteredItems(values) { var _this$_loadFilteredIt, _this$_list3, _this$_list4; null === (_this$_loadFilteredIt = this._loadFilteredItemsPromise) || void 0 === _this$_loadFilteredIt || _this$_loadFilteredIt.reject(); const creator = new FilterCreator(values); const { selectedItems: listSelectedItems } = (null === (_this$_list3 = this._list) || void 0 === _this$_list3 ? void 0 : _this$_list3.option()) ?? {}; const isListItemsLoaded = !!listSelectedItems && (null === (_this$_list4 = this._list) || void 0 === _this$_list4 ? void 0 : _this$_list4._dataController.isLoaded()); const { selectedItems: optionSelectedItems } = this.option(); const selectedItems = listSelectedItems ?? optionSelectedItems; const clientFilterFunction = creator.getLocalFilter(this._valueGetter); const filteredItems = selectedItems.filter(clientFilterFunction); const selectedItemsAlreadyLoaded = filteredItems.length === values.length; const d = Deferred(); const dataController = this._dataController; if (!this._dataSource) { return d.resolve([]).promise() } if ((!this._isDataSourceChanged || isListItemsLoaded) && selectedItemsAlreadyLoaded && !this._isDataSourceOptionChanged) { return d.resolve(filteredItems).promise() } const { customQueryParams: customQueryParams, expand: expand, select: select } = dataController.loadOptions(); const filter = this._getFilter(creator); dataController.loadFromStore({ filter: filter, customQueryParams: customQueryParams, expand: expand, select: select }).done((data, extra) => { this._isDataSourceChanged = false; this._isDataSourceOptionChanged = false; if (this._disposed) { d.reject(); return } const { data: items } = normalizeLoadResult(data, extra); const mappedItems = dataController.applyMapFunction(items); d.resolve(mappedItems.filter(clientFilterFunction)) }).fail(d.reject); this._loadFilteredItemsPromise = d; return d.promise() } _createTagsData(values, filteredItems) { const items = []; const cache = {}; const isValueExprSpecified = "this" === this._valueGetterExpr(); const { acceptCustomValue: acceptCustomValue } = this.option(); const filteredValues = {}; filteredItems.forEach(filteredItem => { const filteredItemValue = isValueExprSpecified ? JSON.stringify(filteredItem) : this._valueGetter(filteredItem); filteredValues[filteredItemValue] = filteredItem }); const loadItemPromises = []; values.forEach((value, index) => { const currentItem = filteredValues[isValueExprSpecified ? JSON.stringify(value) : value]; if (isValueExprSpecified && !isDefined(currentItem)) { if (!this._dataSource) { return } loadItemPromises.push(this._loadItem(value, cache).done(item => { const newItem = this._createTagData(item, value); items.splice(index, 0, newItem) }).fail(() => { if (acceptCustomValue) { const newItem = this._createTagData(void 0, value); items.splice(index, 0, newItem) } })) } else { const newItem = this._createTagData(currentItem, value); items.splice(index, 0, newItem) } }); const d = Deferred(); when.apply(this, loadItemPromises).always(() => { d.resolve(items) }); return d.promise() } _createTagData(item, value) { if (isDefined(item)) { var _this$_selectedItems; null === (_this$_selectedItems = this._selectedItems) || void 0 === _this$_selectedItems || _this$_selectedItems.push(item); return item } const { selectedItem: selectedItem } = this.option(); const customItem = this._valueGetter(selectedItem) === value ? selectedItem : value; return customItem } _isGroupedData() { const { grouped: grouped } = this.option(); return Boolean(grouped) && !this._dataController.group() } _getItemsByValues(values) { const resultItems = []; values.forEach(value => { const item = this._getItemFromPlain(value); if (isDefined(item)) { resultItems.push(item) } }); return resultItems } _getFilteredGroupedItems(values) { const selectedItems = Deferred(); if (this._filteredGroupedItemsLoadPromise) { this._dataController.cancel(this._filteredGroupedItemsLoadPromise.operationId) } if (!this._dataController.items().length) { this._filteredGroupedItemsLoadPromise = this._dataController.load().done(() => { selectedItems.resolve(this._getItemsByValues(values)) }).fail(() => { selectedItems.resolve([]) }).always(() => { this._filteredGroupedItemsLoadPromise = void 0 }) } else { selectedItems.resolve(this._getItemsByValues(values)) } return selectedItems.promise() } _loadTagsData() { const values = this._getValue(); const tagData = Deferred(); this._selectedItems = []; const filteredItemsPromise = this._isGroupedData() ? this._getFilteredGroupedItems(values) : this._getFilteredItems(values); filteredItemsPromise.done(filteredItems => { const items = this._createTagsData(values, filteredItems); items.always(data => { tagData.resolve(data) }) }).fail(tagData.reject.bind(this)); return tagData.promise() } _renderTags() { const d = Deferred(); let isPlainDataUsed = false; const valuesToUpdate = this._valuesToUpdate; if (valuesToUpdate && this._shouldGetItemsFromPlain(valuesToUpdate)) { this._selectedItems = this._getItemsFromPlain(valuesToUpdate); if (this._selectedItems.length === valuesToUpdate.length) { this._tagsToRender = this._sortSelectedItemsByValues(this._selectedItems, valuesToUpdate); this._renderTagsImpl(); isPlainDataUsed = true; d.resolve() } } if (!isPlainDataUsed) { this._loadTagsData().done(items => { if (this._disposed) { d.reject(); return } this._tagsToRender = items; this._renderTagsImpl(); d.resolve() }).fail(d.reject) } return d.promise() } _renderTagsImpl() { this._renderField(); if (this._shouldUpdateSelectedItems()) { var _this$_selectedItems2; this.option("selectedItems", null === (_this$_selectedItems2 = this._selectedItems) || void 0 === _this$_selectedItems2 ? void 0 : _this$_selectedItems2.slice()) } const fieldTemplate = this._getFieldTemplate(); if (!fieldTemplate) { this._cleanTags(); this._renderTagsCore() } } _shouldGetItemsFromPlain(values) { return Boolean(values && this._dataController.isLoaded() && values.length <= this._getPlainItems().length) } _getItemsFromPlain(values) { let selectedItems = this._getSelectedItemsFromList(values); const needFilterPlainItems = 0 === selectedItems.length && values.length > 0 || selectedItems.length < values.length; if (needFilterPlainItems) { const plainItems = this._getPlainItems(); selectedItems = this._filterSelectedItems(plainItems, values) } return selectedItems } _getSelectedItemsFromList(values) { const { selectedItems: listSelectedItems } = this._list ? this._list.option() : { selectedItems: [] }; let selectedItems = []; if (values.length === (null === listSelectedItems || void 0 === listSelectedItems ? void 0 : listSelectedItems.length)) { selectedItems = this._filterSelectedItems(listSelectedItems, values) } return selectedItems } _shouldUseClickOrderForTags(values) { const { maxDisplayedTags: maxDisplayedTags, showMultiTagOnly: showMultiTagOnly } = this.option(); return !showMultiTagOnly && isDefined(maxDisplayedTags) && values.length > maxDisplayedTags } _sortSelectedItemsByValues(selectedItems, values) { if (!this._shouldUseClickOrderForTags(values) || !selectedItems.length) { return selectedItems } const isValueExprDefault = "this" === this._valueGetterExpr(); const mappedSelectedItems = selectedItems.reduce((result, item) => { const itemValue = isValueExprDefault ? JSON.stringify(item) : this._valueGetter(item); result[itemValue] = item; return result }, {}); const selectedByOrderItems = values.reduce((result, currentValue) => { const normalizedValue = isValueExprDefault ? JSON.stringify(currentValue) : currentValue; const item = mappedSelectedItems[normalizedValue]; if (isDefined(item)) { result.push(item) } return result }, []); return selectedByOrderItems } _filterSelectedItems(plainItems, values) { return plainItems.filter(dataItem => values.some(currentValue => { if (isObject(currentValue)) { return this._isValueEquals(dataItem, currentValue) } return this._isValueEquals(this._valueGetter(dataItem), currentValue) })) } _processDataSourceChanging() { this._isDataSourceOptionChanged = true; super._processDataSourceChanging() } _integrateInput() { super._integrateInput(); const tagsContainer = this.$element().find(".dx-texteditor-input-container"); this._updateTagsContainer(tagsContainer); this._renderTagRemoveAction(); this._renderTagsCore() } _renderTagsCore() { var _this$_popup; this._renderTagsElements(this._tagsToRender ?? []); this._renderEmptyState(); if (!this._preserveFocusedTag) { this._clearTagFocus() } null === (_this$_popup = this._popup) || void 0 === _this$_popup || _this$_popup.refreshPosition() } _shouldUpdateSelectedItems() { var _this$_selectedItems3, _this$_selectedItems4; const { selectedItems: selectedItems } = this.option(); if (isDefined(selectedItems) && selectedItems.length !== (null === (_this$_selectedItems3 = this._selectedItems) || void 0 === _this$_selectedItems3 ? void 0 : _this$_selectedItems3.length)) { return true } const intersection = getIntersection(selectedItems, this._selectedItems); if (intersection.length !== (null === (_this$_selectedItems4 = this._selectedItems) || void 0 === _this$_selectedItems4 ? void 0 : _this$_selectedItems4.length)) { return true } return false } _renderTagsElements(items) { const $multiTag = this._multiTagRequired() && this._renderMultiTag(this._input()); const { showMultiTagOnly: showMultiTagOnly, maxDisplayedTags: maxDisplayedTags } = this.option(); items.forEach((item, index) => { const isOverMaxDisplayed = isDefined(maxDisplayedTags) && index - maxDisplayedTags >= -1; if ($multiTag && (showMultiTagOnly || isOverMaxDisplayed)) { return } this._renderTag(item, $multiTag || this._input()) }); if (this._isFocused()) { this._scrollContainer("end") } this._refreshTagElements() } _cleanTags() { if (this._multiTagRequired()) { var _this$_tagElements2; null === (_this$_tagElements2 = this._tagElements()) || void 0 === _this$_tagElements2 || _this$_tagElements2.remove() } else { const $tags = this._tagElements(); const { selectedItems: selectedItems = [] } = this.option(); const values = selectedItems.map(item => this._valueGetter(item)); Array.from($tags).forEach(tag => { const $tag = $(tag); const tagData = $tag.data("dxTagData"); if (!values.includes(tagData)) { $tag.remove() } }) } this._updateElementAria() } _renderEmptyState() { var _this$_selectedItems5; const isEmpty = !(this._getValue().length || null !== (_this$_selectedItems5 = this._selectedItems) && void 0 !== _this$_selectedItems5 && _this$_selectedItems5.length || this._searchValue()); this._toggleEmptiness(isEmpty); this._renderDisplayText() } _renderDisplayText() { this._renderInputSize() } _refreshTagElements() { this._tagElementsCache = this.$element().find(".dx-tag") } _tagElements() { return this._tagElementsCache } _applyTagTemplate(item, $tag) { var _this$_tagTemplate2; null === (_this$_tagTemplate2 = this._tagTemplate) || void 0 === _this$_tagTemplate2 || _this$_tagTemplate2.render({ model: item, container: getPublicElement($tag) }) } _renderTag(item, $input) { const value = this._valueGetter(item); if (!isDefined(value)) { return } let $tag = this._getTag(value); const displayValue = this._displayGetter(item); const itemModel = this._getItemModel(item, displayValue); if ($tag) { const tagDisplayValue = $tag.data("dxTagDisplayValue"); if (isDefined(displayValue) && !equalByValue(tagDisplayValue, displayValue)) { $tag.empty(); this._applyTagTemplate(itemModel, $tag) } this._updateElementAria($tag.attr("id")) } else { const tagId = `dx-${new Guid}`; $tag = this._createTag(value, $input, tagId, displayValue); this._setTagAria($tag, isDefined(displayValue) ? displayValue : value); if (isDefined(item)) { this._applyTagTemplate(itemModel, $tag) } else { this._applyTagTemplate(value, $tag) } this._updateElementAria(tagId) } } _setTagAria($tag, tagText) { const aria = { role: "button", label: tagText, roledescription: messageLocalization.format("dxTagBox-tagRoleDescription") }; this.setAria(aria, $tag) } _getItemModel(item, displayValue) { if (isObject(item) && isDefined(displayValue)) { return item } return ensureDefined(displayValue, "") } _getTag(value) { const $tags = this._tagElements(); if (!$tags) { return false } for (const tag of Array.from($tags)) { const tagData = elementData(tag, "dxTagData"); if (value === tagData || equalByValue(value, tagData)) { return $(tag) } } return false } _createTag(value, $input, tagId, displayValue) { return $("<div>").attr("id", tagId).addClass("dx-tag").data("dxTagData", value).data("dxTagDisplayValue", displayValue).insertBefore($input) } _toggleEmptinessEventHandler() { this._toggleEmptiness(!this._getValue().length && !this._searchValue().length) } _customItemAddedHandler(e) { super._customItemAddedHandler(e); this._clearTextValue() } _removeTagHandler(args) { const e = args.event; e.stopPropagation(); this._saveValueChangeEvent(e); const $tag = $(e.target).closest(".dx-tag"); this._removeTagElement($tag) } _removeTagElement($tag) { const { showMultiTagOnly: showMultiTagOnly, maxDisplayedTags: maxDisplayedTags } = this.option(); if ($tag.hasClass("dx-tagbox-multi-tag")) { if (!showMultiTagOnly && isDefined(maxDisplayedTags)) { const displayedTagsCount = Math.max(0, maxDisplayedTags - 1); const newValue = this._getValue().slice(0, displayedTagsCount); this.option("value", newValue) } else { this.clear() } return } const itemValue = $tag.data("dxTagData"); const itemId = $tag.attr("id"); this._removeTagWithUpdate(itemValue); this._updateElementAria(itemId, true); this._refreshTagElements() } _updateField() {} _removeTagWithUpdate(itemValue) { const value = this._getValue().slice(); this._removeTag(value, itemValue); this.option("value", value); this.option("selectedItem", null); if (0 === value.length) { this._clearTagFocus() } } _getCurrentValue() { return this._lastValue() } _selectionChangeHandler(e) { var _this$_list5, _this$_list7; const { applyValueMode: applyValueMode, value: currentValue } = this.option(); if ("useButtons" === applyValueMode) { return } const value = this._getValue().slice(); e.removedItems.forEach(removedItem => { this._removeTag(value, this._valueGetter(removedItem)) }); e.addedItems.forEach(addedItem => { this._addTag(value, this._valueGetter(addedItem)) }); this._updateWidgetHeight(); const { selectedItemKeys: selectedItemKeys } = (null === (_this$_list5 = this._list) || void 0 === _this$_list5 ? void 0 : _this$_list5.option()) ?? {}; if (!equalByValue(selectedItemKeys, currentValue)) { var _this$_list6; const listSelectionChangeEvent = null === (_this$_list6 = this._list) || void 0 === _this$_list6 ? void 0 : _this$_list6._getSelectionChangeEvent(); if (listSelectionChangeEvent) { this._saveValueChangeEvent(listSelectionChangeEvent) } this.option("value", value) } null === (_this$_list7 = this._list) || void 0 === _this$_list7 || _this$_list7._saveSelectionChangeEvent(void 0) } _removeTag(value, item) { const index = this._valueIndex(item, value); if (index >= 0) { value.splice(index, 1) } } _addTag(value, item) { const index = this._valueIndex(item); if (index < 0) { value.push(item) } } _fieldRenderData() { var _this$_selectedItems6; return null === (_this$_selectedItems6 = this._selectedItems) || void 0 === _this$_selectedItems6 ? void 0 : _this$_selectedItems6.slice() } _completeSelection(value) { const { showSelectionControls: showSelectionControls } = this.option(); if (!showSelectionControls) { this._setValue(value) } } _setValue(value) { var _this$_list8; if (null === value) { return } const { applyValueMode: applyValueMode } = this.option(); const useButtons = "useButtons" === applyValueMode; const valueIndex = this._valueIndex(value); const { selectedItemKeys: selectedItemKeys } = (null === (_this$_list8 = this._list) || void 0 === _this$_list8 ? void 0 : _this$_list8.option()) || { selectedItemKeys: [] }; const values = (useButtons ? selectedItemKeys ?? [] : this._getValue()).slice(); if (valueIndex >= 0) { values.splice(valueIndex, 1) } else { values.push(value) } if (useButtons) { var _this$_list9; null === (_this$_list9 = this._list) || void 0 === _this$_list9 || _this$_list9.option("selectedItemKeys", values) } else { this.option("value", values) } } _isSelectedValue(value, cache) { return this._valueIndex(value, null, cache) > -1 } _valueIndex(value, values, cache) { if (cache && "object" !== typeof value) { if (!cache.indexByValues) { const indexByValues = {}; cache.indexByValues = indexByValues; const resolvedForCache = values ?? this._getValue(); resolvedForCache.forEach((item, index) => { indexByValues[String(item)] = index }) } if (String(value) in cache.indexByValues) { return cache.indexByValues[String(value)] } } const resolvedValues = values ?? this._getValue(); return resolvedValues.findIndex(selectedValue => this._isValueEquals(value, selectedValue)) } _lastValue() { const values = this._getValue(); const lastValue = values[values.length - 1]; return lastValue ?? null } _shouldRenderSearchEvent() { const { searchEnabled: searchEnabled, acceptCustomValue: acceptCustomValue } = this.option(); return searchEnabled || acceptCustomValue } _searchHandler(e) { const { searchEnabled: searchEnabled } = this.option(); if (searchEnabled && !!e && !this._isTagRemoved) { super._searchHandler(e); this._setListDataSourceFilter() } this._updateWidgetHeight(); delete this._isTagRemoved } _updateWidgetHeight() { const element = this.$element(); const originalHeight = getHeight(element); this._renderInputSize(); const currentHeight = getHeight(element); const { opened: opened } = this.option(); if (this._popup && opened && this._isEditable() && currentHeight !== originalHeight) { this._popup.repaint() } } _refreshSelected() { var _this$_list0; if (null !== (_this$_list0 = this._list) && void 0 !== _this$_list0 && _this$_list0.getDataSource()) { this._list.option("selectedItems", this._selectedItems) } } _resetListDataSourceFilter() { const dataController = this._dataController; delete this._userFilter; dataController.filter(null); dataController.reload() } _setListDataSourceFilter() { const { hideSelectedItems: hideSelectedItems } = this.option(); i