When resolving the remove item/label/icon, add a 3rd argument item argument. Update default remove item label to use (Fixes #1296)

This commit is contained in:
Xon 2025-07-10 03:56:32 +08:00
commit 5ca0350e8e
9 changed files with 65 additions and 41 deletions

View file

@ -703,21 +703,23 @@ Return type must be safe to insert into HTML (ie use the 1st argument which is s
### removeItemIconText
**Type:** `String/Function` **Default:** `Remove item"` **Arguments:** `value`, `valueRaw`
**Type:** `String/Function` **Default:** `Remove item"` **Arguments:** `value`, `valueRaw`, `item`
**Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** The text/icon for the remove button. To access the item's value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string.
To access the item's label, use the 3rd argument. *Note*; this label is not escaped.
Return type must be safe to insert into HTML (ie use the 1st argument which is sanitised)
### removeItemLabelText
**Type:** `String/Function` **Default:** `Remove item: ${value}"` **Arguments:** `value`, `valueRaw`
**Type:** `String/Function` **Default:** `Remove item: ${value}"` **Arguments:** `value`, `valueRaw`, `item`
**Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** The text for the remove button's aria label. To access the item's value, pass a function with a `value` argument (see the **default config** [https://github.com/jshjohnson/Choices#setup] for an example), otherwise pass a string.
To access the item's label, use the 3rd argument. *Note*; this label is not escaped.
Return type must be safe to insert into HTML (ie use the 1st argument which is sanitised)

View file

@ -13,6 +13,7 @@ import {
escapeForTemplate,
generateId,
getAdjacentEl,
getChoiceForOutput,
getClassNames,
getClassNamesSelector,
isScrolledIntoView,
@ -415,7 +416,7 @@ class Choices {
this._store.dispatch(highlightItem(choice, true));
if (runEvent) {
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(choice));
this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(choice));
}
return this;
@ -433,7 +434,7 @@ class Choices {
this._store.dispatch(highlightItem(choice, false));
if (runEvent) {
this.passedElement.triggerEvent(EventType.unhighlightItem, this._getChoiceForOutput(choice));
this.passedElement.triggerEvent(EventType.unhighlightItem, getChoiceForOutput(choice));
}
return this;
@ -445,7 +446,7 @@ class Choices {
if (!item.highlighted) {
this._store.dispatch(highlightItem(item, true));
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(item));
}
});
});
@ -459,7 +460,7 @@ class Choices {
if (item.highlighted) {
this._store.dispatch(highlightItem(item, false));
this.passedElement.triggerEvent(EventType.highlightItem, this._getChoiceForOutput(item));
this.passedElement.triggerEvent(EventType.highlightItem, getChoiceForOutput(item));
}
});
});
@ -545,7 +546,7 @@ class Choices {
getValue<B extends boolean = false>(valueOnly?: B): EventChoiceValueType<B> | EventChoiceValueType<B>[] {
const values = this._store.items.map((item) => {
return (valueOnly ? item.value : this._getChoiceForOutput(item)) as EventChoiceValueType<B>;
return (valueOnly ? item.value : getChoiceForOutput(item)) as EventChoiceValueType<B>;
});
return this._isSelectOneElement || this.config.singleModeForMultiSelect ? values[0] : values;
@ -848,7 +849,7 @@ class Choices {
this._searcher.reset();
if (choice.selected) {
this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(choice));
this.passedElement.triggerEvent(EventType.removeItem, getChoiceForOutput(choice));
}
return this;
@ -1178,23 +1179,12 @@ class Choices {
}
}
/**
* @deprecated Use utils.getChoiceForOutput
*/
// eslint-disable-next-line class-methods-use-this
_getChoiceForOutput(choice: ChoiceFull, keyCode?: number): EventChoice {
return {
id: choice.id,
highlighted: choice.highlighted,
labelClass: choice.labelClass,
labelDescription: choice.labelDescription,
customProperties: choice.customProperties,
disabled: choice.disabled,
active: choice.active,
label: choice.label,
placeholder: choice.placeholder,
value: choice.value,
groupValue: choice.group ? choice.group.label : undefined,
element: choice.element,
keyCode,
};
return getChoiceForOutput(choice, keyCode);
}
_triggerChange(value): void {
@ -1423,7 +1413,7 @@ class Choices {
if (canAddItem && typeof config.addItemFilter === 'function' && !config.addItemFilter(value)) {
canAddItem = false;
notice = resolveNoticeFunction(config.customAddItemText, value);
notice = resolveNoticeFunction(config.customAddItemText, value, undefined);
}
if (canAddItem) {
@ -1437,13 +1427,13 @@ class Choices {
}
if (!config.duplicateItemsAllowed) {
canAddItem = false;
notice = resolveNoticeFunction(config.uniqueItemText, value);
notice = resolveNoticeFunction(config.uniqueItemText, value, undefined);
}
}
}
if (canAddItem) {
notice = resolveNoticeFunction(config.addItemText, value);
notice = resolveNoticeFunction(config.addItemText, value, undefined);
}
if (notice) {
@ -2075,10 +2065,11 @@ class Choices {
this._store.dispatch(addItem(item));
if (withEvents) {
this.passedElement.triggerEvent(EventType.addItem, this._getChoiceForOutput(item));
const eventChoice = getChoiceForOutput(item);
this.passedElement.triggerEvent(EventType.addItem, eventChoice);
if (userTriggered) {
this.passedElement.triggerEvent(EventType.choice, this._getChoiceForOutput(item));
this.passedElement.triggerEvent(EventType.choice, eventChoice);
}
}
}
@ -2094,7 +2085,7 @@ class Choices {
this._clearNotice();
}
this.passedElement.triggerEvent(EventType.removeItem, this._getChoiceForOutput(item));
this.passedElement.triggerEvent(EventType.removeItem, getChoiceForOutput(item));
}
_addChoice(choice: ChoiceFull, withEvents: boolean = true, userTriggered = false): void {

View file

@ -1,6 +1,7 @@
import { ClassNames } from './interfaces/class-names';
import { Options } from './interfaces/options';
import { sortByAlpha } from './lib/utils';
import { sanitise, sortByAlpha } from './lib/utils';
import { EventChoice } from './interfaces';
export const DEFAULT_CLASSNAMES: ClassNames = {
containerOuter: ['choices'],
@ -79,7 +80,8 @@ export const DEFAULT_CONFIG: Options = {
customAddItemText: 'Only values matching specific conditions can be added',
addItemText: (value: string) => `Press Enter to add <b>"${value}"</b>`,
removeItemIconText: (): string => `Remove item`,
removeItemLabelText: (value: string): string => `Remove item: ${value}`,
removeItemLabelText: (value: string, _valueRaw: string, i?: EventChoice): string =>
`Remove item: ${i ? sanitise<string>(i.label) : value}`,
maxItemText: (maxItemCount: number): string => `Only ${maxItemCount} values can be added`,
valueComparer: (value1: string, value2: string): boolean => value1 === value2,
fuseOptions: {

View file

@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-cycle
import { InputChoice } from './input-choice';
export type EventChoiceValueType<B extends boolean> = B extends true ? string : EventChoice;

View file

@ -1,4 +1,5 @@
import { StringUntrusted } from './string-untrusted';
// eslint-disable-next-line
import { Types } from './types';
export interface InputChoice {
@ -13,5 +14,5 @@ export interface InputChoice {
placeholder?: boolean;
selected?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
value: any; // string;
}

View file

@ -200,7 +200,7 @@ export interface Options {
*
* @default
* ```
* (value, valueRaw) => `Remove item`;
* (value, valueRaw, item) => `Remove item`;
* ```
*/
removeItemIconText: string | Types.NoticeStringFunction;
@ -215,7 +215,7 @@ export interface Options {
*
* @default
* ```
* (value, valueRaw) => `Remove item: ${value}`;
* (value, valueRaw, item) => `Remove item: ${value}`;
* ```
*/
removeItemLabelText: string | Types.NoticeStringFunction;

View file

@ -1,12 +1,14 @@
import { StringUntrusted } from './string-untrusted';
import { StringPreEscaped } from './string-pre-escaped';
// eslint-disable-next-line import/no-cycle
import { EventChoice } from './event-choice';
export namespace Types {
export type StrToEl = (str: string) => HTMLElement | HTMLInputElement | HTMLOptionElement;
export type EscapeForTemplateFn = (allowHTML: boolean, s: StringUntrusted | StringPreEscaped | string) => string;
export type GetClassNamesFn = (s: string | Array<string>) => string;
export type StringFunction = () => string;
export type NoticeStringFunction = (value: string, valueRaw: string) => string;
export type NoticeStringFunction = (value: string, valueRaw: string, item?: EventChoice) => string;
export type NoticeLimitFunction = (maxItemCount: number) => string;
export type FilterFunction = (value: string) => boolean;
export type ValueCompareFunction = (value1: string, value2: string) => boolean;

View file

@ -4,6 +4,7 @@ import { StringPreEscaped } from '../interfaces/string-pre-escaped';
import { ChoiceFull } from '../interfaces/choice-full';
import { Types } from '../interfaces/types';
import { canUseDom } from '../interfaces/build-flags';
import { EventChoice } from '../interfaces';
const getRandomNumber = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min);
@ -91,10 +92,6 @@ export const strToEl = ((): ((str: string) => Element) => {
};
})();
export const resolveNoticeFunction = (fn: Types.NoticeStringFunction | string, value: string): string => {
return typeof fn === 'function' ? fn(sanitise(value), value) : fn;
};
export const resolveStringFunction = (fn: Types.StringFunction | string): string => {
return typeof fn === 'function' ? fn() : fn;
};
@ -133,6 +130,32 @@ export const unwrapStringForEscaped = (s?: StringUntrusted | StringPreEscaped |
return '';
};
export const getChoiceForOutput = (choice: ChoiceFull, keyCode?: number): EventChoice => {
return {
id: choice.id,
highlighted: choice.highlighted,
labelClass: choice.labelClass,
labelDescription: choice.labelDescription,
customProperties: choice.customProperties,
disabled: choice.disabled,
active: choice.active,
label: choice.label,
placeholder: choice.placeholder,
value: choice.value,
groupValue: choice.group ? choice.group.label : undefined,
element: choice.element,
keyCode,
};
};
export const resolveNoticeFunction = (
fn: Types.NoticeStringFunction | string,
value: StringUntrusted | StringPreEscaped | string,
item?: EventChoice,
): string => {
return typeof fn === 'function' ? fn(sanitise<string>(value), unwrapStringForRaw(value), item) : fn;
};
export const escapeForTemplate = (allowHTML: boolean, s: StringUntrusted | StringPreEscaped | string): string =>
allowHTML ? unwrapStringForEscaped(s) : (sanitise(s) as string);

View file

@ -16,6 +16,7 @@ import {
escapeForTemplate,
addClassesToElement,
removeClassesFromElement,
getChoiceForOutput,
} from './lib/utils';
import { NoticeType, NoticeTypes, TemplateOptions, Templates as TemplatesInterface } from './interfaces/templates';
import { StringUntrusted } from './interfaces/string-untrusted';
@ -189,9 +190,10 @@ const templates: TemplatesInterface = {
const removeButton = document.createElement('button');
removeButton.type = 'button';
addClassesToElement(removeButton, button);
setElementHtml(removeButton, true, resolveNoticeFunction(removeItemIconText, choice.value));
const eventChoice = getChoiceForOutput(choice);
setElementHtml(removeButton, true, resolveNoticeFunction(removeItemIconText, choice.value, eventChoice));
const REMOVE_ITEM_LABEL = resolveNoticeFunction(removeItemLabelText, choice.value);
const REMOVE_ITEM_LABEL = resolveNoticeFunction(removeItemLabelText, choice.value, eventChoice);
if (REMOVE_ITEM_LABEL) {
removeButton.setAttribute('aria-label', REMOVE_ITEM_LABEL);
}