diff --git a/packages/devextreme/js/__internal/ui/m_tag_box.ts b/packages/devextreme/js/__internal/ui/m_tag_box.ts index f9a22288fbd1..f3f6526c8c94 100644 --- a/packages/devextreme/js/__internal/ui/m_tag_box.ts +++ b/packages/devextreme/js/__internal/ui/m_tag_box.ts @@ -33,6 +33,9 @@ function xor(a: boolean, b: boolean): boolean { return (a || b) && !(a && b); } +type TagBoxItem = string | number | any; +type SelectedItemsMap = Record; + const TAGBOX_TAG_DATA_KEY = 'dxTagData'; const TAGBOX_TAG_DISPLAY_VALUE = 'dxTagDisplayValue'; @@ -51,13 +54,13 @@ const TEXTEDITOR_INPUT_CONTAINER_CLASS = 'dx-texteditor-input-container'; const TAGBOX_MOUSE_WHEEL_DELTA_MULTIPLIER = -0.3; -export interface TagBoxProperties extends Omit { - +export interface TagBoxProperties extends Omit< + Properties, + 'onCustomItemCreating' + | 'onItemClick' | 'onSelectionChanged' + | 'onOpened' | 'onClosed' + | 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste' + | 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> { } class TagBox< @@ -860,7 +863,7 @@ class TagBox< // @ts-expect-error ts-error const isListItemsLoaded = !!listSelectedItems && this._list._dataController.isLoaded(); const selectedItems = listSelectedItems || this.option('selectedItems'); - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const clientFilterFunction = creator.getLocalFilter(this._valueGetter); // @ts-expect-error ts-error const filteredItems = selectedItems.filter(clientFilterFunction); @@ -907,13 +910,13 @@ class TagBox< _createTagsData(values, filteredItems) { const items = []; const cache = {}; - // @ts-expect-error ts-error + // @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin const isValueExprSpecified = this._valueGetterExpr() === 'this'; const { acceptCustomValue } = this.option(); const filteredValues = {}; filteredItems.forEach((filteredItem) => { - // @ts-expect-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const filteredItemValue = isValueExprSpecified ? JSON.stringify(filteredItem) : this._valueGetter(filteredItem); filteredValues[filteredItemValue] = filteredItem; @@ -967,7 +970,7 @@ class TagBox< return item; } const selectedItem = this.option('selectedItem'); - // @ts-expect-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const customItem = this._valueGetter(selectedItem) === value ? selectedItem : value; return customItem; @@ -1046,7 +1049,10 @@ class TagBox< this._selectedItems = this._getItemsFromPlain(this._valuesToUpdate); if (this._selectedItems.length === this._valuesToUpdate.length) { - this._tagsToRender = this._selectedItems; + this._tagsToRender = this._sortSelectedItemsByValues( + this._selectedItems, + this._valuesToUpdate, + ); this._renderTagsImpl(); isPlainDataUsed = true; d.resolve(); @@ -1113,6 +1119,45 @@ class TagBox< return selectedItems; } + _shouldUseClickOrderForTags(values: TagBox['_valuesToUpdate']): boolean { + const { maxDisplayedTags, showMultiTagOnly } = this.option(); + + return !showMultiTagOnly + && isDefined(maxDisplayedTags) + && values.length > maxDisplayedTags; + } + + _sortSelectedItemsByValues( + selectedItems: TagBoxItem[], + values: TagBoxItem[], + ): TagBoxItem[] { + if (!this._shouldUseClickOrderForTags(values) || !selectedItems.length) { + return selectedItems; + } + // @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin + const isValueExprDefault = this._valueGetterExpr() === 'this'; + + const mappedSelectedItems = selectedItems.reduce((result, item) => { + // @ts-expect-error _valueGetter is injected by DataExpressionMixin + const itemValue = isValueExprDefault ? JSON.stringify(item) : this._valueGetter(item); + result[itemValue] = item; + + return result; + }, {}); + + const selectedByOrderItems: TagBoxItem[] = 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) { const selectedItems = plainItems.filter((dataItem) => { let currentValue; @@ -1180,8 +1225,7 @@ class TagBox< _renderTagsElements(items): void { const $multiTag = this._multiTagRequired() && this._renderMultiTag(this._input()); - const showMultiTagOnly = this.option('showMultiTagOnly'); - const maxDisplayedTags = this.option('maxDisplayedTags'); + const { showMultiTagOnly, maxDisplayedTags } = this.option(); items.forEach((item, index) => { // @ts-expect-error ts-error @@ -1206,7 +1250,7 @@ class TagBox< const $tags = this._tagElements(); const selectedItems = this.option('selectedItems') ?? []; - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const values = selectedItems.map((item) => this._valueGetter(item)); each($tags, (_, tag) => { @@ -1252,7 +1296,7 @@ class TagBox< } _renderTag(item, $input): void { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const value = this._valueGetter(item); if (!isDefined(value)) { @@ -1399,12 +1443,12 @@ class TagBox< const value = this._getValue().slice(); each(e.removedItems || [], (_, removedItem) => { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin this._removeTag(value, this._valueGetter(removedItem)); }); each(e.addedItems || [], (_, addedItem) => { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin this._addTag(value, this._valueGetter(addedItem)); }); @@ -1562,7 +1606,7 @@ class TagBox< } const dataController = this._dataController; - // @ts-expect-error ts-error + // @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin const valueGetterExpr = this._valueGetterExpr(); if (isString(valueGetterExpr) && valueGetterExpr !== 'this') { @@ -1584,14 +1628,14 @@ class TagBox< _dataSourceFilterExpr() { const filter = []; - // @ts-expect-error + // @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin this._getValue().forEach((value) => filter.push(['!', [this._valueGetterExpr(), value]])); return filter; } _dataSourceFilterFunction(itemData) { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const itemValue = this._valueGetter(itemData); let result = true; @@ -1640,7 +1684,7 @@ class TagBox< return this ._getPlainItems(this._list.option('selectedItems')) - // @ts-expect-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin .map((item) => this._valueGetter(item)); } @@ -1699,7 +1743,7 @@ class TagBox< } const previousItemsValuesMap = previousItems.reduce((map, item) => { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const value = this._valueGetter(item); map[value] = item; return map; @@ -1707,7 +1751,7 @@ class TagBox< const addedItems = []; newItems.forEach((item) => { - // @ts-expect-error ts-error + // @ts-expect-error _valueGetter is injected by DataExpressionMixin const value = this._valueGetter(item); if (!previousItemsValuesMap[value]) { addedItems.push(item as never); diff --git a/packages/devextreme/playground/index.ts b/packages/devextreme/playground/index.ts index 15f94bf2a99f..68ff30f0d976 100644 --- a/packages/devextreme/playground/index.ts +++ b/packages/devextreme/playground/index.ts @@ -29,4 +29,4 @@ window.addEventListener('load', () => cardMinWidth: 320, columns: ['Company', 'Address', 'City', 'State', 'Zipcode', 'Phone'], }); - })); + })); \ No newline at end of file diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js index a235065b5fac..195144002412 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js @@ -1078,6 +1078,62 @@ QUnit.module('multi tag support', { assert.deepEqual(this.getTexts($tagBox.find('.' + TAGBOX_TAG_CLASS)), ['1', '2'], 'tags have correct text'); }); + + QUnit.test('TagBox should preserve reverse click order in leading tag when showMultiTagOnly is false', function(assert) { + const $tagBox = $('#tagBox').dxTagBox({ + items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + showSelectionControls: true, + maxDisplayedTags: 2, + showMultiTagOnly: false, + opened: true + }); + + const tagBox = $tagBox.dxTagBox('instance'); + + this.clock.tick(TIME_TO_WAIT); + + const $listItems = getListItems(tagBox); + + $listItems.last().trigger('dxclick'); + $listItems.eq(7).trigger('dxclick'); + $listItems.eq(6).trigger('dxclick'); + + + assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), '10', 'leading tag has correct text'); + }); + + QUnit.test('TagBox should work correctly with string ID\'s in item when valueExpr is used', function(assert) { + const items = [ + { ID: 'a', Name: 'HD Video Player' }, + { ID: 'b', Name: 'SuperHD Video Player' }, + { ID: 'c', Name: 'SuperPlasma 50' }, + { ID: 'd', Name: 'SuperLED 50' } + ]; + const $tagBox = $('#tagBox').dxTagBox({ + items: items, + valueExpr: 'ID', + displayExpr: 'Name', + showSelectionControls: true, + maxDisplayedTags: 2, + showMultiTagOnly: false, + opened: true + }); + + const tagBox = $tagBox.dxTagBox('instance'); + + this.clock.tick(TIME_TO_WAIT); + + const $listItems = getListItems(tagBox); + + $listItems.last().trigger('dxclick'); + $listItems.eq(2).trigger('dxclick'); + $listItems.eq(1).trigger('dxclick'); + + + assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), items[items.length - 1].Name, 'leading tag has correct text'); + }); + + QUnit.test('only one multi tag should be rendered when selectAll checked and value changind on runtime', function(assert) { let suppressSelectionChanged = false;