Skip to content
Merged
92 changes: 68 additions & 24 deletions packages/devextreme/js/__internal/ui/m_tag_box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ function xor(a: boolean, b: boolean): boolean {
return (a || b) && !(a && b);
}

type TagBoxItem = string | number | any;
type SelectedItemsMap = Record<string, TagBoxItem>;

const TAGBOX_TAG_DATA_KEY = 'dxTagData';
const TAGBOX_TAG_DISPLAY_VALUE = 'dxTagDisplayValue';

Expand All @@ -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<Properties,
'onCustomItemCreating'
| 'onItemClick' | 'onSelectionChanged'
| 'onOpened' | 'onClosed'
| 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste'
| 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> {

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<
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix only affects rendering the items, but not storing. So, _tagsToRender and _selectedItems will be inconsistent. Consider updating the storing order as well (if possible).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_selectedItems is using in several places, i decided not affect them in my first request

this._selectedItems,
this._valuesToUpdate,
);
this._renderTagsImpl();
isPlainDataUsed = true;
d.resolve();
Expand Down Expand Up @@ -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<SelectedItemsMap>((result, item) => {
// @ts-expect-error _valueGetter is injected by DataExpressionMixin
const itemValue = isValueExprDefault ? JSON.stringify(item) : this._valueGetter(item);
Comment thread
vorobey marked this conversation as resolved.
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;
Expand Down Expand Up @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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));
});

Expand Down Expand Up @@ -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') {
Expand All @@ -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;

Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -1699,15 +1743,15 @@ 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;
}, {});

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);
Expand Down
2 changes: 1 addition & 1 deletion packages/devextreme/playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ window.addEventListener('load', () =>
cardMinWidth: 320,
columns: ['Company', 'Address', 'City', 'State', 'Zipcode', 'Phone'],
});
}));
}));
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
vorobey marked this conversation as resolved.
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');
Comment thread
vorobey marked this conversation as resolved.


assert.strictEqual($tagBox.find('.' + TAGBOX_TAG_CLASS).first().text(), items[items.length - 1].Name, 'leading tag has correct text');
});

Comment thread
vorobey marked this conversation as resolved.

QUnit.test('only one multi tag should be rendered when selectAll checked and value changind on runtime', function(assert) {
let suppressSelectionChanged = false;

Expand Down
Loading