diff --git a/packages/devextreme/js/__internal/ui/m_tag_box.ts b/packages/devextreme/js/__internal/ui/m_tag_box.ts index 07e8784318ef..2228dbd4a1f9 100644 --- a/packages/devextreme/js/__internal/ui/m_tag_box.ts +++ b/packages/devextreme/js/__internal/ui/m_tag_box.ts @@ -6,25 +6,33 @@ import messageLocalization from '@js/common/core/localization/message'; import { normalizeLoadResult } from '@js/common/data/data_source/utils'; import registerComponent from '@js/core/component_registrator'; import devices from '@js/core/devices'; +import type { DxElement } from '@js/core/element'; import { getPublicElement } from '@js/core/element'; import { data as elementData } from '@js/core/element_data'; import Guid from '@js/core/guid'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; +import type { TemplateBase } from '@js/core/templates/template_base'; import { getIntersection, removeDuplicates } from '@js/core/utils/array'; import { ensureDefined, equalByValue } from '@js/core/utils/common'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred, when } from '@js/core/utils/deferred'; import { createTextElementHiddenCopy } from '@js/core/utils/dom'; import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; import { SelectionFilterCreator as FilterCreator } from '@js/core/utils/selection_filter'; import { getHeight, getOuterWidth } from '@js/core/utils/size'; import { isDefined, isObject, isString } from '@js/core/utils/type'; import { hasWindow } from '@js/core/utils/window'; +import type { DxEvent } from '@js/events'; +import type { ItemClickEvent, SelectionChangedEvent as ListSelectionChangedEvent } from '@js/ui/list'; import type { Properties } from '@js/ui/tag_box'; import errors from '@js/ui/widget/ui.errors'; +import type { OptionChanged } from '@ts/core/widget/types'; +import type { KeyboardKeyDownEvent } from '@ts/events/core/m_keyboard_processor'; +import type { ItemCache } from '@ts/ui/drop_down_editor/drop_down_list'; +import type { ListBaseProperties } from '@ts/ui/list/list.base'; +import type { DxMouseWheelEvent } from '@ts/ui/scroll_view/types'; import SelectBox from '@ts/ui/select_box'; import caret from '@ts/ui/text_box/utils.caret'; import { allowScroll } from '@ts/ui/text_box/utils.scroll'; @@ -33,9 +41,29 @@ function xor(a: boolean, b: boolean): boolean { return (a || b) && !(a && b); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any type TagBoxItem = string | number | any; type SelectedItemsMap = Record; +interface FilterCreatorInstance { + getCombinedFilter: ( + keyExpr: string | ((item: unknown) => unknown) | undefined, + dataSourceFilter: unknown, + ) => unknown; + getLocalFilter: (keyGetter: (item: unknown) => unknown) => (item: unknown) => boolean; +} + +interface MultiTagPreparingArgs { + multiTagElement: DxElement; + selectedItems?: (string | number | unknown)[]; + text?: string; + cancel?: boolean; +} + +interface ValueIndexCache extends ItemCache { + indexByValues?: Record; +} + const TAGBOX_TAG_DATA_KEY = 'dxTagData'; const TAGBOX_TAG_DISPLAY_VALUE = 'dxTagDisplayValue'; @@ -61,6 +89,7 @@ export interface TagBoxProperties extends Omit< | 'onOpened' | 'onClosed' | 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste' | 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> { + useSubmitBehavior?: boolean; } class TagBox< @@ -76,40 +105,46 @@ class TagBox< _selectAllValueChangeAction?: (event?: Record) => void; - _multiTagPreparingAction?: (event?: Record) => void; + _multiTagPreparingAction?: (event?: MultiTagPreparingArgs) => void; - _valuesToUpdate?: any; + _valuesToUpdate?: unknown[]; _preserveFocusedTag?: boolean; _isTagRemoved?: boolean; - _userFilter?: any; + _userFilter?: unknown; _isDataSourceChanged?: boolean; - _tagTemplate?: any; + _tagTemplate?: TemplateBase; _isDataSourceOptionChanged?: boolean; _tagElementsCache?: dxElementWrapper; - _selectedItems?: any[]; + _selectedItems?: unknown[]; - _tagsToRender?: any[]; + _tagsToRender?: unknown[]; _supportedKeys(): Record< - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - string, (e: KeyboardEvent, options?: Record) => boolean | void + string, + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + (e: KeyboardEvent, options?: KeyboardKeyDownEvent) => boolean | void > { const parent = super._supportedKeys(); - // @ts-expect-error ts-error - const sendToList = (options) => this._list._keyboardHandler(options); - const rtlEnabled = this.option('rtlEnabled'); + const sendToList = (options?: KeyboardKeyDownEvent): void => { + if (!options) { + return; + } + + this._list?._keyboardHandler(options); + }; + const { rtlEnabled } = this.option(); return { ...parent, - backspace(e): void { + backspace: (e: KeyboardEvent): void => { if (!this._isCaretAtTheStart()) { return; } @@ -117,23 +152,26 @@ class TagBox< this._processKeyboardEvent(e); this._isTagRemoved = true; - const $tagToDelete = this._$focusedTag || this._tagElements().last(); + const $tagToDelete = this._$focusedTag || this._tagElements()?.last(); if (this._$focusedTag) { this._moveTagFocus('prev', true); } - if ($tagToDelete.length === 0) { + if (!$tagToDelete || $tagToDelete.length === 0) { return; } - this._preserveFocusedTag = true; this._removeTagElement($tagToDelete); delete this._preserveFocusedTag; }, - upArrow: (e, opts) => (e.altKey || !this._list ? parent.upArrow.call(this, e) : sendToList(opts)), - downArrow: (e, opts) => (e.altKey || !this._list ? parent.downArrow.call(this, e) : sendToList(opts)), - del(e) { + upArrow: (e: KeyboardEvent, options?: KeyboardKeyDownEvent) => ( + e.altKey || !this._list ? parent.upArrow.call(this, e) : sendToList(options) + ), + downArrow: (e: KeyboardEvent, options?: KeyboardKeyDownEvent) => ( + e.altKey || !this._list ? parent.downArrow.call(this, e) : sendToList(options) + ), + del: (e: KeyboardEvent): void => { if (!this._$focusedTag || !this._isCaretAtTheStart()) { return; } @@ -149,37 +187,40 @@ class TagBox< this._removeTagElement($tagToDelete); delete this._preserveFocusedTag; }, - enter(e, options): void { - const isListItemFocused = this._list?.option('focusedElement') !== null && this.option('opened') === true; - const isCustomItem = this.option('acceptCustomValue') && !isListItemFocused; + enter: (e: KeyboardEvent, options?: KeyboardKeyDownEvent): void => { + const { opened, acceptCustomValue } = this.option(); + const isListItemFocused = this._list?.option('focusedElement') !== null && opened; + const isCustomItem = acceptCustomValue && !isListItemFocused; if (isCustomItem) { e.preventDefault(); - (this._searchValue() !== '') && this._customItemAddedHandler(e); + if (this._searchValue() !== '') { + this._customItemAddedHandler(e); + } return; } - if (this.option('opened')) { + if (opened) { this._saveValueChangeEvent(e); sendToList(options); e.preventDefault(); } }, - space(e, options): void { - const isOpened = this.option('opened'); + space: (e: KeyboardEvent, options?: KeyboardKeyDownEvent): void => { + const { opened } = this.option(); const isInputActive = this._shouldRenderSearchEvent(); - if (isOpened && !isInputActive) { + if (opened && !isInputActive) { this._saveValueChangeEvent(e); sendToList(options); e.preventDefault(); } }, - leftArrow(e): void { + leftArrow: (e: KeyboardEvent): void => { if ( !this._isCaretAtTheStart() || this._isEmpty() - || this._isEditable() && rtlEnabled && !this._$focusedTag + || (this._isEditable() && rtlEnabled && !this._$focusedTag) ) { return; } @@ -189,14 +230,16 @@ class TagBox< const direction = rtlEnabled ? 'next' : 'prev'; this._moveTagFocus(direction); - if (!this.option('multiline')) { + const { multiline } = this.option(); + + if (!multiline) { this._scrollContainer(direction); } }, - rightArrow(e): void { + rightArrow: (e: KeyboardEvent): void => { if (!this._isCaretAtTheStart() || this._isEmpty() - || this._isEditable() && !rtlEnabled && !this._$focusedTag + || (this._isEditable() && !rtlEnabled && !this._$focusedTag) ) { return; } @@ -205,12 +248,16 @@ class TagBox< const direction = rtlEnabled ? 'prev' : 'next'; this._moveTagFocus(direction); - !this.option('multiline') && this._scrollContainer(direction); + const { multiline } = this.option(); + + if (!multiline) { + this._scrollContainer(direction); + } }, }; } - _processKeyboardEvent(e): void { + _processKeyboardEvent(e: KeyboardEvent): void { e.preventDefault(); e.stopPropagation(); this._saveValueChangeEvent(e); @@ -238,7 +285,7 @@ class TagBox< return position?.start === 0 && position.end === 0; } - _updateInputAriaActiveDescendant(id?): void { + _updateInputAriaActiveDescendant(id?: string): void { this.setAria('activedescendant', id, this._input()); } @@ -246,10 +293,10 @@ class TagBox< if (!this._$focusedTag) { const tagElements = this._tagElements(); - this._$focusedTag = direction === 'next' ? tagElements.first() : tagElements.last(); + this._$focusedTag = direction === 'next' ? tagElements?.first() : tagElements?.last(); this._toggleFocusClass(true, this._$focusedTag); - this._updateInputAriaActiveDescendant(this._$focusedTag.attr('id')); + this._updateInputAriaActiveDescendant(this._$focusedTag?.attr('id')); return; } @@ -292,12 +339,13 @@ class TagBox< return this._$tagsContainer; } - _getFieldElement() { + _getFieldElement(): dxElementWrapper { return this._input(); } - _scrollContainer(direction: 'start' | 'end'): void { - if (this.option('multiline') || !hasWindow()) { + _scrollContainer(direction: 'start' | 'end' | 'next' | 'prev'): void { + const { multiline } = this.option(); + if (multiline || !hasWindow()) { return; } @@ -306,11 +354,11 @@ class TagBox< } const scrollPosition = this._getScrollPosition(direction); - // @ts-expect-error ts-error + // @ts-expect-error fix on renderer level this._$tagsContainer.scrollLeft(scrollPosition); } - _getScrollPosition(direction) { + _getScrollPosition(direction: 'start' | 'end' | 'next' | 'prev'): number { if (direction === 'start' || direction === 'end') { return this._getBorderPosition(direction); } @@ -320,25 +368,24 @@ class TagBox< : this._getBorderPosition('end'); } - _getBorderPosition(direction): number { + _getBorderPosition(direction: 'start' | 'end'): number { const { rtlEnabled } = this.option(); - // @ts-expect-error ts-error - const isScrollLeft = xor(direction === 'end', rtlEnabled); + const isScrollLeft = xor(direction === 'end', Boolean(rtlEnabled)); const scrollSign = rtlEnabled ? -1 : 1; + const containerScrollWidth = this._$tagsContainer.get(0).scrollWidth; return xor(isScrollLeft, !rtlEnabled) ? 0 - : scrollSign * (this._$tagsContainer.get(0).scrollWidth - getOuterWidth(this._$tagsContainer)); + : scrollSign * (containerScrollWidth - getOuterWidth(this._$tagsContainer)); } - _getFocusedTagPosition(direction) { - const rtlEnabled = this.option('rtlEnabled'); - // @ts-expect-error ts-error - const isScrollLeft = xor(direction === 'next', rtlEnabled); - // @ts-expect-error ts-error - let { left: scrollOffset } = this._$focusedTag.position(); - let scrollLeft = this._$tagsContainer.scrollLeft(); + _getFocusedTagPosition(direction: 'next' | 'prev'): number { + const { rtlEnabled } = this.option(); + const isScrollLeft = xor(direction === 'next', Boolean(rtlEnabled)); + + let { left: scrollOffset = 0 } = this._$focusedTag?.position() || { }; + let scrollLeft = this._$tagsContainer.scrollLeft() as unknown as number; if (isScrollLeft) { scrollOffset += getOuterWidth(this._$focusedTag, true) - getOuterWidth(this._$tagsContainer); @@ -354,7 +401,7 @@ class TagBox< // eslint-disable-next-line class-methods-use-this _setNextValue(): void {} - _getDefaultOptions() { + _getDefaultOptions(): TProperties { return extend(super._getDefaultOptions(), { value: [], @@ -383,8 +430,7 @@ class TagBox< multiline: true, useSubmitBehavior: true, - - }); + }) as TProperties; } _init(): void { @@ -399,34 +445,37 @@ class TagBox< this._initMultiTagPreparingAction(); } - _initMultiTagPreparingAction() { + _initMultiTagPreparingAction(): void { this._multiTagPreparingAction = this._createActionByOption('onMultiTagPreparing', { beforeExecute: (e) => { - // @ts-expect-error ts-error - this._multiTagPreparingHandler(e.args[0]); + this._multiTagPreparingHandler((e.args as MultiTagPreparingArgs[])[0]); }, excludeValidators: ['disabled', 'readOnly'], }); } - _multiTagPreparingHandler(args) { + _multiTagPreparingHandler(args: MultiTagPreparingArgs): void { const { length: selectedCount } = this._getValue(); + const { showMultiTagOnly, maxDisplayedTags } = this.option(); - if (!this.option('showMultiTagOnly')) { - // @ts-expect-error ts-error - args.text = messageLocalization.getFormatter('dxTagBox-moreSelected')(selectedCount - this.option('maxDisplayedTags') + 1); + if (!showMultiTagOnly) { + // @ts-expect-error getFormatter return type not typed as callable + args.text = messageLocalization.getFormatter('dxTagBox-moreSelected')(selectedCount - maxDisplayedTags + 1); } else { - // @ts-expect-error ts-error + // @ts-expect-error getFormatter return type not typed as callable args.text = messageLocalization.getFormatter('dxTagBox-selected')(selectedCount); } } _initDynamicTemplates(): void { - // @ts-expect-error ts-error + // @ts-expect-error internal property super._initDynamicTemplates(); this._templateManager.addDefaultTemplates({ - tag: new BindableTemplate(($container, data) => { + tag: new BindableTemplate(( + $container: dxElementWrapper, + data: TagBoxItem, + ) => { const $tagContent = $('
').addClass(TAGBOX_TAG_CONTENT_CLASS); $('') @@ -445,7 +494,7 @@ class TagBox< }); } - _toggleSubmitElement(enabled): void { + _toggleSubmitElement(enabled?: unknown): void { if (enabled) { this._renderSubmitElement(); this._setSubmitValue(); @@ -459,7 +508,8 @@ class TagBox< } _renderSubmitElement(): void { - if (!this.option('useSubmitBehavior')) { + const { useSubmitBehavior } = this.option(); + if (!useSubmitBehavior) { return; } @@ -469,30 +519,28 @@ class TagBox< }; this._$submitElement = $('