bard-tag_field 0.5.0 → 0.5.2

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.
@@ -0,0 +1,849 @@
1
+ import Taggle from "./taggle.js"
2
+ import autocomplete from "autocompleter"
3
+
4
+
5
+ class TagOption extends HTMLElement {
6
+ constructor() {
7
+ super();
8
+ this._shadowRoot = this.attachShadow({ mode: "open" });
9
+ }
10
+
11
+ connectedCallback() {
12
+ this._shadowRoot.innerHTML = `
13
+ <style>
14
+ :host {
15
+ background: #588a00;
16
+ padding: 3px 10px 3px 10px !important;
17
+ margin-right: 4px !important;
18
+ margin-bottom: 2px !important;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ float: none;
22
+ font-size: 14px;
23
+ line-height: 1;
24
+ min-height: 32px;
25
+ color: #fff;
26
+ text-transform: none;
27
+ border-radius: 3px;
28
+ position: relative;
29
+ cursor: pointer;
30
+ }
31
+ button {
32
+ z-index: 1;
33
+ border: none;
34
+ background: none;
35
+ font-size: 20px;
36
+ display: inline-block;
37
+ color: rgba(255, 255, 255, 0.6);
38
+ right: 10px;
39
+ height: 100%;
40
+ cursor: pointer;
41
+ }
42
+ </style>
43
+ <slot></slot>
44
+ <button type="button">×</button>
45
+ `;
46
+
47
+ this.buttonTarget = this._shadowRoot.querySelector("button")
48
+ this.buttonTarget.onclick = event => {
49
+ this.parentNode._taggle._remove(this, event)
50
+ }
51
+ }
52
+
53
+ get value() {
54
+ return this.getAttribute("value") || this.innerText
55
+ }
56
+
57
+ get label() {
58
+ return this.innerText
59
+ }
60
+ }
61
+ customElements.define("tag-option", TagOption);
62
+
63
+
64
+ class InputTag extends HTMLElement {
65
+ static get formAssociated() {
66
+ return true;
67
+ }
68
+
69
+ static get observedAttributes() {
70
+ return ['name', 'multiple', 'required', 'list'];
71
+ }
72
+
73
+ constructor() {
74
+ super();
75
+ this._internals = this.attachInternals();
76
+ this._shadowRoot = this.attachShadow({ mode: "open" });
77
+
78
+ this.observer = new MutationObserver(mutations => {
79
+ let needsTagOptionsUpdate = false;
80
+ let needsAutocompleteUpdate = false;
81
+
82
+ for (const mutation of mutations) {
83
+ if (mutation.type === 'childList') {
84
+ const addedRemovedNodes = [...mutation.addedNodes, ...mutation.removedNodes]
85
+ if (addedRemovedNodes.some(node => node.tagName === 'TAG-OPTION')) {
86
+ needsTagOptionsUpdate = true;
87
+ }
88
+ if (addedRemovedNodes.some(node => node.tagName === 'DATALIST')) {
89
+ needsAutocompleteUpdate = true;
90
+ }
91
+ } else if (mutation.type === 'attributes') {
92
+ // Handle attribute changes on tag-option elements
93
+ if (mutation.target !== this && mutation.target.tagName === 'TAG-OPTION') {
94
+ needsTagOptionsUpdate = true;
95
+ }
96
+ }
97
+ }
98
+
99
+ if (needsTagOptionsUpdate || needsAutocompleteUpdate) {
100
+ this.unobserve();
101
+ if (needsTagOptionsUpdate) {
102
+ this.processTagOptions();
103
+ }
104
+ if (needsAutocompleteUpdate && this.initialized) {
105
+ this.setupAutocomplete();
106
+ }
107
+ this.observe();
108
+ }
109
+ });
110
+ }
111
+
112
+ unobserve() {
113
+ this.observer.disconnect();
114
+ }
115
+
116
+ observe() {
117
+ this.observer.observe(this, {
118
+ childList: true,
119
+ attributes: true,
120
+ subtree: true,
121
+ attributeFilter: ["value"],
122
+ });
123
+ }
124
+
125
+ processTagOptions() {
126
+ if(!this._taggle || !this._taggle.tag) return
127
+ let tagOptions = Array.from(this.children).filter(e => e.tagName === 'TAG-OPTION')
128
+ let values = tagOptions.map(e => e.value).filter(value => value !== null && value !== undefined)
129
+
130
+ // Enforce maxTags constraint for single mode
131
+ if (!this.multiple && values.length > 1) {
132
+ // Remove excess tag-options from DOM (keep only the first one)
133
+ tagOptions.slice(1).forEach(el => el.remove())
134
+ tagOptions = tagOptions.slice(0, 1)
135
+ values = values.slice(0, 1)
136
+ }
137
+
138
+ this._taggle.tag.elements = tagOptions
139
+ this._taggle.tag.values = values
140
+ this._inputPosition = this._taggle.tag.values.length;
141
+
142
+ // Update the taggle display elements to match the current values
143
+ const taggleElements = this._taggle.tag.elements;
144
+ taggleElements.forEach((element, index) => {
145
+ if (element && element.setAttribute) {
146
+ element.setAttribute('data-value', values[index]);
147
+ }
148
+ });
149
+
150
+ // Update internal value to match
151
+ this.updateValue();
152
+
153
+ // Ensure input visibility is updated when tags change via DOM
154
+ this.updateInputVisibility();
155
+ }
156
+
157
+ get form() {
158
+ return this._internals.form;
159
+ }
160
+
161
+ _setFormValue(values) {
162
+ this._internals.value = values;
163
+
164
+ const formData = new FormData();
165
+ values.forEach(value => formData.append(this.name, value));
166
+ // Always append empty string when no values so server knows to clear the field
167
+ // (like Rails multiple checkboxes which prepend an empty hidden field)
168
+ if (values.length === 0) {
169
+ formData.append(this.name, "");
170
+ }
171
+ this._internals.setFormValue(formData);
172
+ }
173
+
174
+ get name() {
175
+ return this.getAttribute("name");
176
+ }
177
+
178
+ get multiple() {
179
+ return this.hasAttribute('multiple');
180
+ }
181
+
182
+ get value() {
183
+ const internalValue = this._internals.value;
184
+ if (this.multiple) {
185
+ return internalValue; // Return array for multiple mode
186
+ } else {
187
+ return internalValue.length > 0 ? internalValue[0] : ''; // Return string for single mode
188
+ }
189
+ }
190
+
191
+ set value(input) {
192
+ // Convert input to array format for internal storage
193
+ let values;
194
+ if (Array.isArray(input)) {
195
+ values = input;
196
+ } else if (typeof input === 'string') {
197
+ values = input === '' ? [] : [input];
198
+ } else {
199
+ values = [];
200
+ }
201
+
202
+ const oldValues = this._internals.value;
203
+ this._setFormValue(values);
204
+
205
+ // Update taggle to match the new values
206
+ if (this._taggle && this.initialized) {
207
+ this.suppressEvents = true; // Prevent infinite loops
208
+ this._taggle.removeAll();
209
+ if (values.length > 0) {
210
+ this._taggle.add(values);
211
+ }
212
+ this.suppressEvents = false;
213
+ }
214
+
215
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
216
+ this.dispatchEvent(new CustomEvent("change", {
217
+ bubbles: true,
218
+ composed: true,
219
+ }));
220
+ }
221
+ }
222
+
223
+ reset() {
224
+ this._taggle.removeAll()
225
+ this._taggleInputTarget.value = ''
226
+ }
227
+
228
+ get options() {
229
+ const datalistId = this.getAttribute("list")
230
+ if(datalistId) {
231
+ const datalist = document.getElementById(datalistId)
232
+ if(datalist) {
233
+ return [...datalist.options].map(option => option.value).filter(value => value !== null && value !== undefined)
234
+ }
235
+ }
236
+
237
+ // Fall back to nested datalist
238
+ const nestedDatalist = this.querySelector('datalist')
239
+ if(nestedDatalist) {
240
+ return [...nestedDatalist.options].map(option => option.hasAttribute('value') ? option.value : option.textContent).filter(value => value !== null && value !== undefined)
241
+ }
242
+
243
+ return []
244
+ }
245
+
246
+ _getOptionsWithLabels() {
247
+ const datalistId = this.getAttribute("list")
248
+ if(datalistId) {
249
+ const datalist = document.getElementById(datalistId)
250
+ if(datalist) {
251
+ return [...datalist.options].map(option => ({
252
+ value: option.value,
253
+ label: option.textContent || option.value
254
+ })).filter(item => item.value !== null && item.value !== undefined)
255
+ }
256
+ }
257
+
258
+ // Fall back to nested datalist
259
+ const nestedDatalist = this.querySelector('datalist')
260
+ if(nestedDatalist) {
261
+ return [...nestedDatalist.options].map(option => ({
262
+ value: option.hasAttribute('value') ? option.value : option.textContent,
263
+ label: option.textContent || option.value
264
+ })).filter(item => item.value !== null && item.value !== undefined)
265
+ }
266
+
267
+ return []
268
+ }
269
+
270
+ async connectedCallback() {
271
+ this.setAttribute('tabindex', '0');
272
+ this.addEventListener("focus", e => this.focus(e));
273
+
274
+ // Wait for child tag-option elements to be fully connected
275
+ await new Promise(resolve => setTimeout(resolve, 0));
276
+
277
+ this._shadowRoot.innerHTML = `
278
+ <style>
279
+ :host { display: block; }
280
+ :host *{
281
+ position: relative;
282
+ box-sizing: border-box;
283
+ margin: 0;
284
+ padding: 0;
285
+ }
286
+ #container {
287
+ background: rgba(255, 255, 255, 0.8);
288
+ padding: 6px 6px 3px;
289
+ max-height: none;
290
+ display: flex;
291
+ margin: 0;
292
+ flex-wrap: wrap;
293
+ align-items: flex-start;
294
+ min-height: 48px;
295
+ line-height: 48px;
296
+ width: 100%;
297
+ border: 1px solid #d0d0d0;
298
+ outline: 1px solid transparent;
299
+ box-shadow: #ccc 0 1px 4px 0 inset;
300
+ border-radius: 2px;
301
+ cursor: text;
302
+ color: #333;
303
+ list-style: none;
304
+ padding-right: 32px;
305
+ }
306
+ input {
307
+ display: block;
308
+ height: 38px;
309
+ float: none;
310
+ margin: 0;
311
+ padding-left: 10px !important;
312
+ padding-right: 30px !important;
313
+ width: auto !important;
314
+ min-width: 70px;
315
+ font-size: 14px;
316
+ width: 100%;
317
+ line-height: 2;
318
+ padding: 0 0 0 10px;
319
+ border: 1px dashed #d0d0d0;
320
+ outline: 1px solid transparent;
321
+ background: #fff;
322
+ box-shadow: none;
323
+ border-radius: 2px;
324
+ cursor: text;
325
+ color: #333;
326
+ }
327
+ button {
328
+ width: 38px;
329
+ text-align: center;
330
+ line-height: 36px;
331
+ border: 1px solid #e0e0e0;
332
+ font-size: 20px;
333
+ color: #666;
334
+ position: absolute !important;
335
+ z-index: 10;
336
+ right: 0px;
337
+ top: 0;
338
+ font-weight: 400;
339
+ cursor: pointer;
340
+ background: none;
341
+ }
342
+ .taggle_sizer{
343
+ padding: 0;
344
+ margin: 0;
345
+ position: absolute;
346
+ top: -500px;
347
+ z-index: -1;
348
+ visibility: hidden;
349
+ }
350
+ .ui-autocomplete{
351
+ position: static !important;
352
+ width: 100% !important;
353
+ margin-top: 2px;
354
+ }
355
+ .ui-menu{
356
+ margin: 0;
357
+ padding: 6px;
358
+ box-shadow: #ccc 0 1px 6px;
359
+ z-index: 2;
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ background: #fff;
363
+ list-style: none;
364
+ font-size: 14px;
365
+ min-width: 200px;
366
+ }
367
+ .ui-menu .ui-menu-item{
368
+ display: inline-block;
369
+ margin: 0 0 2px;
370
+ line-height: 30px;
371
+ border: none;
372
+ padding: 0 10px;
373
+ text-indent: 0;
374
+ border-radius: 2px;
375
+ width: auto;
376
+ cursor: pointer;
377
+ color: #555;
378
+ }
379
+ .ui-menu .ui-menu-item::before{ display: none; }
380
+ .ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
381
+ .ui-state-active{
382
+ padding: 0;
383
+ border: none;
384
+ background: none;
385
+ color: inherit;
386
+ }
387
+ </style>
388
+ <div style="position: relative;">
389
+ <div id="container">
390
+ <slot></slot>
391
+ </div>
392
+ <input
393
+ id="inputTarget"
394
+ type="hidden"
395
+ name="${this.name}"
396
+ />
397
+ </div>
398
+ `;
399
+
400
+ this.form?.addEventListener("reset", this.reset.bind(this));
401
+
402
+ this.containerTarget = this.shadowRoot.querySelector("#container");
403
+ this.inputTarget = this.shadowRoot.querySelector("#inputTarget");
404
+
405
+ this.required = this.hasAttribute("required")
406
+
407
+ const maxTags = this.multiple ? undefined : 1
408
+ const placeholder = this.inputTarget.getAttribute("placeholder")
409
+
410
+ this.inputTarget.value = ""
411
+ this.inputTarget.id = ""
412
+
413
+ this._taggle = new Taggle(this, {
414
+ inputContainer: this.containerTarget,
415
+ preserveCase: true,
416
+ hiddenInputName: this.name,
417
+ maxTags: maxTags,
418
+ placeholder: placeholder,
419
+ onTagAdd: (event, tag) => this.onTagAdd(event, tag),
420
+ onTagRemove: (event, tag) => this.onTagRemove(event, tag),
421
+ })
422
+ this._taggleInputTarget = this._taggle.getInput()
423
+ this._taggleInputTarget.id = this.id
424
+ this._taggleInputTarget.autocomplete = "off"
425
+ this._taggleInputTarget.setAttribute("data-turbo-permanent", true)
426
+ this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e))
427
+
428
+ // Set initial value after taggle is initialized
429
+ this.value = this._taggle.getTagValues()
430
+
431
+ this.checkRequired()
432
+
433
+ this.buttonTarget = h(`<button class="add">+</button>`)
434
+ this.buttonTarget.addEventListener("click", e => this._add(e))
435
+ this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget)
436
+
437
+ this.autocompleteContainerTarget = h(`<ul>`);
438
+ // Insert autocomplete container into the positioned wrapper div
439
+ const wrapperDiv = this.shadowRoot.querySelector('div[style*="position: relative"]');
440
+ wrapperDiv.appendChild(this.autocompleteContainerTarget)
441
+
442
+ this.setupAutocomplete()
443
+
444
+ this.observe() // Start observing after taggle is set up
445
+ this.initialized = true
446
+
447
+ // Update visibility based on current state
448
+ this.updateInputVisibility()
449
+ }
450
+
451
+ setupAutocomplete() {
452
+ const optionsWithLabels = this._getOptionsWithLabels()
453
+
454
+ autocomplete({
455
+ input: this._taggleInputTarget,
456
+ container: this.autocompleteContainerTarget,
457
+ className: "ui-menu ui-autocomplete",
458
+ fetch: (text, update) => {
459
+ const currentTags = this._taggle.getTagValues()
460
+ const suggestions = optionsWithLabels.filter(option =>
461
+ option.label.toLowerCase().includes(text.toLowerCase()) &&
462
+ !currentTags.includes(option.value)
463
+ )
464
+ // Store the suggestions for testing (can't assign to getter, tests read from DOM)
465
+ update(suggestions)
466
+ },
467
+ render: item => h(`<li class="ui-menu-item" data-value="${item.value}">${item.label}</li>`),
468
+ onSelect: item => {
469
+ // Prevent adding multiple tags in single mode
470
+ if (!this.multiple && this._taggle.getTagValues().length > 0) {
471
+ this._taggleInputTarget.value = ''
472
+ return
473
+ }
474
+
475
+ // Create a tag-option element with proper value/label separation
476
+ const tagOption = document.createElement('tag-option')
477
+ tagOption.setAttribute('value', item.value)
478
+ tagOption.textContent = item.label
479
+ this.appendChild(tagOption)
480
+
481
+ // Clear input
482
+ this._taggleInputTarget.value = ''
483
+ },
484
+ minLength: 1,
485
+ customize: (input, inputRect, container, maxHeight) => {
486
+ // Position autocomplete below the input-tag container, accounting for dynamic height
487
+ this._updateAutocompletePosition(container);
488
+
489
+ // Store reference to update positioning when container height changes
490
+ this._autocompleteContainer = container;
491
+ }
492
+ })
493
+ }
494
+
495
+ disconnectedCallback() {
496
+ this.form?.removeEventListener("reset", this.reset.bind(this));
497
+ this.unobserve();
498
+ }
499
+
500
+ attributeChangedCallback(name, oldValue, newValue) {
501
+ if (oldValue === newValue) return;
502
+
503
+ // Only handle changes after the component is connected and initialized
504
+ if (!this._taggle) return;
505
+
506
+ switch (name) {
507
+ case 'name':
508
+ this.handleNameChange(newValue);
509
+ break;
510
+ case 'multiple':
511
+ this.handleMultipleChange(newValue !== null);
512
+ break;
513
+ case 'required':
514
+ this.handleRequiredChange(newValue !== null);
515
+ break;
516
+ case 'list':
517
+ this.handleListChange(newValue);
518
+ break;
519
+ }
520
+ }
521
+
522
+ checkRequired() {
523
+ const flag = this.required && this._taggle.getTagValues().length == 0
524
+ this._taggleInputTarget.required = flag
525
+
526
+ // Update ElementInternals validity to match internal input
527
+ if (flag) {
528
+ this._internals.setValidity({ valueMissing: true }, 'Please fill out this field.', this._taggleInputTarget)
529
+ } else {
530
+ this._internals.setValidity({})
531
+ }
532
+ }
533
+
534
+ // monkeypatch support for android comma
535
+ keyup(event) {
536
+ const key = event.which || event.keyCode
537
+ const normalKeyboard = key != 229
538
+ if(normalKeyboard) return
539
+ const value = this._taggleInputTarget.value
540
+
541
+ // backspace
542
+ if(value.length == 0) {
543
+ const values = this._taggle.tag.values
544
+ this._taggle.remove(values[values.length - 1])
545
+ return
546
+ }
547
+
548
+ // comma
549
+ if(/,$/.test(value)) {
550
+ const tag = value.replace(',', '')
551
+ this._taggle.add(tag)
552
+ this._taggleInputTarget.value = ''
553
+ return
554
+ }
555
+ }
556
+
557
+ _add(event) {
558
+ event.preventDefault()
559
+ this._taggle.add(this._taggleInputTarget.value)
560
+ this._taggleInputTarget.value = ''
561
+ }
562
+
563
+ onTagAdd(event, tag) {
564
+ if (!this.suppressEvents) {
565
+ const isNew = !this.options.includes(tag)
566
+ this.dispatchEvent(new CustomEvent("update", {
567
+ detail: { tag, isNew },
568
+ bubbles: true,
569
+ composed: true,
570
+ }));
571
+ }
572
+ this.syncValue()
573
+ this.checkRequired()
574
+ this.updateInputVisibility()
575
+
576
+ // Update autocomplete position if it's currently open
577
+ if (this._autocompleteContainer) {
578
+ // Use setTimeout to allow DOM to update first
579
+ setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0)
580
+ }
581
+ }
582
+
583
+ onTagRemove(event, tag) {
584
+ if (!this.suppressEvents) {
585
+ this.dispatchEvent(new CustomEvent("update", {
586
+ detail: { tag },
587
+ bubbles: true,
588
+ composed: true,
589
+ }));
590
+ }
591
+ this.syncValue()
592
+ this.checkRequired()
593
+ this.updateInputVisibility()
594
+
595
+ // Update autocomplete position if it's currently open
596
+ if (this._autocompleteContainer) {
597
+ // Use setTimeout to allow DOM to update first
598
+ setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0)
599
+ }
600
+ }
601
+
602
+ syncValue() {
603
+ // Directly update internals without triggering the setter
604
+ const values = this._taggle.getTagValues()
605
+ const oldValues = this._internals.value;
606
+ this._setFormValue(values);
607
+
608
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
609
+ this.dispatchEvent(new CustomEvent("change", {
610
+ bubbles: true,
611
+ composed: true,
612
+ }));
613
+ }
614
+ }
615
+
616
+ // Public API methods
617
+ add(tags) {
618
+ if (!this._taggle) return
619
+ this._taggle.add(tags)
620
+ }
621
+
622
+ remove(tag) {
623
+ if (!this._taggle) return
624
+ this._taggle.remove(tag)
625
+ }
626
+
627
+ removeAll() {
628
+ if (!this._taggle) return
629
+ this._taggle.removeAll()
630
+ }
631
+
632
+ has(tag) {
633
+ if (!this._taggle) return false
634
+ return this._taggle.getTagValues().includes(tag)
635
+ }
636
+
637
+ get tags() {
638
+ if (!this._taggle) return []
639
+ return this._taggle.getTagValues()
640
+ }
641
+
642
+ // Private getter for testing autocomplete suggestions
643
+ get _autocompleteSuggestions() {
644
+ if (!this.autocompleteContainerTarget) return []
645
+ const items = this.autocompleteContainerTarget.querySelectorAll('.ui-menu-item')
646
+ return Array.from(items).map(item => item.textContent.trim())
647
+ }
648
+
649
+ // Update autocomplete position based on current container height
650
+ _updateAutocompletePosition(container) {
651
+ if (!container) return
652
+
653
+ const inputTagRect = this.containerTarget.getBoundingClientRect();
654
+
655
+ container.style.setProperty('position', 'absolute', 'important');
656
+ container.style.setProperty('top', `${inputTagRect.height}px`, 'important');
657
+ container.style.setProperty('left', '0', 'important');
658
+ container.style.setProperty('right', '0', 'important');
659
+ container.style.setProperty('width', '100%', 'important');
660
+ container.style.setProperty('z-index', '1000', 'important');
661
+ }
662
+
663
+ updateInputVisibility() {
664
+ if (!this._taggleInputTarget || !this.buttonTarget) return;
665
+
666
+ const hasTags = this._taggle && this._taggle.getTagValues().length > 0;
667
+
668
+ if (this.multiple) {
669
+ // Multiple mode: always show input and button
670
+ this._taggleInputTarget.style.display = '';
671
+ this.buttonTarget.style.display = '';
672
+ } else {
673
+ // Single mode: hide input and button when tag exists
674
+ if (hasTags) {
675
+ this._taggleInputTarget.style.display = 'none';
676
+ this.buttonTarget.style.display = 'none';
677
+ } else {
678
+ this._taggleInputTarget.style.display = '';
679
+ this.buttonTarget.style.display = '';
680
+ }
681
+ }
682
+ }
683
+
684
+ addAt(tag, index) {
685
+ if (!this._taggle) return
686
+ this._taggle.add(tag, index)
687
+ }
688
+
689
+ disable() {
690
+ if (this._taggle) {
691
+ this._taggle.disable()
692
+ }
693
+ }
694
+
695
+ enable() {
696
+ if (this._taggle) {
697
+ this._taggle.enable()
698
+ }
699
+ }
700
+
701
+ focus() {
702
+ if (this._taggleInputTarget) {
703
+ this._taggleInputTarget.focus()
704
+ }
705
+ }
706
+
707
+ checkValidity() {
708
+ if (this._taggle) {
709
+ this.checkRequired()
710
+ }
711
+ return this._internals.checkValidity()
712
+ }
713
+
714
+ reportValidity() {
715
+ if (this._taggle) {
716
+ this.checkRequired()
717
+ }
718
+ return this._internals.reportValidity()
719
+ }
720
+
721
+ handleNameChange(newName) {
722
+ // Update the hidden input name to match
723
+ const hiddenInput = this._shadowRoot.querySelector('input[type="hidden"]');
724
+ if (hiddenInput) {
725
+ hiddenInput.name = newName || '';
726
+ }
727
+
728
+ // Update the form value with the new name
729
+ if (this._internals.value) {
730
+ this.value = this._internals.value; // This will recreate FormData with new name
731
+ }
732
+ }
733
+
734
+ handleMultipleChange(isMultiple) {
735
+ if (!this._taggle) return;
736
+
737
+ // Get current tags
738
+ const currentTags = this._taggle.getTagValues();
739
+
740
+ if (!isMultiple && currentTags.length > 1) {
741
+ // Single mode: remove excess tag-option elements from DOM
742
+ const tagOptions = Array.from(this.children);
743
+ // Keep only the first tag-option element, remove the rest
744
+ tagOptions.forEach((tagOption, i) => {
745
+ if (i > 0 && tagOption) {
746
+ this.removeChild(tagOption);
747
+ }
748
+ });
749
+ }
750
+
751
+ // Reinitialize taggle with new multiple setting
752
+ this.reinitializeTaggle();
753
+
754
+ // Restore tags, respecting the new multiple constraint
755
+ if (isMultiple) {
756
+ // Multiple mode: restore all remaining tags
757
+ if (currentTags.length > 0) {
758
+ this._taggle.add(currentTags);
759
+ }
760
+ } else {
761
+ // Single mode: keep only the first tag
762
+ if (currentTags.length > 0) {
763
+ this._taggle.add(currentTags[0]);
764
+ }
765
+ }
766
+
767
+ this.updateValue();
768
+ this.updateInputVisibility();
769
+ }
770
+
771
+ handleRequiredChange(isRequired) {
772
+ if (!this._taggle) return;
773
+
774
+ // Update the internal required state
775
+ this.required = isRequired;
776
+
777
+ // Update validation
778
+ this.checkRequired();
779
+ }
780
+
781
+ handleListChange(newListId) {
782
+ if (!this._taggle) return;
783
+
784
+ // Re-setup autocomplete with new datalist
785
+ this.setupAutocomplete();
786
+ }
787
+
788
+ reinitializeTaggle() {
789
+ // Clean up existing taggle if it exists
790
+ if (this._taggle && this._taggle.destroy) {
791
+ this._taggle.destroy();
792
+ }
793
+
794
+ // Get current configuration
795
+ const maxTags = this.hasAttribute("multiple") ? undefined : 1;
796
+ const placeholder = this.getAttribute("placeholder") || "";
797
+
798
+ // Create new taggle instance using original configuration pattern
799
+ this._taggle = new Taggle(this, {
800
+ inputContainer: this.containerTarget,
801
+ preserveCase: true,
802
+ hiddenInputName: this.name,
803
+ maxTags: maxTags,
804
+ placeholder: placeholder,
805
+ onTagAdd: (event, tag) => this.onTagAdd(event, tag),
806
+ onTagRemove: (event, tag) => this.onTagRemove(event, tag),
807
+ });
808
+
809
+ // Re-get references since taggle was recreated
810
+ this._taggleInputTarget = this._taggle.getInput();
811
+ this._taggleInputTarget.id = this.id || "";
812
+ this._taggleInputTarget.autocomplete = "off";
813
+ this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
814
+ this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
815
+
816
+ // Re-setup autocomplete
817
+ this.setupAutocomplete();
818
+
819
+ // Re-process existing tag options
820
+ this.processTagOptions();
821
+ }
822
+
823
+ updateValue() {
824
+ if (!this._taggle) return;
825
+
826
+ // Update the internal value to match taggle state
827
+ const values = this._taggle.getTagValues();
828
+ const oldValues = this._internals.value;
829
+ this._setFormValue(values);
830
+
831
+ // Check validity after updating
832
+ this.checkRequired();
833
+
834
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
835
+ this.dispatchEvent(new CustomEvent("change", {
836
+ bubbles: true,
837
+ composed: true,
838
+ }));
839
+ }
840
+ }
841
+ }
842
+ customElements.define("input-tag", InputTag);
843
+
844
+
845
+ function h(html) {
846
+ const container = document.createElement("div")
847
+ container.innerHTML = html
848
+ return container.firstElementChild
849
+ }