@symfony/ux-autocomplete 3.2.0 → 3.5.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.
@@ -17,6 +17,7 @@ declare class export_default extends Controller {
17
17
  noMoreResultsText: StringConstructor;
18
18
  createOptionText: StringConstructor;
19
19
  minCharacters: NumberConstructor;
20
+ maxOptions: NumberConstructor;
20
21
  tomSelectOptions: ObjectConstructor;
21
22
  preload: StringConstructor;
22
23
  resetOnFocus: BooleanConstructor;
@@ -28,6 +29,8 @@ declare class export_default extends Controller {
28
29
  readonly noResultsFoundTextValue: string;
29
30
  readonly createOptionTextValue: string;
30
31
  readonly minCharactersValue: number;
32
+ readonly maxOptionsValue: number;
33
+ readonly hasMaxOptionsValue: boolean;
31
34
  readonly hasMinCharactersValue: boolean;
32
35
  readonly tomSelectOptionsValue: object;
33
36
  readonly hasPreloadValue: boolean;
@@ -1,39 +1,24 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
  import TomSelect from "tom-select";
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
- }
3
+ var controller_default = class extends Controller {
4
+ static values = {
5
+ url: String,
6
+ optionsAsHtml: Boolean,
7
+ loadingMoreText: String,
8
+ noResultsFoundText: String,
9
+ noMoreResultsText: String,
10
+ createOptionText: String,
11
+ minCharacters: Number,
12
+ maxOptions: Number,
13
+ tomSelectOptions: Object,
14
+ preload: String,
15
+ resetOnFocus: Boolean
16
+ };
17
+ tomSelect;
18
+ mutationObserver;
19
+ isObserving = false;
20
+ hasLoadedChoicesPreviously = false;
21
+ originalOptions = [];
37
22
  initialize() {
38
23
  if (!this.mutationObserver) this.mutationObserver = new MutationObserver((mutations) => {
39
24
  this.onMutations(mutations);
@@ -46,14 +31,14 @@ var _Class = class extends Controller {
46
31
  initializeTomSelect() {
47
32
  if (this.selectElement) this.selectElement.setAttribute("data-skip-morph", "");
48
33
  if (this.urlValue) {
49
- this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocompleteWithRemoteData).call(this, this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null);
34
+ this.tomSelect = this.#createAutocompleteWithRemoteData(this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null);
50
35
  return;
51
36
  }
52
37
  if (this.optionsAsHtmlValue) {
53
- this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocompleteWithHtmlContents).call(this);
38
+ this.tomSelect = this.#createAutocompleteWithHtmlContents();
54
39
  return;
55
40
  }
56
- this.tomSelect = _assertClassBrand(_Class_brand, this, _createAutocomplete).call(this);
41
+ this.tomSelect = this.#createAutocomplete();
57
42
  this.startMutationObserver();
58
43
  }
59
44
  disconnect() {
@@ -72,9 +57,165 @@ var _Class = class extends Controller {
72
57
  urlValueChanged() {
73
58
  this.resetTomSelect();
74
59
  }
60
+ #getCommonConfig() {
61
+ const plugins = {};
62
+ const isMultiple = !this.selectElement || this.selectElement.multiple;
63
+ if (!this.formElement.disabled && !isMultiple) plugins.clear_button = { title: "" };
64
+ if (isMultiple) plugins.remove_button = { title: "" };
65
+ if (this.urlValue) plugins.virtual_scroll = {};
66
+ const config = {
67
+ render: {
68
+ no_results: () => {
69
+ return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
70
+ },
71
+ option_create: (data, escapeData) => {
72
+ return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
73
+ }
74
+ },
75
+ plugins,
76
+ onItemAdd: () => {
77
+ this.tomSelect?.setTextboxValue("");
78
+ },
79
+ closeAfterSelect: true,
80
+ onOptionAdd: (value, data) => {
81
+ if (!this.tomSelect) return;
82
+ let parentElement = this.tomSelect.input;
83
+ let optgroupData = null;
84
+ const optgroup = data[this.tomSelect.settings.optgroupField];
85
+ if (optgroup && this.tomSelect.optgroups) {
86
+ optgroupData = this.tomSelect.optgroups[optgroup];
87
+ if (optgroupData) {
88
+ const optgroupElement = parentElement.querySelector(`optgroup[label="${optgroupData.label}"]`);
89
+ if (optgroupElement) parentElement = optgroupElement;
90
+ }
91
+ }
92
+ const optionElement = document.createElement("option");
93
+ optionElement.value = value;
94
+ optionElement.text = data[this.tomSelect.settings.labelField];
95
+ const optionOrder = data.$order;
96
+ let orderedOption = null;
97
+ for (const [, tomSelectOption] of Object.entries(this.tomSelect.options)) if (tomSelectOption.$order === optionOrder) {
98
+ orderedOption = parentElement.querySelector(`:scope > option[value="${CSS.escape(tomSelectOption[this.tomSelect.settings.valueField])}"]`);
99
+ break;
100
+ }
101
+ if (orderedOption) orderedOption.insertAdjacentElement("afterend", optionElement);
102
+ else if (optionOrder >= 0) parentElement.append(optionElement);
103
+ else parentElement.prepend(optionElement);
104
+ }
105
+ };
106
+ if (!this.selectElement && !this.urlValue) config.shouldLoad = () => false;
107
+ return this.#mergeConfigs(config, this.tomSelectOptionsValue);
108
+ }
109
+ #createAutocomplete() {
110
+ const config = this.#mergeConfigs(this.#getCommonConfig(), { maxOptions: this.getMaxOptions() });
111
+ return this.#createTomSelect(config);
112
+ }
113
+ #createAutocompleteWithHtmlContents() {
114
+ const commonConfig = this.#getCommonConfig();
115
+ const labelField = commonConfig.labelField ?? "text";
116
+ const config = this.#mergeConfigs(commonConfig, {
117
+ maxOptions: this.getMaxOptions(),
118
+ score: (search) => {
119
+ const scoringFunction = this.tomSelect?.getScoreFunction(search);
120
+ return (item) => {
121
+ return scoringFunction?.({
122
+ ...item,
123
+ text: this.#stripTags(item[labelField])
124
+ });
125
+ };
126
+ },
127
+ render: {
128
+ ...commonConfig.render,
129
+ item: (item) => `<div>${item[labelField]}</div>`,
130
+ option: (item) => `<div>${item[labelField]}</div>`
131
+ }
132
+ });
133
+ return this.#createTomSelect(config);
134
+ }
135
+ #createAutocompleteWithRemoteData(autocompleteEndpointUrl, minCharacterLength) {
136
+ const commonConfig = this.#getCommonConfig();
137
+ const labelField = commonConfig.labelField ?? "text";
138
+ const config = this.#mergeConfigs(commonConfig, {
139
+ firstUrl: (query) => {
140
+ return `${autocompleteEndpointUrl}${autocompleteEndpointUrl.includes("?") ? "&" : "?"}query=${encodeURIComponent(query)}`;
141
+ },
142
+ load: function(query, callback) {
143
+ const url = this.getUrl(query);
144
+ fetch(url).then((response) => response.json()).then((json) => {
145
+ this.setNextUrl(query, json.next_page);
146
+ callback(json.results.options || json.results, json.results.optgroups || []);
147
+ }).catch(() => callback([], []));
148
+ },
149
+ shouldLoad: (query) => {
150
+ if (null !== minCharacterLength) return query.length >= minCharacterLength;
151
+ if (this.hasLoadedChoicesPreviously) return true;
152
+ if (query.length > 0) this.hasLoadedChoicesPreviously = true;
153
+ return query.length >= 3;
154
+ },
155
+ optgroupField: "group_by",
156
+ ...this.hasMaxOptionsValue ? { maxOptions: this.maxOptionsValue } : {},
157
+ score: (_search) => (_item) => 1,
158
+ render: {
159
+ option: (item, escape) => `<div>${this.optionsAsHtmlValue ? item[labelField] : escape(item[labelField])}</div>`,
160
+ item: (item, escape) => `<div>${this.optionsAsHtmlValue ? item[labelField] : escape(item[labelField])}</div>`,
161
+ loading_more: () => {
162
+ return `<div class="loading-more-results">${this.loadingMoreTextValue}</div>`;
163
+ },
164
+ no_more_results: () => {
165
+ return `<div class="no-more-results">${this.noMoreResultsTextValue}</div>`;
166
+ },
167
+ no_results: () => {
168
+ return `<div class="no-results">${this.noResultsFoundTextValue}</div>`;
169
+ },
170
+ option_create: (data, escapeData) => {
171
+ return `<div class="create">${this.createOptionTextValue.replace("%placeholder%", `<strong>${escapeData(data.input)}</strong>`)}</div>`;
172
+ }
173
+ },
174
+ onFocus: () => {
175
+ if (this.resetOnFocusValue && this.tomSelect) {
176
+ if (this.tomSelect.control_input.value.trim() === "") {
177
+ this.tomSelect.clearOptions();
178
+ this.tomSelect.loadedSearches = {};
179
+ if (typeof this.tomSelect["clearPagination"] === "function") this.tomSelect["clearPagination"]();
180
+ this.tomSelect.load("");
181
+ }
182
+ }
183
+ },
184
+ preload: this.preload
185
+ });
186
+ return this.#createTomSelect(config);
187
+ }
75
188
  getMaxOptions() {
189
+ if (this.hasMaxOptionsValue) return this.maxOptionsValue;
76
190
  return this.selectElement ? this.selectElement.options.length : 50;
77
191
  }
