devextreme
Version:
JavaScript/TypeScript Component Suite for Responsive Web Development
1,320 lines (1,319 loc) • 57.2 kB
JavaScript
/**
* 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