taga11y 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/dist/a11y.d.ts +8 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/core/dropdown.d.ts +63 -0
- package/dist/core/dropdown.d.ts.map +1 -0
- package/dist/core/input.d.ts +42 -0
- package/dist/core/input.d.ts.map +1 -0
- package/dist/core/keyboard.d.ts +37 -0
- package/dist/core/keyboard.d.ts.map +1 -0
- package/dist/core/selection.d.ts +24 -0
- package/dist/core/selection.d.ts.map +1 -0
- package/dist/dom/chips.d.ts +6 -0
- package/dist/dom/chips.d.ts.map +1 -0
- package/dist/dom/listbox.d.ts +5 -0
- package/dist/dom/listbox.d.ts.map +1 -0
- package/dist/dom/render.d.ts +24 -0
- package/dist/dom/render.d.ts.map +1 -0
- package/dist/i18n.d.ts +44 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/index.d.ts +212 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/locales/ar.json +28 -0
- package/dist/locales/pt.json +21 -0
- package/dist/react/index.d.ts +46 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react.cjs +1 -0
- package/dist/react.mjs +83 -0
- package/dist/taga11y.cjs +1 -0
- package/dist/taga11y.css +2 -0
- package/dist/taga11y.iife.js +1 -0
- package/dist/taga11y.mjs +829 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/debounce.d.ts +5 -0
- package/dist/utils/debounce.d.ts.map +1 -0
- package/dist/utils/filter.d.ts +5 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/id.d.ts +11 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/vue/index.d.ts +155 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue.cjs +1 -0
- package/dist/vue.mjs +127 -0
- package/package.json +111 -0
- package/src/a11y.ts +85 -0
- package/src/aria-notify.d.ts +9 -0
- package/src/core/dropdown.ts +331 -0
- package/src/core/input.ts +172 -0
- package/src/core/keyboard.ts +200 -0
- package/src/core/selection.ts +78 -0
- package/src/dom/chips.ts +80 -0
- package/src/dom/listbox.ts +49 -0
- package/src/dom/render.ts +154 -0
- package/src/i18n.ts +131 -0
- package/src/index.ts +900 -0
- package/src/react/index.tsx +223 -0
- package/src/styles/index.css +367 -0
- package/src/types.ts +224 -0
- package/src/utils/debounce.ts +27 -0
- package/src/utils/filter.ts +14 -0
- package/src/utils/id.ts +30 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vue/index.ts +145 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import './styles/index.css';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
TagData,
|
|
5
|
+
Taga11yOptions,
|
|
6
|
+
SuggestionItem,
|
|
7
|
+
SuggestionsSource,
|
|
8
|
+
I18nOptions,
|
|
9
|
+
I18nStrings,
|
|
10
|
+
Taga11yEventMap,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
import { resolveStrings, applyLocale } from './i18n.js';
|
|
13
|
+
import { deriveBaseId, buildIds, type DerivedIds } from './utils/id.js';
|
|
14
|
+
import { getLabel, getValue } from './utils/filter.js';
|
|
15
|
+
import { SelectionManager } from './core/selection.js';
|
|
16
|
+
import { DropdownManager } from './core/dropdown.js';
|
|
17
|
+
import { InputManager } from './core/input.js';
|
|
18
|
+
import { KeyboardManager } from './core/keyboard.js';
|
|
19
|
+
import { render, setupCombobox, announce, type DomElements } from './dom/render.js';
|
|
20
|
+
import { updateChips, createErrorChip } from './dom/chips.js';
|
|
21
|
+
import {
|
|
22
|
+
announceTagAdded,
|
|
23
|
+
announceTagRemoved,
|
|
24
|
+
announceTagsCleared,
|
|
25
|
+
announceTagsSummary,
|
|
26
|
+
announceRejection,
|
|
27
|
+
showError,
|
|
28
|
+
} from './a11y.js';
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
TagData,
|
|
32
|
+
Taga11yOptions,
|
|
33
|
+
SuggestionItem,
|
|
34
|
+
SuggestionsSource,
|
|
35
|
+
I18nOptions,
|
|
36
|
+
I18nStrings,
|
|
37
|
+
};
|
|
38
|
+
export type { PluralForms, I18nKey } from './types.js';
|
|
39
|
+
export type {
|
|
40
|
+
Taga11yEventMap,
|
|
41
|
+
Taga11yAddDetail,
|
|
42
|
+
Taga11yRemoveDetail,
|
|
43
|
+
Taga11yClearDetail,
|
|
44
|
+
Taga11yChangeDetail,
|
|
45
|
+
Taga11yPasteDetail,
|
|
46
|
+
Taga11yDestroyDetail,
|
|
47
|
+
} from './types.js';
|
|
48
|
+
|
|
49
|
+
interface ResolvedOptions {
|
|
50
|
+
suggestions: SuggestionsSource | undefined;
|
|
51
|
+
maxTags: number | undefined;
|
|
52
|
+
maxSuggestions: number;
|
|
53
|
+
delimiter: string[];
|
|
54
|
+
enforceSuggestions: boolean;
|
|
55
|
+
name: string;
|
|
56
|
+
label: string | undefined;
|
|
57
|
+
disabled: boolean;
|
|
58
|
+
theme: 'dark' | 'light' | null;
|
|
59
|
+
serialize: (tags: TagData[]) => string;
|
|
60
|
+
deserialize: (raw: string) => SuggestionItem[];
|
|
61
|
+
debounceMs: number;
|
|
62
|
+
i18n: I18nOptions | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Accessibility-first tagging / multi-select input component.
|
|
67
|
+
*
|
|
68
|
+
* Wraps a native `<input>` element in an accessible combobox widget
|
|
69
|
+
* with inline chips, a keyboard-navigable listbox popup, screen reader
|
|
70
|
+
* announcements, and form integration via a hidden input.
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* const widget = new Taga11y(inputEl, {
|
|
74
|
+
* suggestions: ['JavaScript', 'TypeScript', 'Rust'],
|
|
75
|
+
* maxTags: 10,
|
|
76
|
+
* enforceSuggestions: false,
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* widget.addTag('Rust');
|
|
80
|
+
* console.log(widget.tags); // [{ id: '…', label: 'Rust', value: 'Rust' }]
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* ## Custom events
|
|
84
|
+
*
|
|
85
|
+
* Dispatched on the original input element. All bubble and are not
|
|
86
|
+
* cancelable. Subscribe via {@link Taga11y.on} or
|
|
87
|
+
* `inputEl.addEventListener`.
|
|
88
|
+
*
|
|
89
|
+
* - `taga11y:add` — detail `{ tag: TagData }`. Fired per tag added,
|
|
90
|
+
* including each chunk of a delimited paste.
|
|
91
|
+
* - `taga11y:remove` — detail `{ tag: TagData }`.
|
|
92
|
+
* - `taga11y:clear` — detail `{ tags: TagData[] }`. The tags that were
|
|
93
|
+
* removed.
|
|
94
|
+
* - `taga11y:change` — detail `{ tags: TagData[] }`. Final tag set
|
|
95
|
+
* after any mutation. Fired once after `addTags`, `setTags`, or a
|
|
96
|
+
* delimited paste.
|
|
97
|
+
* - `taga11y:paste` — detail `{ added: TagData[], skipped: string[] }`.
|
|
98
|
+
* Fired once after a paste gesture that contained a configured
|
|
99
|
+
* single-character delimiter. `skipped` lists chunks rejected by
|
|
100
|
+
* whitelist mode, duplicate detection, or `maxTags`. Not fired for
|
|
101
|
+
* paste of text containing no delimiter.
|
|
102
|
+
* - `taga11y:destroy` — detail `{}`.
|
|
103
|
+
*
|
|
104
|
+
* See [docs/guides/events.md](../guides/events.md) for the full
|
|
105
|
+
* reference and examples.
|
|
106
|
+
*/
|
|
107
|
+
function resolveTheme(value: unknown): 'dark' | 'light' | null {
|
|
108
|
+
if (value == null) return null;
|
|
109
|
+
if (value === 'dark' || value === 'light') return value;
|
|
110
|
+
throw new TypeError(`taga11y: theme must be "dark", "light", or null — received "${value}"`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class Taga11y {
|
|
114
|
+
private readonly _input: HTMLInputElement;
|
|
115
|
+
private readonly _ids: DerivedIds;
|
|
116
|
+
private readonly _originalName: string;
|
|
117
|
+
private readonly _originalId: string;
|
|
118
|
+
private readonly _originalValue: string;
|
|
119
|
+
private readonly _originalParent: ParentNode | null;
|
|
120
|
+
private readonly _originalNextSibling: ChildNode | null;
|
|
121
|
+
|
|
122
|
+
private _dom!: DomElements;
|
|
123
|
+
private _selection!: SelectionManager;
|
|
124
|
+
private _dropdown!: DropdownManager;
|
|
125
|
+
private _inputMgr!: InputManager;
|
|
126
|
+
private _keyboard!: KeyboardManager;
|
|
127
|
+
private _opts: ResolvedOptions;
|
|
128
|
+
private readonly _strings: I18nStrings;
|
|
129
|
+
private readonly _locale: string;
|
|
130
|
+
private _initialSnapshot: SuggestionItem[] = [];
|
|
131
|
+
private _formResetHandler: (() => void) | null = null;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates a new taga11y instance and immediately initialises it.
|
|
135
|
+
*
|
|
136
|
+
* @param input - The native `<input>` element to enhance.
|
|
137
|
+
* @param options - Configuration options for the widget.
|
|
138
|
+
* @throws If `suggestions` defines both `once` and `query` keys.
|
|
139
|
+
*/
|
|
140
|
+
constructor(input: HTMLInputElement, options: Taga11yOptions = {}) {
|
|
141
|
+
this._input = input;
|
|
142
|
+
this._originalName = input.name;
|
|
143
|
+
this._originalId = input.id;
|
|
144
|
+
this._originalValue = input.value;
|
|
145
|
+
this._originalParent = input.parentNode;
|
|
146
|
+
this._originalNextSibling = input.nextSibling;
|
|
147
|
+
this._ids = buildIds(deriveBaseId(input));
|
|
148
|
+
this._opts = this._parseOptions(options);
|
|
149
|
+
this._strings = resolveStrings(this._opts.i18n?.strings);
|
|
150
|
+
this._locale = this._opts.i18n?.locale ?? 'en';
|
|
151
|
+
this.init();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── 10.3 init ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Initialises the widget: builds the DOM structure, wires up managers,
|
|
158
|
+
* pre-populates tags from the original input value, and sets up form
|
|
159
|
+
* reset handling.
|
|
160
|
+
*
|
|
161
|
+
* Called automatically by the constructor. Invoke again after
|
|
162
|
+
* {@link destroy} to re-initialise.
|
|
163
|
+
*/
|
|
164
|
+
init(): void {
|
|
165
|
+
this._initialSnapshot = this._opts.deserialize(this._input.value);
|
|
166
|
+
// Chips are the sole visible representation; clear both the current
|
|
167
|
+
// value and the HTML default so native form.reset() doesn't repopulate
|
|
168
|
+
// the raw serialized string into the combobox.
|
|
169
|
+
this._input.value = '';
|
|
170
|
+
this._input.defaultValue = '';
|
|
171
|
+
|
|
172
|
+
setupCombobox(this._input, this._ids);
|
|
173
|
+
|
|
174
|
+
this._dom = render(this._input, this._ids, {
|
|
175
|
+
...(this._opts.label !== undefined ? { label: this._opts.label } : {}),
|
|
176
|
+
name: this._opts.name,
|
|
177
|
+
initialValue: '',
|
|
178
|
+
strings: this._strings,
|
|
179
|
+
locale: this._locale,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (this._opts.theme) {
|
|
183
|
+
this._dom.wrapper.setAttribute('data-theme', this._opts.theme);
|
|
184
|
+
}
|
|
185
|
+
if (this._opts.disabled) {
|
|
186
|
+
this._dom.wrapper.classList.add('taga11y--disabled');
|
|
187
|
+
this._input.disabled = true;
|
|
188
|
+
this._dom.hiddenInput.disabled = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Insert wrapper into DOM where the input was
|
|
192
|
+
if (this._originalParent) {
|
|
193
|
+
this._originalParent.insertBefore(this._dom.wrapper, this._originalNextSibling);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this._opts.i18n) {
|
|
197
|
+
applyLocale(this._dom.wrapper, this._opts.i18n.locale, this._opts.i18n.dir);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this._selection = new SelectionManager(this._ids.base, this._opts.maxTags);
|
|
201
|
+
|
|
202
|
+
this._dropdown = new DropdownManager(
|
|
203
|
+
{
|
|
204
|
+
listbox: this._dom.listbox,
|
|
205
|
+
input: this._input,
|
|
206
|
+
loading: this._dom.loading,
|
|
207
|
+
wrapper: this._dom.wrapper,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
...(this._opts.suggestions !== undefined ? { suggestions: this._opts.suggestions } : {}),
|
|
211
|
+
debounceMs: this._opts.debounceMs,
|
|
212
|
+
maxSuggestions: this._opts.maxSuggestions,
|
|
213
|
+
getSelectedValues: () => this._selection.getTags().map((t) => t.value),
|
|
214
|
+
ids: this._ids,
|
|
215
|
+
strings: this._strings,
|
|
216
|
+
locale: this._locale,
|
|
217
|
+
onOptionSelect: (item) => {
|
|
218
|
+
this._dropdown.close();
|
|
219
|
+
this._handleCommit(getValue(item));
|
|
220
|
+
this._inputMgr.clearValue();
|
|
221
|
+
},
|
|
222
|
+
onAnnounce: (msg) => announce(this._dom.announcer, msg),
|
|
223
|
+
onLoadError: (msg) => showError(this._dom.error, msg),
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
this._inputMgr = new InputManager(this._input, {
|
|
228
|
+
onInput: (value) => this._dropdown.filter(value),
|
|
229
|
+
onBlurCommit: (value) => this._handleBlurCommit(value),
|
|
230
|
+
onBlurClose: () => this._dropdown.close(),
|
|
231
|
+
onFocus: () => this._announceTagsSummary(),
|
|
232
|
+
getDelimiters: () => this._opts.delimiter,
|
|
233
|
+
isEnforceSuggestions: () => this._opts.enforceSuggestions,
|
|
234
|
+
getLoadedSuggestions: () => this._dropdown.getAllSuggestions(),
|
|
235
|
+
onCommit: (value) => this._handleCommit(value),
|
|
236
|
+
onCommitRejected: (value) => this._showRejection(value, 'not-in-list'),
|
|
237
|
+
onPaste: (chunks) => this._handlePaste(chunks),
|
|
238
|
+
onCloseDropdown: () => this._dropdown.close(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this._keyboard = new KeyboardManager(this._input, {
|
|
242
|
+
onCommit: (value) => this._handleCommit(value),
|
|
243
|
+
onCommitRejected: (value) => this._showRejection(value, 'not-in-list'),
|
|
244
|
+
onRemoveLast: () => this._handleRemoveLast(),
|
|
245
|
+
clearInput: () => this._inputMgr.clearValue(),
|
|
246
|
+
hasChips: () => this._selection.getTags().length > 0,
|
|
247
|
+
getDropdown: () => this._dropdown,
|
|
248
|
+
getDelimiters: () => this._opts.delimiter,
|
|
249
|
+
isEnforceSuggestions: () => this._opts.enforceSuggestions,
|
|
250
|
+
getSuggestionSource: () => this._opts.suggestions,
|
|
251
|
+
getLoadedSuggestions: () => this._dropdown.getAllSuggestions(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Pre-populate from original input value
|
|
255
|
+
for (const item of this._initialSnapshot) {
|
|
256
|
+
const value = getValue(item);
|
|
257
|
+
const label =
|
|
258
|
+
typeof item === 'string' ? this._resolveLabelFromAll(value) : getLabel(item);
|
|
259
|
+
this._selection.addTag(value, label);
|
|
260
|
+
}
|
|
261
|
+
this._syncChips();
|
|
262
|
+
this._syncHiddenInput();
|
|
263
|
+
|
|
264
|
+
// Form reset
|
|
265
|
+
const form = this._input.closest('form');
|
|
266
|
+
if (form) {
|
|
267
|
+
this._formResetHandler = () => this._handleFormReset();
|
|
268
|
+
form.addEventListener('reset', this._formResetHandler);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── 10.4 destroy ──────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Tears down the widget: removes the injected DOM, detaches all event
|
|
276
|
+
* listeners, restores the original input's attributes, and moves it
|
|
277
|
+
* back to its original DOM position.
|
|
278
|
+
*
|
|
279
|
+
* Fires a `taga11y:destroy` event on the original input.
|
|
280
|
+
*/
|
|
281
|
+
destroy(): void {
|
|
282
|
+
this._keyboard.destroy();
|
|
283
|
+
this._inputMgr.destroy();
|
|
284
|
+
this._dropdown.destroy();
|
|
285
|
+
|
|
286
|
+
const form = this._input.closest('form');
|
|
287
|
+
if (form && this._formResetHandler) {
|
|
288
|
+
form.removeEventListener('reset', this._formResetHandler);
|
|
289
|
+
this._formResetHandler = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Move input back to original position
|
|
293
|
+
if (this._originalParent) {
|
|
294
|
+
this._originalParent.insertBefore(this._input, this._originalNextSibling);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Restore original attributes
|
|
298
|
+
if (this._originalName) this._input.setAttribute('name', this._originalName);
|
|
299
|
+
if (!this._originalId) {
|
|
300
|
+
this._input.removeAttribute('id');
|
|
301
|
+
}
|
|
302
|
+
this._input.value = this._originalValue;
|
|
303
|
+
this._input.defaultValue = this._originalValue;
|
|
304
|
+
this._input.removeAttribute('role');
|
|
305
|
+
this._input.removeAttribute('aria-autocomplete');
|
|
306
|
+
this._input.removeAttribute('aria-haspopup');
|
|
307
|
+
this._input.removeAttribute('aria-expanded');
|
|
308
|
+
this._input.removeAttribute('aria-controls');
|
|
309
|
+
this._input.removeAttribute('aria-activedescendant');
|
|
310
|
+
this._input.disabled = false;
|
|
311
|
+
|
|
312
|
+
this._dom.wrapper.remove();
|
|
313
|
+
this.emit('taga11y:destroy', {});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 10.5 addTag ───────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Adds a single tag by value.
|
|
320
|
+
*
|
|
321
|
+
* Resolves the display label from loaded suggestions (matching on
|
|
322
|
+
* `value`), falling back to `label === value` if no match exists.
|
|
323
|
+
*
|
|
324
|
+
* @param value - The tag value to add.
|
|
325
|
+
* @returns `true` if the tag was added; `false` if rejected
|
|
326
|
+
* (duplicate or max reached).
|
|
327
|
+
*
|
|
328
|
+
* Fires `taga11y:add` and `taga11y:change` on success.
|
|
329
|
+
* Fires no events on rejection (shows error chip + region instead).
|
|
330
|
+
*/
|
|
331
|
+
addTag(value: string): boolean {
|
|
332
|
+
const label = this._resolveLabelFromAll(value);
|
|
333
|
+
const result = this._selection.addTag(value, label);
|
|
334
|
+
if (result.ok) {
|
|
335
|
+
this._syncChips();
|
|
336
|
+
this._syncHiddenInput();
|
|
337
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
338
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
339
|
+
announceTagAdded(this._dom.announcer, this._strings, this._locale, result.tag.label);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
this._showRejection(value, result.reason);
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── 10.6 removeTag ────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Removes a single tag by value.
|
|
350
|
+
*
|
|
351
|
+
* @param value - The tag value to remove.
|
|
352
|
+
* @returns The removed `TagData`, or `null` if no tag with that
|
|
353
|
+
* value exists.
|
|
354
|
+
*
|
|
355
|
+
* Fires `taga11y:remove` and `taga11y:change`. Returns focus to
|
|
356
|
+
* the combobox input after removal.
|
|
357
|
+
*/
|
|
358
|
+
removeTag(value: string): TagData | null {
|
|
359
|
+
const tag = this._selection.getTags().find((t) => t.value === value);
|
|
360
|
+
if (!tag) return null;
|
|
361
|
+
const removed = this._selection.removeTag(tag.id);
|
|
362
|
+
if (!removed) return null;
|
|
363
|
+
this._syncChips();
|
|
364
|
+
this._syncHiddenInput();
|
|
365
|
+
this.emit('taga11y:remove', { tag: removed });
|
|
366
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
367
|
+
announceTagRemoved(this._dom.announcer, this._strings, this._locale, removed.label);
|
|
368
|
+
this._input.focus();
|
|
369
|
+
return removed;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── 10.7 clearTags ────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Removes all tags at once.
|
|
376
|
+
*
|
|
377
|
+
* Fires `taga11y:clear` (detail: `{ tags: TagData[] }`) and
|
|
378
|
+
* `taga11y:change` (detail: `{ tags: [] }`).
|
|
379
|
+
*/
|
|
380
|
+
clearTags(): void {
|
|
381
|
+
const cleared = this._selection.clearTags();
|
|
382
|
+
this._syncChips();
|
|
383
|
+
this._syncHiddenInput();
|
|
384
|
+
this.emit('taga11y:clear', { tags: cleared });
|
|
385
|
+
this.emit('taga11y:change', { tags: [] });
|
|
386
|
+
announceTagsCleared(this._dom.announcer, this._strings, this._locale);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── 10.8 getTags ──────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Returns a defensive copy of the current tag array.
|
|
393
|
+
*
|
|
394
|
+
* @returns Array of `TagData` objects representing selected tags.
|
|
395
|
+
*/
|
|
396
|
+
getTags(): TagData[] {
|
|
397
|
+
return this._selection.getTags();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── 10.9 addTags ──────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Batch-adds multiple tags. Duplicates are skipped silently.
|
|
404
|
+
*
|
|
405
|
+
* Fires `taga11y:add` per successfully added tag and a single
|
|
406
|
+
* `taga11y:change` after all additions with the final tag set.
|
|
407
|
+
*
|
|
408
|
+
* @param items - Suggestion items to add.
|
|
409
|
+
*/
|
|
410
|
+
addTags(items: SuggestionItem[]): void {
|
|
411
|
+
let anyAdded = false;
|
|
412
|
+
for (const item of items) {
|
|
413
|
+
const value = getValue(item);
|
|
414
|
+
const label = this._resolveLabelFromAll(value) || getLabel(item);
|
|
415
|
+
const result = this._selection.addTag(value, label);
|
|
416
|
+
if (result.ok) {
|
|
417
|
+
anyAdded = true;
|
|
418
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (anyAdded) {
|
|
422
|
+
this._syncChips();
|
|
423
|
+
this._syncHiddenInput();
|
|
424
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── 10.10 setTags ─────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
*Fully replaces all tags with the given items.
|
|
432
|
+
*
|
|
433
|
+
* Fires `taga11y:clear` with the previously selected tags, then
|
|
434
|
+
* `taga11y:add` per newly added tag, and finally a single
|
|
435
|
+
* `taga11y:change` with the final tag set.
|
|
436
|
+
*
|
|
437
|
+
* @param items - Suggestion items to set as the new tag set.
|
|
438
|
+
*/
|
|
439
|
+
setTags(items: SuggestionItem[]): void {
|
|
440
|
+
const cleared = this._selection.clearTags();
|
|
441
|
+
this.emit('taga11y:clear', { tags: cleared });
|
|
442
|
+
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
const value = getValue(item);
|
|
445
|
+
const label = this._resolveLabelFromAll(value) || getLabel(item);
|
|
446
|
+
const result = this._selection.addTag(value, label);
|
|
447
|
+
if (result.ok) {
|
|
448
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
this._syncChips();
|
|
452
|
+
this._syncHiddenInput();
|
|
453
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── 10.11 isTagged ────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Checks whether a tag with the given value is currently selected.
|
|
460
|
+
*
|
|
461
|
+
* @param value - The tag value to check.
|
|
462
|
+
* @returns `true` if a tag with the matching value exists.
|
|
463
|
+
*/
|
|
464
|
+
isTagged(value: string): boolean {
|
|
465
|
+
return this._selection.isTagged(value);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── 10.12 / 10.13 open / close ────────────────────────────
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Opens the suggestion dropdown. No-op if already open or disabled.
|
|
472
|
+
*/
|
|
473
|
+
open(): void {
|
|
474
|
+
if (this._opts.disabled) return;
|
|
475
|
+
if (!this._dropdown.isOpen) this._dropdown.open();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Closes the suggestion dropdown. No-op if already closed.
|
|
480
|
+
*/
|
|
481
|
+
close(): void {
|
|
482
|
+
if (this._dropdown.isOpen) this._dropdown.close();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── 10.14 / 10.15 focus / blur ────────────────────────────
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Focuses the combobox input element.
|
|
489
|
+
*/
|
|
490
|
+
focus(): void {
|
|
491
|
+
this._input.focus();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Blurs the combobox input element.
|
|
496
|
+
*/
|
|
497
|
+
blur(): void {
|
|
498
|
+
this._input.blur();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── 10.16 settings ────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Updates one or more runtime options after initialisation.
|
|
505
|
+
*
|
|
506
|
+
* Only the provided keys are applied; unprovided keys retain their
|
|
507
|
+
* current values. Each option triggers specific side effects:
|
|
508
|
+
*
|
|
509
|
+
* - `disabled` — enables/disables the input, hidden input, and chip
|
|
510
|
+
* remove buttons; toggles `.taga11y--disabled` class.
|
|
511
|
+
* - `theme` — sets or removes `data-theme` on the wrapper.
|
|
512
|
+
* - `maxTags` — updates the selection manager's limit.
|
|
513
|
+
* - `maxSuggestions` — updates the dropdown display cap; takes effect on the
|
|
514
|
+
* next filter (does not re-render an already-open dropdown).
|
|
515
|
+
* - `delimiter` — updates delimiter characters.
|
|
516
|
+
* - `enforceSuggestions` — toggles whitelist mode.
|
|
517
|
+
* - `label` — creates, updates, or removes the visible label element.
|
|
518
|
+
* - `suggestions` — replaces the suggestion source.
|
|
519
|
+
* - `debounceMs` — updates the debounce delay for dynamic mode.
|
|
520
|
+
* - `serialize` — updates the hidden input serializer.
|
|
521
|
+
* - `deserialize` — updates the initial-value parser; takes effect on the
|
|
522
|
+
* next form reset (does not retroactively re-parse current tags).
|
|
523
|
+
* - `name` — updates the hidden input's name attribute.
|
|
524
|
+
*
|
|
525
|
+
* @param newOptions - Partial options object with keys to update.
|
|
526
|
+
*/
|
|
527
|
+
settings(newOptions: Partial<Taga11yOptions>): void {
|
|
528
|
+
if ('i18n' in newOptions) {
|
|
529
|
+
console.warn(
|
|
530
|
+
'taga11y: i18n is init-only and cannot be changed via settings(); ' +
|
|
531
|
+
'use destroy() + new Taga11y() to switch locale. Ignoring i18n; ' +
|
|
532
|
+
'other provided keys are still applied.',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if ('disabled' in newOptions) {
|
|
537
|
+
const disabled = newOptions.disabled ?? false;
|
|
538
|
+
this._opts.disabled = disabled;
|
|
539
|
+
this._input.disabled = disabled;
|
|
540
|
+
this._dom.hiddenInput.disabled = disabled;
|
|
541
|
+
this._dom.tagList
|
|
542
|
+
.querySelectorAll<HTMLButtonElement>('.taga11y__tag-remove')
|
|
543
|
+
.forEach((btn) => { btn.hidden = disabled; });
|
|
544
|
+
this._dom.wrapper.classList.toggle('taga11y--disabled', disabled);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if ('theme' in newOptions) {
|
|
548
|
+
const theme = resolveTheme(newOptions.theme);
|
|
549
|
+
this._opts.theme = theme;
|
|
550
|
+
if (theme) {
|
|
551
|
+
this._dom.wrapper.setAttribute('data-theme', theme);
|
|
552
|
+
} else {
|
|
553
|
+
this._dom.wrapper.removeAttribute('data-theme');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if ('maxTags' in newOptions) {
|
|
558
|
+
this._opts.maxTags = newOptions.maxTags;
|
|
559
|
+
this._selection.setMaxTags(newOptions.maxTags);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if ('maxSuggestions' in newOptions) {
|
|
563
|
+
this._opts.maxSuggestions = Math.max(0, newOptions.maxSuggestions ?? 10);
|
|
564
|
+
this._dropdown.setMaxSuggestions(this._opts.maxSuggestions);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if ('delimiter' in newOptions) {
|
|
568
|
+
const d = newOptions.delimiter;
|
|
569
|
+
this._opts.delimiter = Array.isArray(d) ? d : d ? [d] : [',', 'Enter'];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if ('enforceSuggestions' in newOptions) {
|
|
573
|
+
this._opts.enforceSuggestions = newOptions.enforceSuggestions ?? false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if ('label' in newOptions) {
|
|
577
|
+
const labelText = newOptions.label ?? null;
|
|
578
|
+
let labelEl = this._dom.wrapper.querySelector<HTMLLabelElement>('.taga11y__label');
|
|
579
|
+
if (labelText == null) {
|
|
580
|
+
labelEl?.remove();
|
|
581
|
+
} else {
|
|
582
|
+
if (!labelEl) {
|
|
583
|
+
labelEl = document.createElement('label');
|
|
584
|
+
labelEl.className = 'taga11y__label';
|
|
585
|
+
labelEl.htmlFor = this._input.id;
|
|
586
|
+
this._dom.wrapper.insertBefore(labelEl, this._dom.wrapper.firstChild);
|
|
587
|
+
}
|
|
588
|
+
labelEl.textContent = labelText;
|
|
589
|
+
}
|
|
590
|
+
this._opts.label = labelText ?? undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if ('suggestions' in newOptions) {
|
|
594
|
+
this._opts.suggestions = newOptions.suggestions;
|
|
595
|
+
this._dropdown.setSource(newOptions.suggestions, this._opts.debounceMs);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if ('debounceMs' in newOptions) {
|
|
599
|
+
this._opts.debounceMs = Math.max(0, newOptions.debounceMs ?? 200);
|
|
600
|
+
this._dropdown.setDebounceMs(this._opts.debounceMs);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if ('serialize' in newOptions && newOptions.serialize) {
|
|
604
|
+
this._opts.serialize = newOptions.serialize;
|
|
605
|
+
this._syncHiddenInput();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if ('deserialize' in newOptions && newOptions.deserialize) {
|
|
609
|
+
this._opts.deserialize = newOptions.deserialize;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if ('name' in newOptions) {
|
|
613
|
+
const name = newOptions.name ?? '';
|
|
614
|
+
this._opts.name = name;
|
|
615
|
+
this._dom.hiddenInput.name = name;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── 10.17 / 10.18 on / off ────────────────────────────────
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Registers a listener for a taga11y custom event on the original
|
|
623
|
+
* input element.
|
|
624
|
+
*
|
|
625
|
+
* @param event - Event name. One of `"taga11y:add"`,
|
|
626
|
+
* `"taga11y:remove"`, `"taga11y:clear"`, `"taga11y:change"`,
|
|
627
|
+
* `"taga11y:paste"`, or `"taga11y:destroy"`. A known event name infers
|
|
628
|
+
* the handler's `CustomEvent` detail; any other string is also accepted.
|
|
629
|
+
* @param handler - Event listener callback.
|
|
630
|
+
*/
|
|
631
|
+
on<K extends keyof Taga11yEventMap>(
|
|
632
|
+
event: K,
|
|
633
|
+
handler: (event: CustomEvent<Taga11yEventMap[K]>) => void,
|
|
634
|
+
): void;
|
|
635
|
+
on(event: string, handler: EventListener): void;
|
|
636
|
+
on(event: string, handler: (event: CustomEvent<never>) => void): void {
|
|
637
|
+
this._input.addEventListener(event, handler as EventListener);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Removes a previously registered event listener.
|
|
642
|
+
*
|
|
643
|
+
* @param event - Event name. Typed identically to {@link Taga11y.on}, so the
|
|
644
|
+
* same typed handler reference can be passed to both.
|
|
645
|
+
* @param handler - The handler to remove.
|
|
646
|
+
*/
|
|
647
|
+
off<K extends keyof Taga11yEventMap>(
|
|
648
|
+
event: K,
|
|
649
|
+
handler: (event: CustomEvent<Taga11yEventMap[K]>) => void,
|
|
650
|
+
): void;
|
|
651
|
+
off(event: string, handler: EventListener): void;
|
|
652
|
+
off(event: string, handler: (event: CustomEvent<never>) => void): void {
|
|
653
|
+
this._input.removeEventListener(event, handler as EventListener);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// ── 10.19 emit ────────────────────────────────────────────
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Dispatches a custom event on the original input element.
|
|
660
|
+
*
|
|
661
|
+
* All events bubble and are not cancelable.
|
|
662
|
+
*
|
|
663
|
+
* @param event - Event name.
|
|
664
|
+
* @param data - Event detail payload.
|
|
665
|
+
*/
|
|
666
|
+
emit(event: string, data: Record<string, unknown>): void {
|
|
667
|
+
this._input.dispatchEvent(
|
|
668
|
+
new CustomEvent(event, { detail: data, bubbles: true, cancelable: false }),
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── 10.20 Computed getters ────────────────────────────────
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Read-only property returning the current tag array (defensive copy).
|
|
676
|
+
* Equivalent to `getTags()`.
|
|
677
|
+
*/
|
|
678
|
+
get tags(): TagData[] {
|
|
679
|
+
return this._selection.getTags();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Read-only property indicating whether the suggestion dropdown
|
|
684
|
+
* is currently visible.
|
|
685
|
+
*/
|
|
686
|
+
get isOpen(): boolean {
|
|
687
|
+
return this._dropdown.isOpen;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Read-only property indicating whether the maximum tag count
|
|
692
|
+
* has been reached (when `maxTags` is configured).
|
|
693
|
+
*/
|
|
694
|
+
get maxTagsReached(): boolean {
|
|
695
|
+
if (this._opts.maxTags === undefined) return false;
|
|
696
|
+
return this._selection.getTags().length >= this._opts.maxTags;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── Private helpers ────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
private _parseOptions(options: Taga11yOptions): ResolvedOptions {
|
|
702
|
+
if (options.suggestions && !Array.isArray(options.suggestions)) {
|
|
703
|
+
const src = options.suggestions as Record<string, unknown>;
|
|
704
|
+
if ('once' in src && 'query' in src) {
|
|
705
|
+
throw new Error("suggestions source cannot define both 'once' and 'query'");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const delimiter = options.delimiter;
|
|
710
|
+
return {
|
|
711
|
+
suggestions: options.suggestions,
|
|
712
|
+
maxTags: options.maxTags,
|
|
713
|
+
maxSuggestions: Math.max(0, options.maxSuggestions ?? 10),
|
|
714
|
+
delimiter: Array.isArray(delimiter)
|
|
715
|
+
? delimiter
|
|
716
|
+
: delimiter
|
|
717
|
+
? [delimiter]
|
|
718
|
+
: [',', 'Enter'],
|
|
719
|
+
enforceSuggestions: options.enforceSuggestions ?? false,
|
|
720
|
+
name: options.name ?? this._originalName,
|
|
721
|
+
label: options.label,
|
|
722
|
+
disabled: options.disabled ?? false,
|
|
723
|
+
theme: resolveTheme(options.theme),
|
|
724
|
+
serialize: options.serialize ?? ((tags) => tags.map((t) => t.value).join(',')),
|
|
725
|
+
deserialize:
|
|
726
|
+
options.deserialize ??
|
|
727
|
+
((raw) => (raw ? raw.split(',').map((v) => v.trim()).filter(Boolean) : [])),
|
|
728
|
+
debounceMs: Math.max(0, options.debounceMs ?? 200),
|
|
729
|
+
i18n: options.i18n,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private _handleCommit(value: string): void {
|
|
734
|
+
// Value arrives here after whitelist check already passed in KeyboardManager
|
|
735
|
+
const label = this._resolveLabelFromAll(value);
|
|
736
|
+
const result = this._selection.addTag(value, label);
|
|
737
|
+
if (result.ok) {
|
|
738
|
+
this._syncChips();
|
|
739
|
+
this._syncHiddenInput();
|
|
740
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
741
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
742
|
+
announceTagAdded(this._dom.announcer, this._strings, this._locale, result.tag.label);
|
|
743
|
+
} else {
|
|
744
|
+
this._showRejection(value, result.reason);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private _handlePaste(chunks: string[]): void {
|
|
749
|
+
const added: TagData[] = [];
|
|
750
|
+
const skipped: string[] = [];
|
|
751
|
+
|
|
752
|
+
for (const chunk of chunks) {
|
|
753
|
+
let value: string;
|
|
754
|
+
let label: string;
|
|
755
|
+
if (this._opts.enforceSuggestions) {
|
|
756
|
+
const lower = chunk.toLowerCase();
|
|
757
|
+
const match = this._dropdown.getAllSuggestions().find(
|
|
758
|
+
(item) => getLabel(item).toLowerCase() === lower,
|
|
759
|
+
);
|
|
760
|
+
if (!match) {
|
|
761
|
+
skipped.push(chunk);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
value = getValue(match);
|
|
765
|
+
label = getLabel(match);
|
|
766
|
+
} else {
|
|
767
|
+
value = chunk;
|
|
768
|
+
label = chunk;
|
|
769
|
+
}
|
|
770
|
+
const result = this._selection.addTag(value, label);
|
|
771
|
+
if (result.ok) {
|
|
772
|
+
added.push(result.tag);
|
|
773
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
774
|
+
} else {
|
|
775
|
+
skipped.push(chunk);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (added.length > 0) {
|
|
780
|
+
this._syncChips();
|
|
781
|
+
this._syncHiddenInput();
|
|
782
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
783
|
+
}
|
|
784
|
+
this.emit('taga11y:paste', { added, skipped });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private _handleBlurCommit(value: string): void {
|
|
788
|
+
if (this._opts.enforceSuggestions) {
|
|
789
|
+
const lower = value.toLowerCase();
|
|
790
|
+
const match = this._dropdown.getAllSuggestions().find(
|
|
791
|
+
(item) => getLabel(item).toLowerCase() === lower,
|
|
792
|
+
);
|
|
793
|
+
if (!match) {
|
|
794
|
+
this._showRejection(value, 'not-in-list');
|
|
795
|
+
this._inputMgr.clearValue();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const result = this._selection.addTag(getValue(match), getLabel(match));
|
|
799
|
+
if (result.ok) {
|
|
800
|
+
this._syncChips();
|
|
801
|
+
this._syncHiddenInput();
|
|
802
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
803
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
804
|
+
announceTagAdded(this._dom.announcer, this._strings, this._locale, result.tag.label);
|
|
805
|
+
} else {
|
|
806
|
+
this._showRejection(value, result.reason);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
const result = this._selection.addTag(value, value);
|
|
810
|
+
if (result.ok) {
|
|
811
|
+
this._syncChips();
|
|
812
|
+
this._syncHiddenInput();
|
|
813
|
+
this.emit('taga11y:add', { tag: result.tag });
|
|
814
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
815
|
+
announceTagAdded(this._dom.announcer, this._strings, this._locale, result.tag.label);
|
|
816
|
+
} else {
|
|
817
|
+
this._showRejection(value, result.reason);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
this._inputMgr.clearValue();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private _handleRemove(id: string): void {
|
|
824
|
+
const removed = this._selection.removeTag(id);
|
|
825
|
+
if (!removed) return;
|
|
826
|
+
this._syncChips();
|
|
827
|
+
this._syncHiddenInput();
|
|
828
|
+
this.emit('taga11y:remove', { tag: removed });
|
|
829
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
830
|
+
announceTagRemoved(this._dom.announcer, this._strings, this._locale, removed.label);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private _handleRemoveLast(): void {
|
|
834
|
+
const tags = this._selection.getTags();
|
|
835
|
+
if (tags.length === 0) return;
|
|
836
|
+
this._handleRemove(tags[tags.length - 1]!.id);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private _handleFormReset(): void {
|
|
840
|
+
const cleared = this._selection.clearTags();
|
|
841
|
+
this.emit('taga11y:clear', { tags: cleared });
|
|
842
|
+
for (const item of this._initialSnapshot) {
|
|
843
|
+
const value = getValue(item);
|
|
844
|
+
const label =
|
|
845
|
+
typeof item === 'string' ? this._resolveLabelFromAll(value) : getLabel(item);
|
|
846
|
+
const result = this._selection.addTag(value, label);
|
|
847
|
+
if (result.ok) this.emit('taga11y:add', { tag: result.tag });
|
|
848
|
+
}
|
|
849
|
+
this._syncChips();
|
|
850
|
+
this._syncHiddenInput();
|
|
851
|
+
this.emit('taga11y:change', { tags: this._selection.getTags() });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private _showRejection(
|
|
855
|
+
text: string,
|
|
856
|
+
reason: 'duplicate' | 'max-reached' | 'not-in-list',
|
|
857
|
+
): void {
|
|
858
|
+
this._dom.tagList.querySelector('.taga11y__tag--error')?.remove();
|
|
859
|
+
const chip = createErrorChip(text);
|
|
860
|
+
this._dom.tagList.appendChild(chip);
|
|
861
|
+
announceRejection(this._dom.error, this._strings, this._locale, reason, text, chip);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private _syncChips(): void {
|
|
865
|
+
const errorChip =
|
|
866
|
+
this._dom.tagList.querySelector<HTMLLIElement>('.taga11y__tag--error') ?? undefined;
|
|
867
|
+
updateChips(
|
|
868
|
+
this._dom.tagList,
|
|
869
|
+
this._selection.getTags(),
|
|
870
|
+
(id) => this._handleRemove(id),
|
|
871
|
+
this._strings,
|
|
872
|
+
this._locale,
|
|
873
|
+
errorChip,
|
|
874
|
+
() => this._input.focus(),
|
|
875
|
+
);
|
|
876
|
+
if (this._opts.disabled) {
|
|
877
|
+
this._dom.tagList
|
|
878
|
+
.querySelectorAll<HTMLButtonElement>('.taga11y__tag-remove')
|
|
879
|
+
.forEach((btn) => { btn.hidden = true; });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private _announceTagsSummary(): void {
|
|
884
|
+
announceTagsSummary(
|
|
885
|
+
this._dom.announcer,
|
|
886
|
+
this._strings,
|
|
887
|
+
this._locale,
|
|
888
|
+
this._selection.getTags(),
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private _syncHiddenInput(): void {
|
|
893
|
+
this._dom.hiddenInput.value = this._opts.serialize(this._selection.getTags());
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private _resolveLabelFromAll(value: string): string {
|
|
897
|
+
const match = this._dropdown.getAllSuggestions().find((item) => getValue(item) === value);
|
|
898
|
+
return match ? getLabel(match) : value;
|
|
899
|
+
}
|
|
900
|
+
}
|