192
+ #stripTags(string) {
193
+ return string.replace(/(<([^>]+)>)/gi, "");
194
+ }
195
+ #mergeConfigs(config1, config2) {
196
+ return {
197
+ ...config1,
198
+ ...config2,
199
+ plugins: this.#normalizePlugins({
200
+ ...this.#normalizePluginsToHash(config1.plugins || {}),
201
+ ...this.#normalizePluginsToHash(config2.plugins || {})
202
+ })
203
+ };
204
+ }
205
+ #normalizePluginsToHash = (plugins) => {
206
+ if (Array.isArray(plugins)) return plugins.reduce((acc, plugin) => {
207
+ if (typeof plugin === "string") acc[plugin] = {};
208
+ if (typeof plugin === "object" && plugin.name) acc[plugin.name] = plugin.options || {};
209
+ return acc;
210
+ }, {});
211
+ return plugins;
212
+ };
213
+ #normalizePlugins(plugins) {
214
+ return Object.entries(plugins).reduce((acc, [pluginName, pluginOptions]) => {
215
+ if (pluginOptions !== false) acc[pluginName] = pluginOptions;
216
+ return acc;
217
+ }, {});
218
+ }
78
219
  get selectElement() {
79
220
  if (!(this.element instanceof HTMLSelectElement)) return null;
80
221
  return this.element;
@@ -83,6 +224,17 @@ var _Class = class extends Controller {
83
224
  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
225
  return this.element;
85
226
  }
227
+ #createTomSelect(options) {
228
+ const preConnectPayload = { options };
229
+ this.dispatchEvent("pre-connect", preConnectPayload);
230
+ const tomSelect = new TomSelect(this.formElement, options);
231
+ const connectPayload = {
232
+ tomSelect,
233
+ options
234
+ };
235
+ this.dispatchEvent("connect", connectPayload);
236
+ return tomSelect;
237
+ }
86
238
  dispatchEvent(name, payload) {
87
239
  this.dispatch(name, {
88
240
  detail: payload,
@@ -177,173 +329,4 @@ var _Class = class extends Controller {
177
329
  return originalOptionsSet.size === newOptionsSet.size && [...originalOptionsSet].every((option) => newOptionsSet.has(option));
178
330
  }
179
331
  };
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, escape) => `<div>${this.optionsAsHtmlValue ? item[labelField] : escape(item[labelField])}</div>`,
279
- item: (item, escape) => `<div>${this.optionsAsHtmlValue ? item[labelField] : escape(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
- onFocus: () => {
294
- if (this.resetOnFocusValue && this.tomSelect) {
295
- if (this.tomSelect.control_input.value.trim() === "") {
296
- this.tomSelect.clearOptions();
297
- this.tomSelect.loadedSearches = {};
298
- if (typeof this.tomSelect["clearPagination"] === "function") this.tomSelect["clearPagination"]();
299
- this.tomSelect.load("");
300
- }
301
- }
302
- },
303
- preload: this.preload
304
- });
305
- return _assertClassBrand(_Class_brand, this, _createTomSelect).call(this, config);
306
- }
307
- function _stripTags(string) {
308
- return string.replace(/(<([^>]+)>)/gi, "");
309
- }
310
- function _mergeConfigs(config1, config2) {
311
- return {
312
- ...config1,
313
- ...config2,
314
- plugins: _assertClassBrand(_Class_brand, this, _normalizePlugins).call(this, {
315
- ..._classPrivateFieldGet2(_normalizePluginsToHash, this).call(this, config1.plugins || {}),
316
- ..._classPrivateFieldGet2(_normalizePluginsToHash, this).call(this, config2.plugins || {})
317
- })
318
- };
319
- }
320
- function _normalizePlugins(plugins) {
321
- return Object.entries(plugins).reduce((acc, [pluginName, pluginOptions]) => {
322
- if (pluginOptions !== false) acc[pluginName] = pluginOptions;
323
- return acc;
324
- }, {});
325
- }
326
- function _createTomSelect(options) {
327
- const preConnectPayload = { options };
328
- this.dispatchEvent("pre-connect", preConnectPayload);
329
- const tomSelect = new TomSelect(this.formElement, options);
330
- const connectPayload = {
331
- tomSelect,
332
- options
333
- };
334
- this.dispatchEvent("connect", connectPayload);
335
- return tomSelect;
336
- }
337
- _Class.values = {
338
- url: String,
339
- optionsAsHtml: Boolean,
340
- loadingMoreText: String,
341
- noResultsFoundText: String,
342
- noMoreResultsText: String,
343
- createOptionText: String,
344
- minCharacters: Number,
345
- tomSelectOptions: Object,
346
- preload: String,
347
- resetOnFocus: Boolean
348
- };
349
- export { _Class as default };
332
+ export { controller_default as default };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@symfony/ux-autocomplete",
3
3
  "description": "JavaScript Autocomplete functionality for Symfony",
4
4
  "license": "MIT",
5
- "version": "3.2.0",
5
+ "version": "3.5.0",
6
6
  "keywords": [
7
7
  "symfony-ux"
8
8
  ],