@symfony/ux-autocomplete 2.32.0 → 2.34.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/README.md CHANGED
@@ -6,9 +6,10 @@ JavaScript assets of the [symfony/ux-autocomplete](https://packagist.org/package
6
6
 
7
7
  This npm package is **reserved for advanced users** who want to decouple their JavaScript dependencies from their PHP dependencies (e.g., when building Docker images, running JavaScript-only pipelines, etc.).
8
8
 
9
- We **strongly recommend not installing this package directly**, but instead install the PHP package [symfony/ux-autocomplete](https://packagist.org/packages/symfony/ux-autocomplete) in your Symfony application with [Flex](https://github.com/symfony/flex) enabled.
9
+ We **strongly recommend not installing this package directly**, but instead install the PHP package [symfony/ux-autocomplete](https://packagist.org/packages/symfony/ux-autocomplete) in your Symfony application with [Flex](https://github.com/symfony/flex) enabled.
10
10
 
11
11
  If you still want to install this package directly, please make sure its version exactly matches [symfony/ux-autocomplete](https://packagist.org/packages/symfony/ux-autocomplete) PHP package version:
12
+
12
13
  ```shell
13
14
  composer require symfony/ux-autocomplete:2.23.0
14
15
  npm add @symfony/ux-autocomplete@2.23.0
@@ -18,7 +19,7 @@ npm add @symfony/ux-autocomplete@2.23.0
18
19
 
19
20
  ## Resources
20
21
 
21
- - [Documentation](https://symfony.com/bundles/ux-autocomplete/current/index.html)
22
- - [Report issues](https://github.com/symfony/ux/issues) and
23
- [send Pull Requests](https://github.com/symfony/ux/pulls)
24
- in the [main Symfony UX repository](https://github.com/symfony/ux)
22
+ - [Documentation](https://symfony.com/bundles/ux-autocomplete/current/index.html)
23
+ - [Report issues](https://github.com/symfony/ux/issues) and
24
+ [send Pull Requests](https://github.com/symfony/ux/pulls)
25
+ in the [main Symfony UX repository](https://github.com/symfony/ux)
@@ -1,59 +1,57 @@
1
- import { Controller } from '@hotwired/stimulus';
2
- import TomSelect from 'tom-select';
3
-
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import TomSelect from "tom-select";
4
3
  interface AutocompletePreConnectOptions {
5
- options: any;
4
+ options: any;
6
5
  }
7
6
  interface AutocompleteConnectOptions {
8
- tomSelect: TomSelect;
9
- options: any;
7
+ tomSelect: TomSelect;
8
+ options: any;
10
9
  }
11
10
  declare class export_default extends Controller {
12
- #private;
13
- static values: {
14
- url: StringConstructor;
15
- optionsAsHtml: BooleanConstructor;
16
- loadingMoreText: StringConstructor;
17
- noResultsFoundText: StringConstructor;
18
- noMoreResultsText: StringConstructor;
19
- createOptionText: StringConstructor;
20
- minCharacters: NumberConstructor;
21
- tomSelectOptions: ObjectConstructor;
22
- preload: StringConstructor;
23
- };
24
- readonly urlValue: string;
25
- readonly optionsAsHtmlValue: boolean;
26
- readonly loadingMoreTextValue: string;
27
- readonly noMoreResultsTextValue: string;
28
- readonly noResultsFoundTextValue: string;
29
- readonly createOptionTextValue: string;
30
- readonly minCharactersValue: number;
31
- readonly hasMinCharactersValue: boolean;
32
- readonly tomSelectOptionsValue: object;
33
- readonly hasPreloadValue: boolean;
34
- readonly preloadValue: string;
35
- tomSelect: TomSelect | undefined;
36
- private mutationObserver;
37
- private isObserving;
38
- private hasLoadedChoicesPreviously;
39
- private originalOptions;
40
- initialize(): void;
41
- connect(): void;
42
- initializeTomSelect(): void;
43
- disconnect(): void;
44
- urlValueChanged(): void;
45
- private getMaxOptions;
46
- get selectElement(): HTMLSelectElement | null;
47
- get formElement(): HTMLInputElement | HTMLSelectElement;
48
- private dispatchEvent;
49
- get preload(): string | boolean;
50
- private resetTomSelect;
51
- private changeTomSelectDisabledState;
52
- private startMutationObserver;
53
- private stopMutationObserver;
54
- private onMutations;
55
- private createOptionsDataStructure;
56
- private areOptionsEquivalent;
11
+ #private;
12
+ static values: {
13
+ url: StringConstructor;
14
+ optionsAsHtml: BooleanConstructor;
15
+ loadingMoreText: StringConstructor;
16
+ noResultsFoundText: StringConstructor;
17
+ noMoreResultsText: StringConstructor;
18
+ createOptionText: StringConstructor;
19
+ minCharacters: NumberConstructor;
20
+ tomSelectOptions: ObjectConstructor;
21
+ preload: StringConstructor;
22
+ };
23
+ readonly urlValue: string;
24
+ readonly optionsAsHtmlValue: boolean;
25
+ readonly loadingMoreTextValue: string;
26
+ readonly noMoreResultsTextValue: string;
27
+ readonly noResultsFoundTextValue: string;
28
+ readonly createOptionTextValue: string;
29
+ readonly minCharactersValue: number;
30
+ readonly hasMinCharactersValue: boolean;
31
+ readonly tomSelectOptionsValue: object;
32
+ readonly hasPreloadValue: boolean;
33
+ readonly preloadValue: string;
34
+ tomSelect: TomSelect | undefined;
35
+ private mutationObserver;
36
+ private isObserving;
37
+ private hasLoadedChoicesPreviously;
38
+ private originalOptions;
39
+ initialize(): void;
40
+ connect(): void;
41
+ initializeTomSelect(): void;
42
+ disconnect(): void;
43
+ urlValueChanged(): void;
44
+ private getMaxOptions;
45
+ get selectElement(): HTMLSelectElement | null;
46
+ get formElement(): HTMLInputElement | HTMLSelectElement;
47
+ private dispatchEvent;
48
+ get preload(): string | boolean;
49
+ private resetTomSelect;
50
+ private changeTomSelectDisabledState;
51
+ private startMutationObserver;
52
+ private stopMutationObserver;
53
+ private onMutations;
54
+ private createOptionsDataStructure;
55
+ private areOptionsEquivalent;
57
56
  }
58
-
59
- export { type AutocompleteConnectOptions, type AutocompletePreConnectOptions, export_default as default };
57
+ export { AutocompleteConnectOptions, AutocompletePreConnectOptions, export_default as default };
@@ -1,423 +1,338 @@
1
- var __typeError = (msg) => {
2
- throw TypeError(msg);
3
- };
4
- var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
- var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
- var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
8
-
9
- // src/controller.ts
10
1
  import { Controller } from "@hotwired/stimulus";
11
2
  import TomSelect from "tom-select";
12
- var _instances, getCommonConfig_fn, createAutocomplete_fn, createAutocompleteWithHtmlContents_fn, createAutocompleteWithRemoteData_fn, stripTags_fn, mergeConfigs_fn, _normalizePluginsToHash, normalizePlugins_fn, createTomSelect_fn;
13
- var controller_default = class extends Controller {
14
- constructor() {
15
- super(...arguments);
16
- __privateAdd(this, _instances);
17
- this.isObserving = false;
18
- this.hasLoadedChoicesPreviously = false;
19
- this.originalOptions = [];
20
- /**
21
- * Normalizes the plugins to a hash, so that we can merge them easily.
22
- */
23
- __privateAdd(this, _normalizePluginsToHash, (plugins) => {
24
- if (Array.isArray(plugins)) {
25
- return plugins.reduce((acc, plugin) => {
26
- if (typeof plugin === "string") {
27
- acc[plugin] = {};
28
- }
29
- if (typeof plugin === "object" && plugin.name) {
30
- acc[plugin.name] = plugin.options || {};
31
- }
32
- return acc;
33
- }, {});
34
- }
35
- return plugins;
36
- });
37
- }
38
- initialize() {
39
- if (!this.mutationObserver) {
40
- this.mutationObserver = new MutationObserver((mutations) => {
41
- this.onMutations(mutations);
42
- });
43
- }
44
- }
45
- connect() {
46
- if (this.selectElement) {
47
- this.originalOptions = this.createOptionsDataStructure(this.selectElement);
48
- }
49
- this.initializeTomSelect();
50
- }
51
- initializeTomSelect() {
52
- if (this.selectElement) {
53
- this.selectElement.setAttribute("data-skip-morph", "");
54
- }
55
- if (this.urlValue) {
56
- this.tomSelect = __privateMethod(this, _instances, createAutocompleteWithRemoteData_fn).call(this, this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null);
57
- return;
58
- }
59
- if (this.optionsAsHtmlValue) {
60
- this.tomSelect = __privateMethod(this, _instances, createAutocompleteWithHtmlContents_fn).call(this);
61
- return;
62
- }
63
- this.tomSelect = __privateMethod(this, _instances, createAutocomplete_fn).call(this);
64
- this.startMutationObserver();
65
- }
66
- disconnect() {
67
- this.stopMutationObserver();
68
- if (!this.tomSelect) {
69
- return;
70
- }
71
- let currentSelectedValues = [];
72
- if (this.selectElement) {
73
- if (this.selectElement.multiple) {
74
- currentSelectedValues = Array.from(this.selectElement.options).filter((option) => option.selected).map((option) => option.value);
75
- } else {
76
- currentSelectedValues = [this.selectElement.value];
77
- }
78
- }
79
- this.tomSelect.destroy();
80
- this.tomSelect = void 0;
81
- if (this.selectElement) {
82
- if (this.selectElement.multiple) {
83
- Array.from(this.selectElement.options).forEach((option) => {
84
- option.selected = currentSelectedValues.includes(option.value);
85
- });
86
- } else {
87
- this.selectElement.value = currentSelectedValues[0];
88
- }
89
- }
90
- }
91
- urlValueChanged() {
92
- this.resetTomSelect();
93
- }
94
- getMaxOptions() {
95
- return this.selectElement ? this.selectElement.options.length : 50;
96
- }
97
- /**
98
- * Returns the element, but only if it's a select element.
99
- */
100
- get selectElement() {
101
- if (!(this.element instanceof HTMLSelectElement)) {
102
- return null;
103
- }
104
- return this.element;
105
- }
106
- /**
107
- * Getter to help typing.
108
- */
109
- get formElement() {
110
- if (!(this.element instanceof HTMLInputElement) && !(this.element instanceof HTMLSelectElement)) {
111
- throw new Error("Autocomplete Stimulus controller can only be used on an <input> or <select>.");
112
- }
113
- return this.element;
114
- }
115
- dispatchEvent(name, payload) {
116
- this.dispatch(name, { detail: payload, prefix: "autocomplete" });
117
- }
118
- get preload() {
119
- if (!this.hasPreloadValue) {
120
- return "focus";
121
- }
122
- if (this.preloadValue === "false") {
123
- return false;
124
- }
125
- if (this.preloadValue === "true") {
126
- return true;
127
- }
128
- return this.preloadValue;
129
- }
130
- resetTomSelect() {
131
- if (this.tomSelect) {
132
- this.dispatchEvent("before-reset", { tomSelect: this.tomSelect });
133
- this.stopMutationObserver();
134
- const currentHtml = this.element.innerHTML;
135
- const currentValue = this.tomSelect.getValue();
136
- this.tomSelect.destroy();
137
- this.element.innerHTML = currentHtml;
138
- this.initializeTomSelect();
139
- this.tomSelect.setValue(currentValue);
140
- }
141
- }
142
- changeTomSelectDisabledState(isDisabled) {
143
- if (!this.tomSelect) {
144
- return;
145
- }
146
- this.stopMutationObserver();
147
- if (isDisabled) {
148
- this.tomSelect.disable();
149
- } else {
150
- this.tomSelect.enable();
151
- }
152
- this.startMutationObserver();
153
- }
154
- startMutationObserver() {
155
- if (!this.isObserving && this.mutationObserver) {
156
- this.mutationObserver.observe(this.element, {
157
- childList: true,
158
- subtree: true,
159
- attributes: true,
160
- characterData: true,
161
- attributeOldValue: true
162
- });
163
- this.isObserving = true;
164
- }
165
- }
166
- stopMutationObserver() {
167
- if (this.isObserving && this.mutationObserver) {
168
- this.mutationObserver.disconnect();
169
- this.isObserving = false;
170
- }
171
- }
172
- onMutations(mutations) {
173
- let changeDisabledState = false;
174
- let requireReset = false;
175
- mutations.forEach((mutation) => {
176
- switch (mutation.type) {
177
- case "attributes":
178
- if (mutation.target === this.element && mutation.attributeName === "disabled") {
179
- changeDisabledState = true;
180
- break;
181
- }
182
- if (mutation.target === this.element && mutation.attributeName === "multiple") {
183
- const isNowMultiple = this.element.hasAttribute("multiple");
184
- const wasMultiple = mutation.oldValue === "multiple";
185
- if (isNowMultiple !== wasMultiple) {
186
- requireReset = true;
187
- }
188
- break;
189
- }
190
- break;
191
- }
192
- });
193
- const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
194
- const areOptionsEquivalent = this.areOptionsEquivalent(newOptions);
195
- if (!areOptionsEquivalent || requireReset) {
196
- this.originalOptions = newOptions;
197
- this.resetTomSelect();
198
- }
199
- if (changeDisabledState) {
200
- this.changeTomSelectDisabledState(this.formElement.disabled);
201
- }
202
- }
203
- createOptionsDataStructure(selectElement) {
204
- return Array.from(selectElement.options).map((option) => {
205
- return {
206
- value: option.value,
207
- text: option.text
208
- };
209
- });
210
- }
211
- areOptionsEquivalent(newOptions) {
212
- const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== "");
213
- const filteredNewOptions = newOptions.filter((option) => option.value !== "");
214
- const originalPlaceholderOption = this.originalOptions.find((option) => option.value === "");
215
- const newPlaceholderOption = newOptions.find((option) => option.value === "");
216
- if (originalPlaceholderOption && newPlaceholderOption && originalPlaceholderOption.text !== newPlaceholderOption.text) {
217
- return false;
218
- }
219
- if (filteredOriginalOptions.length !== filteredNewOptions.length) {
220
- return false;
221
- }
222
- const normalizeOption = (option) => `${option.value}-${option.text}`;
223
- const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
224
- const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
225
- return originalOptionsSet.size === newOptionsSet.size && [...originalOptionsSet].every((option) => newOptionsSet.has(option));
226
- }
227
- };
228
- _instances = new WeakSet();
229
- getCommonConfig_fn = function() {
230
- const plugins = {};
231
- const isMultiple = !this.selectElement || this.selectElement.multiple;
232
- if (!this.formElement.disabled && !isMultiple) {
233
- plugins.clear_button = { title: "" };
234
- }
235
- if (isMultiple) {
236
- plugins.remove_button = { title: "" };
237
- }
238
- if (this.urlValue) {
239
- plugins.virtual_scroll = {};
240
- }
241
- const render = {
242
- no_results: () => {
243
- return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
244
- },
245
- option_create: (data, escapeData) => {
246
- return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
247
- }
248
- };
249
- const config = {
250
- render,
251
- plugins,
252
- // clear the text input after selecting a value
253
- onItemAdd: () => {
254
- this.tomSelect?.setTextboxValue("");
255
- },
256
- closeAfterSelect: true,
257
- // fix positioning (in the dropdown) of options added through addOption()
258
- onOptionAdd: (value, data) => {
259
- if (!this.tomSelect) {
260
- return;
261
- }
262
- let parentElement = this.tomSelect.input;
263
- let optgroupData = null;
264
- const optgroup = data[this.tomSelect.settings.optgroupField];
265
- if (optgroup && this.tomSelect.optgroups) {
266
- optgroupData = this.tomSelect.optgroups[optgroup];
267
- if (optgroupData) {
268
- const optgroupElement = parentElement.querySelector(`optgroup[label="${optgroupData.label}"]`);
269
- if (optgroupElement) {
270
- parentElement = optgroupElement;
271
- }
272
- }
273
- }
274
- const optionElement = document.createElement("option");
275
- optionElement.value = value;
276
- optionElement.text = data[this.tomSelect.settings.labelField];
277
- const optionOrder = data.$order;
278
- let orderedOption = null;
279
- for (const [, tomSelectOption] of Object.entries(this.tomSelect.options)) {
280
- if (tomSelectOption.$order === optionOrder) {
281
- orderedOption = parentElement.querySelector(
282
- `:scope > option[value="${CSS.escape(tomSelectOption[this.tomSelect.settings.valueField])}"]`
283
- );
284
- break;
285
- }
286
- }
287
- if (orderedOption) {
288
- orderedOption.insertAdjacentElement("afterend", optionElement);
289
- } else if (optionOrder >= 0) {
290
- parentElement.append(optionElement);
291
- } else {
292
- parentElement.prepend(optionElement);
293
- }
294
- }
295
- };
296
- if (!this.selectElement && !this.urlValue) {
297
- config.shouldLoad = () => false;
298
- }
299
- return __privateMethod(this, _instances, mergeConfigs_fn).call(this, config, this.tomSelectOptionsValue);
300
- };
301
- createAutocomplete_fn = function() {
302
- const config = __privateMethod(this, _instances, mergeConfigs_fn).call(this, __privateMethod(this, _instances, getCommonConfig_fn).call(this), {
303
- maxOptions: this.getMaxOptions()
304
- });
305
- return __privateMethod(this, _instances, createTomSelect_fn).call(this, config);
306
- };
307
- createAutocompleteWithHtmlContents_fn = function() {
308
- const commonConfig = __privateMethod(this, _instances, getCommonConfig_fn).call(this);
309
- const labelField = commonConfig.labelField ?? "text";
310
- const config = __privateMethod(this, _instances, mergeConfigs_fn).call(this, commonConfig, {
311
- maxOptions: this.getMaxOptions(),
312
- score: (search) => {
313
- const scoringFunction = this.tomSelect?.getScoreFunction(search);
314
- return (item) => {
315
- return scoringFunction?.({ ...item, text: __privateMethod(this, _instances, stripTags_fn).call(this, item[labelField]) });
316
- };
317
- },
318
- render: {
319
- ...commonConfig.render,
320
- item: (item) => `<div>${item[labelField]}</div>`,
321
- option: (item) => `<div>${item[labelField]}</div>`
322
- }
323
- });
324
- return __privateMethod(this, _instances, createTomSelect_fn).call(this, config);
325
- };
326
- createAutocompleteWithRemoteData_fn = function(autocompleteEndpointUrl, minCharacterLength) {
327
- const commonConfig = __privateMethod(this, _instances, getCommonConfig_fn).call(this);
328
- const labelField = commonConfig.labelField ?? "text";
329
- const config = __privateMethod(this, _instances, mergeConfigs_fn).call(this, commonConfig, {
330
- firstUrl: (query) => {
331
- const separator = autocompleteEndpointUrl.includes("?") ? "&" : "?";
332
- return `${autocompleteEndpointUrl}${separator}query=${encodeURIComponent(query)}`;
333
- },
334
- // VERY IMPORTANT: use 'function (query, callback) { ... }' instead of the
335
- // '(query, callback) => { ... }' syntax because, otherwise,
336
- // the 'this.XXX' calls inside this method fail
337
- load: function(query, callback) {
338
- const url = this.getUrl(query);
339
- fetch(url).then((response) => response.json()).then((json) => {
340
- this.setNextUrl(query, json.next_page);
341
- callback(json.results.options || json.results, json.results.optgroups || []);
342
- }).catch(() => callback([], []));
343
- },
344
- shouldLoad: (query) => {
345
- if (null !== minCharacterLength) {
346
- return query.length >= minCharacterLength;
347
- }
348
- if (this.hasLoadedChoicesPreviously) {
349
- return true;
350
- }
351
- if (query.length > 0) {
352
- this.hasLoadedChoicesPreviously = true;
353
- }
354
- return query.length >= 3;
355
- },
356
- optgroupField: "group_by",
357
- // avoid extra filtering after results are returned
358
- score: (_search) => (_item) => 1,
359
- render: {
360
- option: (item) => `<div>${item[labelField]}</div>`,
361
- item: (item) => `<div>${item[labelField]}</div>`,
362
- loading_more: () => {
363
- return `<div class="loading-more-results">${this.loadingMoreTextValue}</div>`;
364
- },
365
- no_more_results: () => {
366
- return `<div class="no-more-results">${this.noMoreResultsTextValue}</div>`;
367
- },
368
- no_results: () => {
369
- return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
370
- },
371
- option_create: (data, escapeData) => {
372
- return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
373
- }
374
- },
375
- preload: this.preload
376
- });
377
- return __privateMethod(this, _instances, createTomSelect_fn).call(this, config);
378
- };
379
- stripTags_fn = function(string) {
380
- return string.replace(/(<([^>]+)>)/gi, "");
381
- };
382
- mergeConfigs_fn = function(config1, config2) {
383
- return {
384
- ...config1,
385
- ...config2,
386
- // Plugins from both configs should be merged together.
387
- plugins: __privateMethod(this, _instances, normalizePlugins_fn).call(this, {
388
- ...__privateGet(this, _normalizePluginsToHash).call(this, config1.plugins || {}),
389
- ...__privateGet(this, _normalizePluginsToHash).call(this, config2.plugins || {})
390
- })
391
- };
392
- };
393
- _normalizePluginsToHash = new WeakMap();
394
- normalizePlugins_fn = function(plugins) {
395
- return Object.entries(plugins).reduce((acc, [pluginName, pluginOptions]) => {
396
- if (pluginOptions !== false) {
397
- acc[pluginName] = pluginOptions;
398
- }
399
- return acc;
400
- }, {});
401
- };
402
- createTomSelect_fn = function(options) {
403
- const preConnectPayload = { options };
404
- this.dispatchEvent("pre-connect", preConnectPayload);
405
- const tomSelect = new TomSelect(this.formElement, options);
406
- const connectPayload = { tomSelect, options };
407
- this.dispatchEvent("connect", connectPayload);
408
- return tomSelect;
409
- };
410
- controller_default.values = {
411
- url: String,
412
- optionsAsHtml: Boolean,
413
- loadingMoreText: String,
414
- noResultsFoundText: String,
415
- noMoreResultsText: String,
416
- createOptionText: String,
417
- minCharacters: Number,
418
- tomSelectOptions: Object,
419
- preload: String
3
+ function _checkPrivateRedeclaration(e, t) {
4
+ if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
5
+ }
6
+ function _classPrivateMethodInitSpec(e, a) {
7
+ _checkPrivateRedeclaration(e, a), a.add(e);
8
+ }
9
+ function _classPrivateFieldInitSpec(e, t, a) {
10
+ _checkPrivateRedeclaration(e, t), t.set(e, a);
11
+ }
12
+ function _assertClassBrand(e, t, n) {
13
+ if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
14
+ throw new TypeError("Private element is not present on this object");
15
+ }
16
+ function _classPrivateFieldGet2(s, a) {
17
+ return s.get(_assertClassBrand(s, a));
18
+ }
19
+ var _Class_brand = /* @__PURE__ */ new WeakSet();
20
+ var _normalizePluginsToHash = /* @__PURE__ */ new WeakMap();
21
+ var _Class = class extends Controller {
22
+ constructor(..._args) {
23
+ super(..._args);
24
+ _classPrivateMethodInitSpec(this, _Class_brand);
25
+ this.isObserving = false;
26
+ this.hasLoadedChoicesPreviously = false;
27
+ this.originalOptions = [];
28
+ _classPrivateFieldInitSpec(this, _normalizePluginsToHash, (plugins) => {
29
+ if (Array.isArray(plugins)) return plugins.reduce((acc, plugin) => {
30
+ if (typeof plugin === "string") acc[plugin] = {};
31
+ if (typeof plugin === "object" && plugin.name) acc[plugin.name] = plugin.options || {};
32
+ return acc;
33
+ }, {});
34
+ return plugins;
35
+ });
36
+ }
37
+ initialize() {
38
+ if (!this.mutationObserver) this.mutationObserver = new MutationObserver((mutations) => {
39
+ this.onMutations(mutations);
40
+ });
41
+ }
42
+ connect() {
43
+ if (this.selectElement) this.originalOptions = this.createOptionsDataStructure(this.selectElement);
44
+ this.initializeTomSelect();
45
+ }
46
+ initializeTomSelect() {
47
+ if (this.selectElement) this.selectElement.setAttribute("data-skip-morph", "");
48
+ if (this.urlValue) {
49
+ this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocompleteWithRemoteData).call(this, this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null);
50
+ return;
51
+ }
52
+ if (this.optionsAsHtmlValue) {
53
+ this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocompleteWithHtmlContents).call(this);
54
+ return;
55
+ }
56
+ this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocomplete).call(this);
57
+ this.startMutationObserver();
58
+ }
59
+ disconnect() {
60
+ this.stopMutationObserver();
61
+ if (!this.tomSelect) return;
62
+ let currentSelectedValues = [];
63
+ if (this.selectElement) if (this.selectElement.multiple) currentSelectedValues = Array.from(this.selectElement.options).filter((option) => option.selected).map((option) => option.value);
64
+ else currentSelectedValues = [this.selectElement.value];
65
+ this.tomSelect.destroy();
66
+ this.tomSelect = void 0;
67
+ if (this.selectElement) if (this.selectElement.multiple) Array.from(this.selectElement.options).forEach((option) => {
68
+ option.selected = currentSelectedValues.includes(option.value);
69
+ });
70
+ else this.selectElement.value = currentSelectedValues[0];
71
+ }
72
+ urlValueChanged() {
73
+ this.resetTomSelect();
74
+ }
75
+ getMaxOptions() {
76
+ return this.selectElement ? this.selectElement.options.length : 50;
77
+ }
78
+ get selectElement() {
79
+ if (!(this.element instanceof HTMLSelectElement)) return null;
80
+ return this.element;
81
+ }
82
+ get formElement() {
83
+ if (!(this.element instanceof HTMLInputElement) && !(this.element instanceof HTMLSelectElement)) throw new Error("Autocomplete Stimulus controller can only be used on an <input> or <select>.");
84
+ return this.element;
85
+ }
86
+ dispatchEvent(name, payload) {
87
+ this.dispatch(name, {
88
+ detail: payload,
89
+ prefix: "autocomplete"
90
+ });
91
+ }
92
+ get preload() {
93
+ if (!this.hasPreloadValue) return "focus";
94
+ if (this.preloadValue === "false") return false;
95
+ if (this.preloadValue === "true") return true;
96
+ return this.preloadValue;
97
+ }
98
+ resetTomSelect() {
99
+ if (this.tomSelect) {
100
+ this.dispatchEvent("before-reset", { tomSelect: this.tomSelect });
101
+ this.stopMutationObserver();
102
+ const currentHtml = this.element.innerHTML;
103
+ const currentValue = this.tomSelect.getValue();
104
+ this.tomSelect.destroy();
105
+ this.element.innerHTML = currentHtml;
106
+ this.initializeTomSelect();
107
+ this.tomSelect.setValue(currentValue);
108
+ }
109
+ }
110
+ changeTomSelectDisabledState(isDisabled) {
111
+ if (!this.tomSelect) return;
112
+ this.stopMutationObserver();
113
+ if (isDisabled) this.tomSelect.disable();
114
+ else this.tomSelect.enable();
115
+ this.startMutationObserver();
116
+ }
117
+ startMutationObserver() {
118
+ if (!this.isObserving && this.mutationObserver) {
119
+ this.mutationObserver.observe(this.element, {
120
+ childList: true,
121
+ subtree: true,
122
+ attributes: true,
123
+ characterData: true,
124
+ attributeOldValue: true
125
+ });
126
+ this.isObserving = true;
127
+ }
128
+ }
129
+ stopMutationObserver() {
130
+ if (this.isObserving && this.mutationObserver) {
131
+ this.mutationObserver.disconnect();
132
+ this.isObserving = false;
133
+ }
134
+ }
135
+ onMutations(mutations) {
136
+ let changeDisabledState = false;
137
+ let requireReset = false;
138
+ mutations.forEach((mutation) => {
139
+ switch (mutation.type) {
140
+ case "attributes":
141
+ if (mutation.target === this.element && mutation.attributeName === "disabled") {
142
+ changeDisabledState = true;
143
+ break;
144
+ }
145
+ if (mutation.target === this.element && mutation.attributeName === "multiple") {
146
+ if (this.element.hasAttribute("multiple") !== (mutation.oldValue === "multiple")) requireReset = true;
147
+ break;
148
+ }
149
+ break;
150
+ }
151
+ });
152
+ const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
153
+ if (!this.areOptionsEquivalent(newOptions) || requireReset) {
154
+ this.originalOptions = newOptions;
155
+ this.resetTomSelect();
156
+ }
157
+ if (changeDisabledState) this.changeTomSelectDisabledState(this.formElement.disabled);
158
+ }
159
+ createOptionsDataStructure(selectElement) {
160
+ return Array.from(selectElement.options).map((option) => {
161
+ return {
162
+ value: option.value,
163
+ text: option.text
164
+ };
165
+ });
166
+ }
167
+ areOptionsEquivalent(newOptions) {
168
+ const filteredOriginalOptions = this.originalOptions.filter((option) => option.value !== "");
169
+ const filteredNewOptions = newOptions.filter((option) => option.value !== "");
170
+ const originalPlaceholderOption = this.originalOptions.find((option) => option.value === "");
171
+ const newPlaceholderOption = newOptions.find((option) => option.value === "");
172
+ if (originalPlaceholderOption && newPlaceholderOption && originalPlaceholderOption.text !== newPlaceholderOption.text) return false;
173
+ if (filteredOriginalOptions.length !== filteredNewOptions.length) return false;
174
+ const normalizeOption = (option) => `${option.value}-${option.text}`;
175
+ const originalOptionsSet = new Set(filteredOriginalOptions.map(normalizeOption));
176
+ const newOptionsSet = new Set(filteredNewOptions.map(normalizeOption));
177
+ return originalOptionsSet.size === newOptionsSet.size && [...originalOptionsSet].every((option) => newOptionsSet.has(option));
178
+ }
420
179
  };
421
- export {
422
- controller_default as default
180
+ function _getCommonConfig() {
181
+ const plugins = {};
182
+ const isMultiple = !this.selectElement || this.selectElement.multiple;
183
+ if (!this.formElement.disabled && !isMultiple) plugins.clear_button = { title: "" };
184
+ if (isMultiple) plugins.remove_button = { title: "" };
185
+ if (this.urlValue) plugins.virtual_scroll = {};
186
+ const config = {
187
+ render: {
188
+ no_results: () => {
189
+ return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
190
+ },
191
+ option_create: (data, escapeData) => {
192
+ return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
193
+ }
194
+ },
195
+ plugins,
196
+ onItemAdd: () => {
197
+ this.tomSelect?.setTextboxValue("");
198
+ },
199
+ closeAfterSelect: true,
200
+ onOptionAdd: (value, data) => {
201
+ if (!this.tomSelect) return;
202
+ let parentElement = this.tomSelect.input;
203
+ let optgroupData = null;
204
+ const optgroup = data[this.tomSelect.settings.optgroupField];
205
+ if (optgroup && this.tomSelect.optgroups) {
206
+ optgroupData = this.tomSelect.optgroups[optgroup];
207
+ if (optgroupData) {
208
+ const optgroupElement = parentElement.querySelector(`optgroup[label="${optgroupData.label}"]`);
209
+ if (optgroupElement) parentElement = optgroupElement;
210
+ }
211
+ }
212
+ const optionElement = document.createElement("option");
213
+ optionElement.value = value;
214
+ optionElement.text = data[this.tomSelect.settings.labelField];
215
+ const optionOrder = data.$order;
216
+ let orderedOption = null;
217
+ for (const [, tomSelectOption] of Object.entries(this.tomSelect.options)) if (tomSelectOption.$order === optionOrder) {
218
+ orderedOption = parentElement.querySelector(`:scope > option[value="${CSS.escape(tomSelectOption[this.tomSelect.settings.valueField])}"]`);
219
+ break;
220
+ }
221
+ if (orderedOption) orderedOption.insertAdjacentElement("afterend", optionElement);
222
+ else if (optionOrder >= 0) parentElement.append(optionElement);
223
+ else parentElement.prepend(optionElement);
224
+ }
225
+ };
226
+ if (!this.selectElement && !this.urlValue) config.shouldLoad = () => false;
227
+ return _assertClassBrand(_Class_brand, this, _mergeConfigs).call(this, config, this.tomSelectOptionsValue);
228
+ }
229
+ function _createAutocomplete() {
230
+ const config = _assertClassBrand(_Class_brand, this, _mergeConfigs).call(this, _assertClassBrand(_Class_brand, this, _getCommonConfig).call(this), { maxOptions: this.getMaxOptions() });
231
+ return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config);
232
+ }
233
+ function _createAutocompleteWithHtmlContents() {
234
+ const commonConfig = _assertClassBrand(_Class_brand, this, _getCommonConfig).call(this);
235
+ const labelField = commonConfig.labelField ?? "text";
236
+ const config = _assertClassBrand(_Class_brand, this, _mergeConfigs).call(this, commonConfig, {
237
+ maxOptions: this.getMaxOptions(),
238
+ score: (search) => {
239
+ const scoringFunction = this.tomSelect?.getScoreFunction(search);
240
+ return (item) => {
241
+ return scoringFunction?.({
242
+ ...item,
243
+ text: _assertClassBrand(_Class_brand, this, _stripTags).call(this, item[labelField])
244
+ });
245
+ };
246
+ },
247
+ render: {
248
+ ...commonConfig.render,
249
+ item: (item) => `<div>${item[labelField]}</div>`,
250
+ option: (item) => `<div>${item[labelField]}</div>`
251
+ }
252
+ });
253
+ return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config);
254
+ }
255
+ function _createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacterLength) {
256
+ const commonConfig = _assertClassBrand(_Class_brand, this, _getCommonConfig).call(this);
257
+ const labelField = commonConfig.labelField ?? "text";
258
+ const config = _assertClassBrand(_Class_brand, this, _mergeConfigs).call(this, commonConfig, {
259
+ firstUrl: (query) => {
260
+ return `${autocompleteEndpointUrl}${autocompleteEndpointUrl.includes("?") ? "&" : "?"}query=${encodeURIComponent(query)}`;
261
+ },
262
+ load: function(query, callback) {
263
+ const url = this.getUrl(query);
264
+ fetch(url).then((response) => response.json()).then((json) => {
265
+ this.setNextUrl(query, json.next_page);
266
+ callback(json.results.options || json.results, json.results.optgroups || []);
267
+ }).catch(() => callback([], []));
268
+ },
269
+ shouldLoad: (query) => {
270
+ if (null !== minCharacterLength) return query.length >= minCharacterLength;
271
+ if (this.hasLoadedChoicesPreviously) return true;
272
+ if (query.length > 0) this.hasLoadedChoicesPreviously = true;
273
+ return query.length >= 3;
274
+ },
275
+ optgroupField: "group_by",
276
+ score: (_search) => (_item) => 1,
277
+ render: {
278
+ option: (item) => `<div>${item[labelField]}</div>`,
279
+ item: (item) => `<div>${item[labelField]}</div>`,
280
+ loading_more: () => {
281
+ return `<div class="loading-more-results">${this.loadingMoreTextValue}</div>`;
282
+ },
283
+ no_more_results: () => {
284
+ return `<div class="no-more-results">${this.noMoreResultsTextValue}</div>`;
285
+ },
286
+ no_results: () => {
287
+ return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
288
+ },
289
+ option_create: (data, escapeData) => {
290
+ return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
291
+ }
292
+ },
293
+ preload: this.preload
294
+ });
295
+ return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config);
296
+ }
297
+ function _stripTags(string) {
298
+ return string.replace(/(<([^>]+)>)/gi, "");
299
+ }
300
+ function _mergeConfigs(config1, config2) {
301
+ return {
302
+ ...config1,
303
+ ...config2,
304
+ plugins: _assertClassBrand(_Class_brand, this, _normalizePlugins).call(this, {
305
+ ..._classPrivateFieldGet2(_normalizePluginsToHash, this).call(this, config1.plugins || {}),
306
+ ..._classPrivateFieldGet2(_normalizePluginsToHash, this).call(this, config2.plugins || {})
307
+ })
308
+ };
309
+ }
310
+ function _normalizePlugins(plugins) {
311
+ return Object.entries(plugins).reduce((acc, [pluginName, pluginOptions]) => {
312
+ if (pluginOptions !== false) acc[pluginName] = pluginOptions;
313
+ return acc;
314
+ }, {});
315
+ }
316
+ function _createTomSelect(options) {
317
+ const preConnectPayload = { options };
318
+ this.dispatchEvent("pre-connect", preConnectPayload);
319
+ const tomSelect = new TomSelect(this.formElement, options);
320
+ const connectPayload = {
321
+ tomSelect,
322
+ options
323
+ };
324
+ this.dispatchEvent("connect", connectPayload);
325
+ return tomSelect;
326
+ }
327
+ _Class.values = {
328
+ url: String,
329
+ optionsAsHtml: Boolean,
330
+ loadingMoreText: String,
331
+ noResultsFoundText: String,
332
+ noMoreResultsText: String,
333
+ createOptionText: String,
334
+ minCharacters: Number,
335
+ tomSelectOptions: Object,
336
+ preload: String
423
337
  };
338
+ export { _Class as default };
package/package.json CHANGED
@@ -2,12 +2,12 @@
2
2
  "name": "@symfony/ux-autocomplete",
3
3
  "description": "JavaScript Autocomplete functionality for Symfony",
4
4
  "license": "MIT",
5
- "version": "2.32.0",
5
+ "version": "2.34.0",
6
6
  "keywords": [
7
7
  "symfony-ux"
8
8
  ],
9
9
  "homepage": "https://ux.symfony.com/autocomplete",
10
- "repository": "https://github.com/symfony/ux-autocomplete",
10
+ "repository": "https://github.com/symfony/ux",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist"
@@ -48,19 +48,16 @@
48
48
  "jsdom": "^26.1.0",
49
49
  "tom-select": "^2.2.2",
50
50
  "tslib": "^2.8.1",
51
- "tsx": "^4.20.3",
52
51
  "typescript": "^5.8.3",
53
- "vitest": "^3.2.4",
52
+ "vitest": "^4.1.0",
54
53
  "vitest-fetch-mock": "^0.2.2"
55
54
  },
56
55
  "scripts": {
57
- "build": "tsx ../../../bin/build_package.ts .",
58
- "watch": "tsx ../../../bin/build_package.ts . --watch",
56
+ "build": "node ../../../bin/build_package.ts .",
57
+ "watch": "node ../../../bin/build_package.ts . --watch",
59
58
  "test": "pnpm run test:unit && pnpm run test:browser",
60
59
  "test:unit": "../../../bin/unit_test_package.sh .",
61
60
  "test:browser": "playwright test",
62
- "test:browser:ui": "playwright test --ui",
63
- "check": "biome check",
64
- "ci": "biome ci"
61
+ "test:browser:ui": "playwright test --ui"
65
62
  }
66
63
  }