Skip to content

Commit bbafb36

Browse files
vorobeyAndrei VorobevCopilot
authored
TagBox: Keeping click order when maxDisplayTag enabled and showMultiTagOnly disabled (T1328498) (#33655)
Signed-off-by: Andrey Vorobev <dobriy.kaa@gmail.com> Co-authored-by: Andrei Vorobev <andrei.vorobev@devexpress.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent a05ab96 commit bbafb36

3 files changed

Lines changed: 125 additions & 25 deletions

File tree

packages/devextreme/js/__internal/ui/m_tag_box.ts

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ function xor(a: boolean, b: boolean): boolean {
3333
return (a || b) && !(a && b);
3434
}
3535

36+
type TagBoxItem = string | number | any;
37+
type SelectedItemsMap = Record<string, TagBoxItem>;
38+
3639
const TAGBOX_TAG_DATA_KEY = 'dxTagData';
3740
const TAGBOX_TAG_DISPLAY_VALUE = 'dxTagDisplayValue';
3841

@@ -51,13 +54,13 @@ const TEXTEDITOR_INPUT_CONTAINER_CLASS = 'dx-texteditor-input-container';
5154

5255
const TAGBOX_MOUSE_WHEEL_DELTA_MULTIPLIER = -0.3;
5356

54-
export interface TagBoxProperties extends Omit<Properties,
55-
'onCustomItemCreating'
56-
| 'onItemClick' | 'onSelectionChanged'
57-
| 'onOpened' | 'onClosed'
58-
| 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste'
59-
| 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> {
60-
57+
export interface TagBoxProperties extends Omit<
58+
Properties,
59+
'onCustomItemCreating'
60+
| 'onItemClick' | 'onSelectionChanged'
61+
| 'onOpened' | 'onClosed'
62+
| 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste'
63+
| 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> {
6164
}
6265

6366
class TagBox<
@@ -856,7 +859,7 @@ class TagBox<
856859
// @ts-expect-error ts-error
857860
const isListItemsLoaded = !!listSelectedItems && this._list._dataController.isLoaded();
858861
const selectedItems = listSelectedItems || this.option('selectedItems');
859-
// @ts-expect-error ts-error
862+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
860863
const clientFilterFunction = creator.getLocalFilter(this._valueGetter);
861864
// @ts-expect-error ts-error
862865
const filteredItems = selectedItems.filter(clientFilterFunction);
@@ -903,13 +906,13 @@ class TagBox<
903906
_createTagsData(values, filteredItems) {
904907
const items = [];
905908
const cache = {};
906-
// @ts-expect-error ts-error
909+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
907910
const isValueExprSpecified = this._valueGetterExpr() === 'this';
908911
const { acceptCustomValue } = this.option();
909912
const filteredValues = {};
910913

911914
filteredItems.forEach((filteredItem) => {
912-
// @ts-expect-error
915+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
913916
const filteredItemValue = isValueExprSpecified ? JSON.stringify(filteredItem) : this._valueGetter(filteredItem);
914917

915918
filteredValues[filteredItemValue] = filteredItem;
@@ -963,7 +966,7 @@ class TagBox<
963966
return item;
964967
}
965968
const selectedItem = this.option('selectedItem');
966-
// @ts-expect-error
969+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
967970
const customItem = this._valueGetter(selectedItem) === value ? selectedItem : value;
968971

969972
return customItem;
@@ -1042,7 +1045,10 @@ class TagBox<
10421045
this._selectedItems = this._getItemsFromPlain(this._valuesToUpdate);
10431046

10441047
if (this._selectedItems.length === this._valuesToUpdate.length) {
1045-
this._tagsToRender = this._selectedItems;
1048+
this._tagsToRender = this._sortSelectedItemsByValues(
1049+
this._selectedItems,
1050+
this._valuesToUpdate,
1051+
);
10461052
this._renderTagsImpl();
10471053
isPlainDataUsed = true;
10481054
d.resolve();
@@ -1109,6 +1115,45 @@ class TagBox<
11091115
return selectedItems;
11101116
}
11111117

1118+
_shouldUseClickOrderForTags(values: TagBox['_valuesToUpdate']): boolean {
1119+
const { maxDisplayedTags, showMultiTagOnly } = this.option();
1120+
1121+
return !showMultiTagOnly
1122+
&& isDefined(maxDisplayedTags)
1123+
&& values.length > maxDisplayedTags;
1124+
}
1125+
1126+
_sortSelectedItemsByValues(
1127+
selectedItems: TagBoxItem[],
1128+
values: TagBoxItem[],
1129+
): TagBoxItem[] {
1130+
if (!this._shouldUseClickOrderForTags(values) || !selectedItems.length) {
1131+
return selectedItems;
1132+
}
1133+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
1134+
const isValueExprDefault = this._valueGetterExpr() === 'this';
1135+
1136+
const mappedSelectedItems = selectedItems.reduce<SelectedItemsMap>((result, item) => {
1137+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
1138+
const itemValue = isValueExprDefault ? JSON.stringify(item) : this._valueGetter(item);
1139+
result[itemValue] = item;
1140+
1141+
return result;
1142+
}, {});
1143+
1144+
const selectedByOrderItems: TagBoxItem[] = values.reduce((result, currentValue) => {
1145+
const normalizedValue = isValueExprDefault ? JSON.stringify(currentValue) : currentValue;
1146+
const item = mappedSelectedItems[normalizedValue];
1147+
if (isDefined(item)) {
1148+
result.push(item);
1149+
}
1150+
1151+
return result;
1152+
}, []);
1153+
1154+
return selectedByOrderItems;
1155+
}
1156+
11121157
_filterSelectedItems(plainItems, values) {
11131158
const selectedItems = plainItems.filter((dataItem) => {
11141159
let currentValue;
@@ -1176,8 +1221,7 @@ class TagBox<
11761221

11771222
_renderTagsElements(items): void {
11781223
const $multiTag = this._multiTagRequired() && this._renderMultiTag(this._input());
1179-
const showMultiTagOnly = this.option('showMultiTagOnly');
1180-
const maxDisplayedTags = this.option('maxDisplayedTags');
1224+
const { showMultiTagOnly, maxDisplayedTags } = this.option();
11811225

11821226
items.forEach((item, index) => {
11831227
// @ts-expect-error ts-error
@@ -1202,7 +1246,7 @@ class TagBox<
12021246
const $tags = this._tagElements();
12031247

12041248
const selectedItems = this.option('selectedItems') ?? [];
1205-
// @ts-expect-error ts-error
1249+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
12061250
const values = selectedItems.map((item) => this._valueGetter(item));
12071251

12081252
each($tags, (_, tag) => {
@@ -1248,7 +1292,7 @@ class TagBox<
12481292
}
12491293

12501294
_renderTag(item, $input): void {
1251-
// @ts-expect-error ts-error
1295+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
12521296
const value = this._valueGetter(item);
12531297

12541298
if (!isDefined(value)) {
@@ -1395,12 +1439,12 @@ class TagBox<
13951439
const value = this._getValue().slice();
13961440

13971441
each(e.removedItems || [], (_, removedItem) => {
1398-
// @ts-expect-error ts-error
1442+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
13991443
this._removeTag(value, this._valueGetter(removedItem));
14001444
});
14011445

14021446
each(e.addedItems || [], (_, addedItem) => {
1403-
// @ts-expect-error ts-error
1447+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
14041448
this._addTag(value, this._valueGetter(addedItem));
14051449
});
14061450

@@ -1558,7 +1602,7 @@ class TagBox<
15581602
}
15591603

15601604
const dataController = this._dataController;
1561-
// @ts-expect-error ts-error
1605+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
15621606
const valueGetterExpr = this._valueGetterExpr();
15631607

15641608
if (isString(valueGetterExpr) && valueGetterExpr !== 'this') {
@@ -1580,14 +1624,14 @@ class TagBox<
15801624

15811625
_dataSourceFilterExpr() {
15821626
const filter = [];
1583-
// @ts-expect-error
1627+
// @ts-expect-error _valueGetterExpr is injected by DataExpressionMixin
15841628
this._getValue().forEach((value) => filter.push(['!', [this._valueGetterExpr(), value]]));
15851629

15861630
return filter;
15871631
}
15881632

15891633
_dataSourceFilterFunction(itemData) {
1590-
// @ts-expect-error ts-error
1634+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
15911635
const itemValue = this._valueGetter(itemData);
15921636
let result = true;
15931637

@@ -1636,7 +1680,7 @@ class TagBox<
16361680

16371681
return this
16381682
._getPlainItems(this._list.option('selectedItems'))
1639-
// @ts-expect-error
1683+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
16401684
.map((item) => this._valueGetter(item));
16411685
}
16421686

@@ -1695,15 +1739,15 @@ class TagBox<
16951739
}
16961740

16971741
const previousItemsValuesMap = previousItems.reduce((map, item) => {
1698-
// @ts-expect-error ts-error
1742+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
16991743
const value = this._valueGetter(item);
17001744
map[value] = item;
17011745
return map;
17021746
}, {});
17031747

17041748
const addedItems = [];
17051749
newItems.forEach((item) => {
1706-
// @ts-expect-error ts-error
1750+
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
17071751
const value = this._valueGetter(item);
17081752
if (!previousItemsValuesMap[value]) {
17091753
addedItems.push(item as never);

packages/devextreme/playground/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ window.addEventListener('load', () =>
2929
cardMinWidth: 320,
3030
columns: ['Company', 'Address', 'City', 'State', 'Zipcode', 'Phone'],
3131
});
32-
}));
32+
}));

packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/tagBox.tests.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,62 @@ QUnit.module('multi tag support', {
10781078
assert.deepEqual(this.getTexts($tagBox.find('.' + TAGBOX_TAG_CLASS)), ['1', '2'], 'tags have correct text');
10791079
});
10801080

1081+
1082+
QUnit.test('TagBox should preserve reverse click order in leading tag when showMultiTagOnly is false', function(assert) {
1083+
const $tagBox = $('#tagBox').dxTagBox({
1084+
items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
1085+
showSelectionControls: true,
1086+
maxDisplayedTags: 2,
1087+
showMultiTagOnly: false,
1088+
opened: true
1089+
});
1090+
1091+
const tagBox = $tagBox.dxTagBox('instance');
1092+
1093+
this.clock.tick(TIME_TO_WAIT);
1094+
1095+
const $listItems = getListItems(tagBox);
1096+
1097+
$listItems.last().trigger('dxclick');
1098+
$listItems.eq(7).trigger('dxclick');
1099+
$listItems.eq(6).trigger('dxclick');
1100+
1101+
1102+
assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), '10', 'leading tag has correct text');
1103+
});
1104+
1105+
QUnit.test('TagBox should work correctly with string ID\'s in item when valueExpr is used', function(assert) {
1106+
const items = [
1107+
{ ID: 'a', Name: 'HD Video Player' },
1108+
{ ID: 'b', Name: 'SuperHD Video Player' },
1109+
{ ID: 'c', Name: 'SuperPlasma 50' },
1110+
{ ID: 'd', Name: 'SuperLED 50' }
1111+
];
1112+
const $tagBox = $('#tagBox').dxTagBox({
1113+
items: items,
1114+
valueExpr: 'ID',
1115+
displayExpr: 'Name',
1116+
showSelectionControls: true,
1117+
maxDisplayedTags: 2,
1118+
showMultiTagOnly: false,
1119+
opened: true
1120+
});
1121+
1122+
const tagBox = $tagBox.dxTagBox('instance');
1123+
1124+
this.clock.tick(TIME_TO_WAIT);
1125+
1126+
const $listItems = getListItems(tagBox);
1127+
1128+
$listItems.last().trigger('dxclick');
1129+
$listItems.eq(2).trigger('dxclick');
1130+
$listItems.eq(1).trigger('dxclick');
1131+
1132+
1133+
assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), items[items.length - 1].Name, 'leading tag has correct text');
1134+
});
1135+
1136+
10811137
QUnit.test('only one multi tag should be rendered when selectAll checked and value changind on runtime', function(assert) {
10821138
let suppressSelectionChanged = false;
10831139

0 commit comments

Comments
 (0)