bard-tag_field 0.1.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.
@@ -0,0 +1,1827 @@
1
+ /**
2
+ * Taggle - dependency-less tagging library
3
+ * @author Sean Coker <hello@sean.is>
4
+ * @version 1.15.0 (modified)
5
+ * @license MIT
6
+ */
7
+
8
+ /////////////////////
9
+ // Default options //
10
+ /////////////////////
11
+
12
+ const BACKSPACE = 8;
13
+ const DELETE = 46;
14
+ const COMMA = 188;
15
+ const TAB = 9;
16
+ const ENTER = 13;
17
+
18
+ const DEFAULTS = {
19
+ /**
20
+ * Class added to the container div when focused
21
+ * @type {String}
22
+ */
23
+ containerFocusClass: 'active',
24
+
25
+ /**
26
+ * Spaces will be removed from the tags by default
27
+ * @type {Boolean}
28
+ */
29
+ trimTags: true,
30
+
31
+ /**
32
+ * Limit the number of tags that can be added
33
+ * @type {Number}
34
+ */
35
+ maxTags: null,
36
+
37
+ /**
38
+ * Placeholder string to be placed in an empty taggle field
39
+ * @type {String}
40
+ */
41
+ placeholder: 'Enter tags...',
42
+
43
+ /**
44
+ * Keycodes that will add a tag
45
+ * @type {Array}
46
+ */
47
+ submitKeys: [COMMA, TAB, ENTER],
48
+
49
+ /**
50
+ * Preserve case of tags being added ie
51
+ * "tag" is different than "Tag"
52
+ * @type {Boolean}
53
+ */
54
+ preserveCase: false,
55
+
56
+ /**
57
+ * Function hook called when a tag is added
58
+ * @param {Event} event Event triggered when tag was added
59
+ * @param {String} tag The tag added
60
+ */
61
+ onTagAdd: () => {},
62
+
63
+ /**
64
+ * Function hook called when a tag is removed
65
+ * @param {Event} event Event triggered when tag was removed
66
+ * @param {String} tag The tag removed
67
+ */
68
+ onTagRemove: () => {}
69
+ };
70
+
71
+ //////////////////////
72
+ // Helper functions //
73
+ //////////////////////
74
+ function _clamp(val, min, max) {
75
+ return Math.min(Math.max(val, min), max);
76
+ }
77
+
78
+ /**
79
+ * Taggle ES6 Class - Modern tagging library
80
+ */
81
+ class Taggle {
82
+ /**
83
+ * Constructor
84
+ * @param {Mixed} el ID of an element or the actual element
85
+ * @param {Object} options
86
+ */
87
+ constructor(el, options) {
88
+ this.settings = Object.assign({}, DEFAULTS, options);
89
+ this.measurements = {
90
+ container: {
91
+ rect: null,
92
+ style: null,
93
+ padding: null
94
+ }
95
+ };
96
+ this.container = el;
97
+ this.tag = {
98
+ values: [],
99
+ elements: []
100
+ };
101
+ this.inputContainer = options.inputContainer;
102
+ this.input = document.createElement('input');
103
+ this.sizer = document.createElement('div');
104
+ this.pasting = false;
105
+ this.placeholder = null;
106
+
107
+ if (this.settings.placeholder) {
108
+ this.placeholder = document.createElement('span');
109
+ }
110
+
111
+ this._backspacePressed = false;
112
+ this._inputPosition = 0;
113
+ this._setMeasurements();
114
+ this._setupTextarea();
115
+ this._attachEvents();
116
+ }
117
+
118
+ /**
119
+ * Gets all the layout measurements up front
120
+ */
121
+ _setMeasurements() {
122
+ this.measurements.container.rect = this.container.getBoundingClientRect();
123
+ const style = window.getComputedStyle(this.container);
124
+ this.measurements.container.style = style;
125
+
126
+ const lpad = parseInt(style.paddingLeft, 10);
127
+ const rpad = parseInt(style.paddingRight, 10);
128
+ const lborder = parseInt(style.borderLeftWidth, 10);
129
+ const rborder = parseInt(style.borderRightWidth, 10);
130
+
131
+ this.measurements.container.padding = lpad + rpad + lborder + rborder;
132
+ }
133
+
134
+ /**
135
+ * Setup the div container for tags to be entered
136
+ */
137
+ _setupTextarea() {
138
+ this.input.type = 'text';
139
+ // Make sure no left/right padding messes with the input sizing
140
+ this.input.style.paddingLeft = 0;
141
+ this.input.style.paddingRight = 0;
142
+ this.input.className = 'taggle_input';
143
+ this.input.tabIndex = 1;
144
+ this.sizer.className = 'taggle_sizer';
145
+
146
+ [...this.container.children].filter(child => child.tagName === 'TAG-OPTION').forEach(tagOption => {
147
+ this.tag.values.push(tagOption.value);
148
+ this.tag.elements.push(tagOption);
149
+ this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
150
+ });
151
+
152
+
153
+ if (this.placeholder) {
154
+ this._hidePlaceholder();
155
+ this.placeholder.classList.add('taggle_placeholder');
156
+ this.container.appendChild(this.placeholder);
157
+ this.placeholder.textContent = this.settings.placeholder;
158
+
159
+ if (!this.tag.values.length) {
160
+ this._showPlaceholder();
161
+ }
162
+ }
163
+
164
+
165
+ const div = document.createElement('div');
166
+ div.appendChild(this.input);
167
+ div.appendChild(this.sizer);
168
+ this.inputContainer.appendChild(div);
169
+ const fontSize = window.getComputedStyle(this.input).fontSize;
170
+ this.sizer.style.fontSize = fontSize;
171
+ }
172
+
173
+ /**
174
+ * Attaches neccessary events
175
+ */
176
+ _attachEvents() {
177
+ if (this._eventsAttached) {
178
+ return false;
179
+ }
180
+ this._eventsAttached = true;
181
+
182
+ this._handleContainerClick = () => this.input.focus();
183
+ this.container.addEventListener('click', this._handleContainerClick);
184
+
185
+ this._handleFocus = this._setFocusStateForContainer.bind(this);
186
+ this._handleBlur = this._blurEvent.bind(this);
187
+ this._handleKeydown = this._keydownEvents.bind(this);
188
+ this._handleKeyup = this._keyupEvents.bind(this);
189
+
190
+ this.input.addEventListener('focus', this._handleFocus);
191
+ this.input.addEventListener('blur', this._handleBlur);
192
+ this.input.addEventListener('keydown', this._handleKeydown);
193
+ this.input.addEventListener('keyup', this._handleKeyup);
194
+
195
+ return true;
196
+ }
197
+
198
+ _detachEvents() {
199
+ if (!this._eventsAttached) {
200
+ return false;
201
+ }
202
+ this._eventsAttached = false;
203
+
204
+ this.container.removeEventListener('click', this._handleContainerClick);
205
+ this.input.removeEventListener('focus', this._handleFocus);
206
+ this.input.removeEventListener('blur', this._handleBlur);
207
+ this.input.removeEventListener('keydown', this._handleKeydown);
208
+ this.input.removeEventListener('keyup', this._handleKeyup);
209
+
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * Returns whether or not the specified tag text can be added
215
+ * @param {Event} e event causing the potentially added tag
216
+ * @param {String} text tag value
217
+ * @return {Boolean}
218
+ */
219
+ _canAdd(e, text) {
220
+ if (!text) {
221
+ return false;
222
+ }
223
+ const limit = this.settings.maxTags;
224
+ if (limit !== null && limit <= this.getTagValues().length) {
225
+ return false;
226
+ }
227
+
228
+ // Check for duplicates
229
+ return this.tag.values.indexOf(text) === -1;
230
+ }
231
+
232
+ /**
233
+ * Appends tag with its corresponding input to the list
234
+ * @param {Event} e
235
+ * @param {String} text
236
+ * @param {Number} index
237
+ */
238
+ _add(e, text, index) {
239
+ let values = text || '';
240
+ const delimiter = ',';
241
+
242
+ if (typeof text !== 'string') {
243
+ values = this.input.value;
244
+
245
+ if (this.settings.trimTags) {
246
+ if (values[0] === delimiter) {
247
+ values = values.replace(delimiter, '');
248
+ }
249
+ values = values.trim();
250
+ }
251
+ }
252
+
253
+ values.split(delimiter).map(val => {
254
+ if (this.settings.trimTags) {
255
+ val = val.trim();
256
+ }
257
+ return this._formatTag(val);
258
+ }).forEach(val => {
259
+ if (!this._canAdd(e, val)) {
260
+ return;
261
+ }
262
+
263
+ const currentTagLength = this.tag.values.length;
264
+ const tagIndex = _clamp(index || currentTagLength, 0, currentTagLength);
265
+ const tagOption = this._createTag(val, tagIndex);
266
+ this.container.append(tagOption);
267
+
268
+ val = this.tag.values[tagIndex];
269
+
270
+ this.settings.onTagAdd(e, val);
271
+
272
+ this.input.value = '';
273
+ this._setMeasurements();
274
+ this._setInputWidth();
275
+ this._setFocusStateForContainer();
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Removes last tag if it has already been probed
281
+ * @param {Event} e
282
+ */
283
+ _checkPrevOrNextTag(e) {
284
+ const taggles = this.container.querySelectorAll('tag-option');
285
+ const prevTagIndex = _clamp(this._inputPosition - 1, 0, taggles.length - 1);
286
+ const nextTagIndex = _clamp(this._inputPosition, 0, taggles.length - 1);
287
+ let index = prevTagIndex;
288
+
289
+ if (e.keyCode === DELETE) {
290
+ index = nextTagIndex;
291
+ }
292
+
293
+ const targetTaggle = taggles[index];
294
+ const hotClass = 'taggle_hot';
295
+ const isDeleteOrBackspace = [BACKSPACE, DELETE].includes(e.keyCode);
296
+
297
+ // prevent holding backspace from deleting all tags
298
+ if (this.input.value === '' && isDeleteOrBackspace && !this._backspacePressed) {
299
+ if (targetTaggle.classList.contains(hotClass)) {
300
+ this._backspacePressed = true;
301
+ this._remove(targetTaggle, e);
302
+ this._setMeasurements();
303
+ this._setInputWidth();
304
+ this._setFocusStateForContainer();
305
+ }
306
+ else {
307
+ targetTaggle.classList.add(hotClass);
308
+ }
309
+ }
310
+ else if (targetTaggle.classList.contains(hotClass)) {
311
+ targetTaggle.classList.remove(hotClass);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Setter for the hidden input.
317
+ * @param {Number} width
318
+ */
319
+ _setInputWidth() {
320
+ const width = this.sizer.getBoundingClientRect().width;
321
+ const max = this.measurements.container.rect.width - this.measurements.container.padding;
322
+ const size = parseInt(this.sizer.style.fontSize, 10);
323
+
324
+ // 1.5 just seems to be a good multiplier here
325
+ const newWidth = Math.round(_clamp(width + (size * 1.5), 10, max));
326
+
327
+ this.input.style.width = `${newWidth}px`;
328
+ }
329
+
330
+ /**
331
+ * Handles focus state of div container.
332
+ */
333
+ _setFocusStateForContainer() {
334
+ this._setMeasurements();
335
+ this._setInputWidth();
336
+
337
+ if (!this.container.classList.contains(this.settings.containerFocusClass)) {
338
+ this.container.classList.add(this.settings.containerFocusClass);
339
+ }
340
+
341
+ this._hidePlaceholder();
342
+ }
343
+
344
+ /**
345
+ * Runs all the events that need to happen on a blur
346
+ * @param {Event} e
347
+ */
348
+ _blurEvent(e) {
349
+ if (this.container.classList.contains(this.settings.containerFocusClass)) {
350
+ this.container.classList.remove(this.settings.containerFocusClass);
351
+ }
352
+
353
+ if (!this.tag.values.length && !this.input.value) {
354
+ this._showPlaceholder();
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Runs all the events that need to run on keydown
360
+ * @param {Event} e
361
+ */
362
+ _keydownEvents(e) {
363
+ const key = e.keyCode;
364
+ this.pasting = false;
365
+
366
+ this._setInputWidth();
367
+
368
+ if (key === 86 && e.metaKey) {
369
+ this.pasting = true;
370
+ }
371
+
372
+ if (this.settings.submitKeys.includes(key) && this.input.value !== '') {
373
+ this._confirmValidTagEvent(e);
374
+ return;
375
+ }
376
+
377
+ if (this.tag.values.length) {
378
+ this._checkPrevOrNextTag(e);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Runs all the events that need to run on keyup
384
+ * @param {Event} e
385
+ */
386
+ _keyupEvents(e) {
387
+ this._backspacePressed = false;
388
+
389
+ this.sizer.textContent = this.input.value;
390
+
391
+ // If we break to a new line because the text is too long
392
+ // and decide to delete everything, we should resize the input
393
+ // so it falls back inline
394
+ if (!this.input.value) {
395
+ this._setInputWidth();
396
+ }
397
+
398
+ if (this.pasting && this.input.value !== '') {
399
+ this._add(e);
400
+ this.pasting = false;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Confirms the inputted value to be converted to a tag
406
+ * @param {Event} e
407
+ */
408
+ _confirmValidTagEvent(e) {
409
+ // prevents from jumping out of textarea
410
+ e.preventDefault();
411
+
412
+ this._add(e, null, this._inputPosition);
413
+ }
414
+
415
+ _createTag(text, index) {
416
+ const tagOption = document.createElement('tag-option');
417
+
418
+ text = this._formatTag(text);
419
+ tagOption.textContent = text;
420
+ tagOption.setAttribute('value', text);
421
+
422
+ this.tag.values.splice(index, 0, text);
423
+ this.tag.elements.splice(index, 0, tagOption);
424
+ this._inputPosition = _clamp(this._inputPosition + 1, 0, this.tag.values.length);
425
+
426
+ return tagOption;
427
+ }
428
+
429
+ _showPlaceholder() {
430
+ if (this.placeholder) {
431
+ this.placeholder.style.opacity = 1;
432
+ this.placeholder.setAttribute('aria-hidden', 'false');
433
+ }
434
+ }
435
+
436
+ _hidePlaceholder() {
437
+ if (this.placeholder) {
438
+ this.placeholder.style.opacity = 0;
439
+ this.placeholder.setAttribute('aria-hidden', 'true');
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Removes tag from the tags collection
445
+ * @param {HTMLElement} tagOption List item to remove
446
+ * @param {Event} e
447
+ */
448
+ _remove(tagOption, e) {
449
+ const index = this.tag.elements.indexOf(tagOption);
450
+ if (index === -1) return;
451
+
452
+ const text = this.tag.values[index];
453
+
454
+ tagOption.remove();
455
+ this.tag.elements.splice(index, 1);
456
+ this.tag.values.splice(index, 1);
457
+ this.settings.onTagRemove(e, text);
458
+
459
+ if (index < this._inputPosition) {
460
+ this._inputPosition = _clamp(this._inputPosition - 1, 0, this.tag.values.length);
461
+ }
462
+
463
+ this._setFocusStateForContainer();
464
+ }
465
+
466
+ /**
467
+ * Format the text for a tag
468
+ * @param {String} text Tag text
469
+ * @return {String}
470
+ */
471
+ _formatTag(text) {
472
+ return this.settings.preserveCase ? text : text.toLowerCase();
473
+ }
474
+
475
+ // @todo
476
+ // @deprecated use getTags().values
477
+ getTagValues() {
478
+ return [...this.tag.values];
479
+ }
480
+
481
+ getInput() {
482
+ return this.input;
483
+ }
484
+
485
+ add(text, index) {
486
+ const isArr = Array.isArray(text);
487
+
488
+ if (isArr) {
489
+ text.forEach((tag, i) => {
490
+ if (typeof tag === 'string') {
491
+ this._add(null, tag, index ? index + i : index);
492
+ }
493
+ });
494
+ }
495
+ else {
496
+ this._add(null, text, index);
497
+ }
498
+
499
+ return this;
500
+ }
501
+
502
+ remove(text) {
503
+ const index = this.tag.values.indexOf(text);
504
+ if (index > -1) {
505
+ this._remove(this.tag.elements[index]);
506
+ }
507
+ return this;
508
+ }
509
+
510
+ removeAll() {
511
+ [...this.tag.elements].forEach(element => this._remove(element));
512
+ this._showPlaceholder();
513
+ return this;
514
+ }
515
+
516
+ _setDisabledState(disabled) {
517
+ const elements = [
518
+ ...this.container.querySelectorAll('button'),
519
+ ...this.container.querySelectorAll('input')
520
+ ];
521
+
522
+ elements.forEach((el) => {
523
+ if (disabled) {
524
+ el.setAttribute('disabled', '');
525
+ } else {
526
+ el.removeAttribute('disabled');
527
+ }
528
+ });
529
+
530
+ return this;
531
+ }
532
+
533
+ enable() {
534
+ return this._setDisabledState(false);
535
+ }
536
+
537
+ disable() {
538
+ return this._setDisabledState(true);
539
+ }
540
+
541
+ destroy() {
542
+ this._detachEvents();
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Copyright (c) 2016 Denis Taran
548
+ *
549
+ * Homepage: https://smartscheduling.com/en/documentation/autocomplete
550
+ * Source: https://github.com/denis-taran/autocomplete
551
+ *
552
+ * MIT License
553
+ */
554
+ function autocomplete(settings) {
555
+ // just an alias to minimize JS file size
556
+ var doc = document;
557
+ var container = settings.container || doc.createElement('div');
558
+ var preventSubmit = settings.preventSubmit || 0 /* Never */;
559
+ container.id = container.id || 'autocomplete-' + uid();
560
+ var containerStyle = container.style;
561
+ var debounceWaitMs = settings.debounceWaitMs || 0;
562
+ var disableAutoSelect = settings.disableAutoSelect || false;
563
+ var customContainerParent = container.parentElement;
564
+ var items = [];
565
+ var inputValue = '';
566
+ var minLen = 2;
567
+ var showOnFocus = settings.showOnFocus;
568
+ var selected;
569
+ var fetchCounter = 0;
570
+ var debounceTimer;
571
+ var destroyed = false;
572
+ // Fixes #104: autocomplete selection is broken on Firefox for Android
573
+ var suppressAutocomplete = false;
574
+ if (settings.minLength !== undefined) {
575
+ minLen = settings.minLength;
576
+ }
577
+ if (!settings.input) {
578
+ throw new Error('input undefined');
579
+ }
580
+ var input = settings.input;
581
+ container.className = [container.className, 'autocomplete', settings.className || ''].join(' ').trim();
582
+ container.setAttribute('role', 'listbox');
583
+ input.setAttribute('role', 'combobox');
584
+ input.setAttribute('aria-expanded', 'false');
585
+ input.setAttribute('aria-autocomplete', 'list');
586
+ input.setAttribute('aria-controls', container.id);
587
+ input.setAttribute('aria-owns', container.id);
588
+ input.setAttribute('aria-activedescendant', '');
589
+ input.setAttribute('aria-haspopup', 'listbox');
590
+ // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning
591
+ containerStyle.position = 'absolute';
592
+ /**
593
+ * Generate a very complex textual ID that greatly reduces the chance of a collision with another ID or text.
594
+ */
595
+ function uid() {
596
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
597
+ }
598
+ /**
599
+ * Detach the container from DOM
600
+ */
601
+ function detach() {
602
+ var parent = container.parentNode;
603
+ if (parent) {
604
+ parent.removeChild(container);
605
+ }
606
+ }
607
+ /**
608
+ * Clear debouncing timer if assigned
609
+ */
610
+ function clearDebounceTimer() {
611
+ if (debounceTimer) {
612
+ window.clearTimeout(debounceTimer);
613
+ }
614
+ }
615
+ /**
616
+ * Attach the container to DOM
617
+ */
618
+ function attach() {
619
+ if (!container.parentNode) {
620
+ (customContainerParent || doc.body).appendChild(container);
621
+ }
622
+ }
623
+ /**
624
+ * Check if container for autocomplete is displayed
625
+ */
626
+ function containerDisplayed() {
627
+ return !!container.parentNode;
628
+ }
629
+ /**
630
+ * Clear autocomplete state and hide container
631
+ */
632
+ function clear() {
633
+ // prevent the update call if there are pending AJAX requests
634
+ fetchCounter++;
635
+ items = [];
636
+ inputValue = '';
637
+ selected = undefined;
638
+ input.setAttribute('aria-activedescendant', '');
639
+ input.setAttribute('aria-expanded', 'false');
640
+ detach();
641
+ }
642
+ /**
643
+ * Update autocomplete position
644
+ */
645
+ function updatePosition() {
646
+ if (!containerDisplayed()) {
647
+ return;
648
+ }
649
+ input.setAttribute('aria-expanded', 'true');
650
+ containerStyle.height = 'auto';
651
+ containerStyle.width = input.offsetWidth + 'px';
652
+ var maxHeight = 0;
653
+ var inputRect;
654
+ function calc() {
655
+ var docEl = doc.documentElement;
656
+ var clientTop = docEl.clientTop || doc.body.clientTop || 0;
657
+ var clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;
658
+ var scrollTop = window.pageYOffset || docEl.scrollTop;
659
+ var scrollLeft = window.pageXOffset || docEl.scrollLeft;
660
+ inputRect = input.getBoundingClientRect();
661
+ var top = inputRect.top + input.offsetHeight + scrollTop - clientTop;
662
+ var left = inputRect.left + scrollLeft - clientLeft;
663
+ containerStyle.top = top + 'px';
664
+ containerStyle.left = left + 'px';
665
+ maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);
666
+ if (maxHeight < 0) {
667
+ maxHeight = 0;
668
+ }
669
+ containerStyle.top = top + 'px';
670
+ containerStyle.bottom = '';
671
+ containerStyle.left = left + 'px';
672
+ containerStyle.maxHeight = maxHeight + 'px';
673
+ }
674
+ // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)
675
+ calc();
676
+ calc();
677
+ if (settings.customize && inputRect) {
678
+ settings.customize(input, inputRect, container, maxHeight);
679
+ }
680
+ }
681
+ /**
682
+ * Redraw the autocomplete div element with suggestions
683
+ */
684
+ function update() {
685
+ container.textContent = '';
686
+ input.setAttribute('aria-activedescendant', '');
687
+ // function for rendering autocomplete suggestions
688
+ var render = function (item, _, __) {
689
+ var itemElement = doc.createElement('div');
690
+ itemElement.textContent = item.label || '';
691
+ return itemElement;
692
+ };
693
+ if (settings.render) {
694
+ render = settings.render;
695
+ }
696
+ // function to render autocomplete groups
697
+ var renderGroup = function (groupName, _) {
698
+ var groupDiv = doc.createElement('div');
699
+ groupDiv.textContent = groupName;
700
+ return groupDiv;
701
+ };
702
+ if (settings.renderGroup) {
703
+ renderGroup = settings.renderGroup;
704
+ }
705
+ var fragment = doc.createDocumentFragment();
706
+ var prevGroup = uid();
707
+ items.forEach(function (item, index) {
708
+ if (item.group && item.group !== prevGroup) {
709
+ prevGroup = item.group;
710
+ var groupDiv = renderGroup(item.group, inputValue);
711
+ if (groupDiv) {
712
+ groupDiv.className += ' group';
713
+ fragment.appendChild(groupDiv);
714
+ }
715
+ }
716
+ var div = render(item, inputValue, index);
717
+ if (div) {
718
+ div.id = container.id + "_" + index;
719
+ div.setAttribute('role', 'option');
720
+ div.addEventListener('click', function (ev) {
721
+ suppressAutocomplete = true;
722
+ try {
723
+ settings.onSelect(item, input);
724
+ }
725
+ finally {
726
+ suppressAutocomplete = false;
727
+ }
728
+ clear();
729
+ ev.preventDefault();
730
+ ev.stopPropagation();
731
+ });
732
+ if (item === selected) {
733
+ div.className += ' selected';
734
+ div.setAttribute('aria-selected', 'true');
735
+ input.setAttribute('aria-activedescendant', div.id);
736
+ }
737
+ fragment.appendChild(div);
738
+ }
739
+ });
740
+ container.appendChild(fragment);
741
+ if (items.length < 1) {
742
+ if (settings.emptyMsg) {
743
+ var empty = doc.createElement('div');
744
+ empty.id = container.id + "_" + uid();
745
+ empty.className = 'empty';
746
+ empty.textContent = settings.emptyMsg;
747
+ container.appendChild(empty);
748
+ input.setAttribute('aria-activedescendant', empty.id);
749
+ }
750
+ else {
751
+ clear();
752
+ return;
753
+ }
754
+ }
755
+ attach();
756
+ updatePosition();
757
+ updateScroll();
758
+ }
759
+ function updateIfDisplayed() {
760
+ if (containerDisplayed()) {
761
+ update();
762
+ }
763
+ }
764
+ function resizeEventHandler() {
765
+ updateIfDisplayed();
766
+ }
767
+ function scrollEventHandler(e) {
768
+ if (e.target !== container) {
769
+ updateIfDisplayed();
770
+ }
771
+ else {
772
+ e.preventDefault();
773
+ }
774
+ }
775
+ function inputEventHandler() {
776
+ if (!suppressAutocomplete) {
777
+ fetch(0 /* Keyboard */);
778
+ }
779
+ }
780
+ /**
781
+ * Automatically move scroll bar if selected item is not visible
782
+ */
783
+ function updateScroll() {
784
+ var elements = container.getElementsByClassName('selected');
785
+ if (elements.length > 0) {
786
+ var element = elements[0];
787
+ // make group visible
788
+ var previous = element.previousElementSibling;
789
+ if (previous && previous.className.indexOf('group') !== -1 && !previous.previousElementSibling) {
790
+ element = previous;
791
+ }
792
+ if (element.offsetTop < container.scrollTop) {
793
+ container.scrollTop = element.offsetTop;
794
+ }
795
+ else {
796
+ var selectBottom = element.offsetTop + element.offsetHeight;
797
+ var containerBottom = container.scrollTop + container.offsetHeight;
798
+ if (selectBottom > containerBottom) {
799
+ container.scrollTop += selectBottom - containerBottom;
800
+ }
801
+ }
802
+ }
803
+ }
804
+ function selectPreviousSuggestion() {
805
+ var index = items.indexOf(selected);
806
+ selected = index === -1
807
+ ? undefined
808
+ : items[(index + items.length - 1) % items.length];
809
+ updateSelectedSuggestion(index);
810
+ }
811
+ function selectNextSuggestion() {
812
+ var index = items.indexOf(selected);
813
+ selected = items.length < 1
814
+ ? undefined
815
+ : index === -1
816
+ ? items[0]
817
+ : items[(index + 1) % items.length];
818
+ updateSelectedSuggestion(index);
819
+ }
820
+ function updateSelectedSuggestion(index) {
821
+ if (items.length > 0) {
822
+ unselectSuggestion(index);
823
+ selectSuggestion(items.indexOf(selected));
824
+ updateScroll();
825
+ }
826
+ }
827
+ function selectSuggestion(index) {
828
+ var element = doc.getElementById(container.id + "_" + index);
829
+ if (element) {
830
+ element.classList.add('selected');
831
+ element.setAttribute('aria-selected', 'true');
832
+ input.setAttribute('aria-activedescendant', element.id);
833
+ }
834
+ }
835
+ function unselectSuggestion(index) {
836
+ var element = doc.getElementById(container.id + "_" + index);
837
+ if (element) {
838
+ element.classList.remove('selected');
839
+ element.removeAttribute('aria-selected');
840
+ input.removeAttribute('aria-activedescendant');
841
+ }
842
+ }
843
+ function handleArrowAndEscapeKeys(ev, key) {
844
+ var containerIsDisplayed = containerDisplayed();
845
+ if (key === 'Escape') {
846
+ clear();
847
+ }
848
+ else {
849
+ if (!containerIsDisplayed || items.length < 1) {
850
+ return;
851
+ }
852
+ key === 'ArrowUp'
853
+ ? selectPreviousSuggestion()
854
+ : selectNextSuggestion();
855
+ }
856
+ ev.preventDefault();
857
+ if (containerIsDisplayed) {
858
+ ev.stopPropagation();
859
+ }
860
+ }
861
+ function handleEnterKey(ev) {
862
+ if (selected) {
863
+ if (preventSubmit === 2 /* OnSelect */) {
864
+ ev.preventDefault();
865
+ }
866
+ suppressAutocomplete = true;
867
+ try {
868
+ settings.onSelect(selected, input);
869
+ }
870
+ finally {
871
+ suppressAutocomplete = false;
872
+ }
873
+ clear();
874
+ }
875
+ if (preventSubmit === 1 /* Always */) {
876
+ ev.preventDefault();
877
+ }
878
+ }
879
+ function keydownEventHandler(ev) {
880
+ var key = ev.key;
881
+ switch (key) {
882
+ case 'ArrowUp':
883
+ case 'ArrowDown':
884
+ case 'Escape':
885
+ handleArrowAndEscapeKeys(ev, key);
886
+ break;
887
+ case 'Enter':
888
+ handleEnterKey(ev);
889
+ break;
890
+ }
891
+ }
892
+ function focusEventHandler() {
893
+ if (showOnFocus) {
894
+ fetch(1 /* Focus */);
895
+ }
896
+ }
897
+ function fetch(trigger) {
898
+ if (input.value.length >= minLen || trigger === 1 /* Focus */) {
899
+ clearDebounceTimer();
900
+ debounceTimer = window.setTimeout(function () { return startFetch(input.value, trigger, input.selectionStart || 0); }, trigger === 0 /* Keyboard */ || trigger === 2 /* Mouse */ ? debounceWaitMs : 0);
901
+ }
902
+ else {
903
+ clear();
904
+ }
905
+ }
906
+ function startFetch(inputText, trigger, cursorPos) {
907
+ if (destroyed)
908
+ return;
909
+ var savedFetchCounter = ++fetchCounter;
910
+ settings.fetch(inputText, function (elements) {
911
+ if (fetchCounter === savedFetchCounter && elements) {
912
+ items = elements;
913
+ inputValue = inputText;
914
+ selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];
915
+ update();
916
+ }
917
+ }, trigger, cursorPos);
918
+ }
919
+ function keyupEventHandler(e) {
920
+ if (settings.keyup) {
921
+ settings.keyup({
922
+ event: e,
923
+ fetch: function () { return fetch(0 /* Keyboard */); }
924
+ });
925
+ return;
926
+ }
927
+ if (!containerDisplayed() && e.key === 'ArrowDown') {
928
+ fetch(0 /* Keyboard */);
929
+ }
930
+ }
931
+ function clickEventHandler(e) {
932
+ settings.click && settings.click({
933
+ event: e,
934
+ fetch: function () { return fetch(2 /* Mouse */); }
935
+ });
936
+ }
937
+ function blurEventHandler() {
938
+ // when an item is selected by mouse click, the blur event will be initiated before the click event and remove DOM elements,
939
+ // so that the click event will never be triggered. In order to avoid this issue, DOM removal should be delayed.
940
+ setTimeout(function () {
941
+ if (doc.activeElement !== input) {
942
+ clear();
943
+ }
944
+ }, 200);
945
+ }
946
+ function manualFetch() {
947
+ startFetch(input.value, 3 /* Manual */, input.selectionStart || 0);
948
+ }
949
+ /**
950
+ * Fixes #26: on long clicks focus will be lost and onSelect method will not be called
951
+ */
952
+ container.addEventListener('mousedown', function (evt) {
953
+ evt.stopPropagation();
954
+ evt.preventDefault();
955
+ });
956
+ /**
957
+ * Fixes #30: autocomplete closes when scrollbar is clicked in IE
958
+ * See: https://stackoverflow.com/a/9210267/13172349
959
+ */
960
+ container.addEventListener('focus', function () { return input.focus(); });
961
+ // If the custom autocomplete container is already appended to the DOM during widget initialization, detach it.
962
+ detach();
963
+ /**
964
+ * This function will remove DOM elements and clear event handlers
965
+ */
966
+ function destroy() {
967
+ input.removeEventListener('focus', focusEventHandler);
968
+ input.removeEventListener('keyup', keyupEventHandler);
969
+ input.removeEventListener('click', clickEventHandler);
970
+ input.removeEventListener('keydown', keydownEventHandler);
971
+ input.removeEventListener('input', inputEventHandler);
972
+ input.removeEventListener('blur', blurEventHandler);
973
+ window.removeEventListener('resize', resizeEventHandler);
974
+ doc.removeEventListener('scroll', scrollEventHandler, true);
975
+ input.removeAttribute('role');
976
+ input.removeAttribute('aria-expanded');
977
+ input.removeAttribute('aria-autocomplete');
978
+ input.removeAttribute('aria-controls');
979
+ input.removeAttribute('aria-activedescendant');
980
+ input.removeAttribute('aria-owns');
981
+ input.removeAttribute('aria-haspopup');
982
+ clearDebounceTimer();
983
+ clear();
984
+ destroyed = true;
985
+ }
986
+ // setup event handlers
987
+ input.addEventListener('keyup', keyupEventHandler);
988
+ input.addEventListener('click', clickEventHandler);
989
+ input.addEventListener('keydown', keydownEventHandler);
990
+ input.addEventListener('input', inputEventHandler);
991
+ input.addEventListener('blur', blurEventHandler);
992
+ input.addEventListener('focus', focusEventHandler);
993
+ window.addEventListener('resize', resizeEventHandler);
994
+ doc.addEventListener('scroll', scrollEventHandler, true);
995
+ return {
996
+ destroy: destroy,
997
+ fetch: manualFetch
998
+ };
999
+ }
1000
+
1001
+ class TagOption extends HTMLElement {
1002
+ constructor() {
1003
+ super();
1004
+ this._shadowRoot = this.attachShadow({ mode: "open" });
1005
+ }
1006
+
1007
+ connectedCallback() {
1008
+ this._shadowRoot.innerHTML = `
1009
+ <style>
1010
+ :host {
1011
+ background: #588a00;
1012
+ padding: 3px 10px 3px 10px !important;
1013
+ margin-right: 4px !important;
1014
+ margin-bottom: 2px !important;
1015
+ display: inline-flex;
1016
+ align-items: center;
1017
+ float: none;
1018
+ font-size: 1.25em;
1019
+ line-height: 1;
1020
+ min-height: 32px;
1021
+ color: #fff;
1022
+ text-transform: none;
1023
+ border-radius: 3px;
1024
+ position: relative;
1025
+ cursor: pointer;
1026
+ }
1027
+ button {
1028
+ z-index: 1;
1029
+ border: none;
1030
+ background: none;
1031
+ font-size: 1.4em;
1032
+ display: inline-block;
1033
+ color: rgba(255, 255, 255, 0.6);
1034
+ right: 10px;
1035
+ height: 100%;
1036
+ cursor: pointer;
1037
+ }
1038
+ </style>
1039
+ <slot></slot>
1040
+ <button type="button">×</button>
1041
+ `;
1042
+
1043
+ this.buttonTarget = this._shadowRoot.querySelector("button");
1044
+ this.buttonTarget.onclick = event => {
1045
+ this.parentNode._taggle._remove(this, event);
1046
+ };
1047
+ }
1048
+
1049
+ get value() {
1050
+ return this.getAttribute("value") || this.innerText
1051
+ }
1052
+
1053
+ get label() {
1054
+ return this.innerText
1055
+ }
1056
+ }
1057
+ customElements.define("tag-option", TagOption);
1058
+
1059
+
1060
+ class InputTag extends HTMLElement {
1061
+ static get formAssociated() {
1062
+ return true;
1063
+ }
1064
+
1065
+ static get observedAttributes() {
1066
+ return ['name', 'multiple', 'required', 'list'];
1067
+ }
1068
+
1069
+ constructor() {
1070
+ super();
1071
+ this._internals = this.attachInternals();
1072
+ this._shadowRoot = this.attachShadow({ mode: "open" });
1073
+
1074
+ this.observer = new MutationObserver(mutations => {
1075
+ let needsTagOptionsUpdate = false;
1076
+ let needsAutocompleteUpdate = false;
1077
+
1078
+ for (const mutation of mutations) {
1079
+ if (mutation.type === 'childList') {
1080
+ const addedRemovedNodes = [...mutation.addedNodes, ...mutation.removedNodes];
1081
+ if (addedRemovedNodes.some(node => node.tagName === 'TAG-OPTION')) {
1082
+ needsTagOptionsUpdate = true;
1083
+ }
1084
+ if (addedRemovedNodes.some(node => node.tagName === 'DATALIST')) {
1085
+ needsAutocompleteUpdate = true;
1086
+ }
1087
+ } else if (mutation.type === 'attributes') {
1088
+ // Handle attribute changes on tag-option elements
1089
+ if (mutation.target !== this && mutation.target.tagName === 'TAG-OPTION') {
1090
+ needsTagOptionsUpdate = true;
1091
+ }
1092
+ }
1093
+ }
1094
+
1095
+ if (needsTagOptionsUpdate || needsAutocompleteUpdate) {
1096
+ this.unobserve();
1097
+ if (needsTagOptionsUpdate) {
1098
+ this.processTagOptions();
1099
+ }
1100
+ if (needsAutocompleteUpdate && this.initialized) {
1101
+ this.setupAutocomplete();
1102
+ }
1103
+ this.observe();
1104
+ }
1105
+ });
1106
+ }
1107
+
1108
+ unobserve() {
1109
+ this.observer.disconnect();
1110
+ }
1111
+
1112
+ observe() {
1113
+ this.observer.observe(this, {
1114
+ childList: true,
1115
+ attributes: true,
1116
+ subtree: true,
1117
+ attributeFilter: ["value"],
1118
+ });
1119
+ }
1120
+
1121
+ processTagOptions() {
1122
+ if(!this._taggle || !this._taggle.tag) return
1123
+ const tagOptions = Array.from(this.children).filter(e => e.tagName === 'TAG-OPTION');
1124
+ const values = tagOptions.map(e => e.value).filter(value => value !== null && value !== undefined);
1125
+ this._taggle.tag.elements = tagOptions;
1126
+ this._taggle.tag.values = values;
1127
+ this._inputPosition = this._taggle.tag.values.length;
1128
+
1129
+ // Update the taggle display elements to match the current values
1130
+ const taggleElements = this._taggle.tag.elements;
1131
+ taggleElements.forEach((element, index) => {
1132
+ if (element && element.setAttribute) {
1133
+ element.setAttribute('data-value', values[index]);
1134
+ }
1135
+ });
1136
+
1137
+ // Update internal value to match
1138
+ this.updateValue();
1139
+ }
1140
+
1141
+ get form() {
1142
+ return this._internals.form;
1143
+ }
1144
+
1145
+ get name() {
1146
+ return this.getAttribute("name");
1147
+ }
1148
+
1149
+ get value() {
1150
+ return this._internals.value;
1151
+ }
1152
+
1153
+ set value(values) {
1154
+ const oldValues = this._internals.value;
1155
+ this._internals.value = values;
1156
+
1157
+ const formData = new FormData();
1158
+ values.forEach(value => formData.append(this.name, value));
1159
+ // For single mode, append empty string when no values (like standard HTML inputs)
1160
+ // For multiple mode, leave empty (like standard HTML multiple selects)
1161
+ if (values.length === 0 && !this.hasAttribute('multiple')) {
1162
+ formData.append(this.name, "");
1163
+ }
1164
+ this._internals.setFormValue(formData);
1165
+
1166
+ // Update taggle to match the new values
1167
+ if (this._taggle && this.initialized) {
1168
+ this.suppressEvents = true; // Prevent infinite loops
1169
+ this._taggle.removeAll();
1170
+ if (values.length > 0) {
1171
+ this._taggle.add(values);
1172
+ }
1173
+ this.suppressEvents = false;
1174
+ }
1175
+
1176
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
1177
+ this.dispatchEvent(new CustomEvent("change", {
1178
+ bubbles: true,
1179
+ composed: true,
1180
+ }));
1181
+ }
1182
+ }
1183
+
1184
+ reset() {
1185
+ this._taggle.removeAll();
1186
+ this._taggleInputTarget.value = '';
1187
+ }
1188
+
1189
+ get options() {
1190
+ const datalistId = this.getAttribute("list");
1191
+ if(datalistId) {
1192
+ const datalist = document.getElementById(datalistId);
1193
+ if(datalist) {
1194
+ return [...datalist.options].map(option => option.value).filter(value => value !== null && value !== undefined)
1195
+ }
1196
+ }
1197
+
1198
+ // Fall back to nested datalist
1199
+ const nestedDatalist = this.querySelector('datalist');
1200
+ if(nestedDatalist) {
1201
+ return [...nestedDatalist.options].map(option => option.hasAttribute('value') ? option.value : option.textContent).filter(value => value !== null && value !== undefined)
1202
+ }
1203
+
1204
+ return []
1205
+ }
1206
+
1207
+ _getOptionsWithLabels() {
1208
+ const datalistId = this.getAttribute("list");
1209
+ if(datalistId) {
1210
+ const datalist = document.getElementById(datalistId);
1211
+ if(datalist) {
1212
+ return [...datalist.options].map(option => ({
1213
+ value: option.value,
1214
+ label: option.textContent || option.value
1215
+ })).filter(item => item.value !== null && item.value !== undefined)
1216
+ }
1217
+ }
1218
+
1219
+ // Fall back to nested datalist
1220
+ const nestedDatalist = this.querySelector('datalist');
1221
+ if(nestedDatalist) {
1222
+ return [...nestedDatalist.options].map(option => ({
1223
+ value: option.hasAttribute('value') ? option.value : option.textContent,
1224
+ label: option.textContent || option.value
1225
+ })).filter(item => item.value !== null && item.value !== undefined)
1226
+ }
1227
+
1228
+ return []
1229
+ }
1230
+
1231
+ async connectedCallback() {
1232
+ this.setAttribute('tabindex', '0');
1233
+ this.addEventListener("focus", e => this.focus(e));
1234
+
1235
+ // Wait for child tag-option elements to be fully connected
1236
+ await new Promise(resolve => setTimeout(resolve, 0));
1237
+
1238
+ this._shadowRoot.innerHTML = `
1239
+ <style>
1240
+ :host { display: block; }
1241
+ :host *{
1242
+ position: relative;
1243
+ box-sizing: border-box;
1244
+ margin: 0;
1245
+ padding: 0;
1246
+ }
1247
+ #container {
1248
+ background: rgba(255, 255, 255, 0.8);
1249
+ padding: 6px 6px 3px;
1250
+ max-height: none;
1251
+ display: flex;
1252
+ margin: 0;
1253
+ flex-wrap: wrap;
1254
+ align-items: flex-start;
1255
+ min-height: 48px;
1256
+ line-height: 48px;
1257
+ width: 100%;
1258
+ border: 1px solid #d0d0d0;
1259
+ outline: 1px solid transparent;
1260
+ box-shadow: #ccc 0 1px 4px 0 inset;
1261
+ border-radius: 2px;
1262
+ cursor: text;
1263
+ color: #333;
1264
+ list-style: none;
1265
+ padding-right: 32px;
1266
+ }
1267
+ input {
1268
+ display: block;
1269
+ height: 32px;
1270
+ float: none;
1271
+ margin: 0;
1272
+ padding-left: 10px !important;
1273
+ padding-right: 30px !important;
1274
+ width: auto !important;
1275
+ min-width: 70px;
1276
+ font-size: 1.25em;
1277
+ width: 100%;
1278
+ line-height: 2;
1279
+ padding: 0 0 0 10px;
1280
+ border: 1px dashed #d0d0d0;
1281
+ outline: 1px solid transparent;
1282
+ background: #fff;
1283
+ box-shadow: none;
1284
+ border-radius: 2px;
1285
+ cursor: text;
1286
+ color: #333;
1287
+ }
1288
+ button {
1289
+ width: 30px;
1290
+ text-align: center;
1291
+ line-height: 30px;
1292
+ border: 1px solid #e0e0e0;
1293
+ font-size: 2em;
1294
+ color: #666;
1295
+ position: absolute !important;
1296
+ z-index: 10;
1297
+ right: 0px;
1298
+ top: 0;
1299
+ font-weight: 400;
1300
+ cursor: pointer;
1301
+ background: none;
1302
+ }
1303
+ .taggle_sizer{
1304
+ padding: 0;
1305
+ margin: 0;
1306
+ position: absolute;
1307
+ top: -500px;
1308
+ z-index: -1;
1309
+ visibility: hidden;
1310
+ }
1311
+ .ui-autocomplete{
1312
+ position: static !important;
1313
+ width: 100% !important;
1314
+ margin-top: 2px;
1315
+ }
1316
+ .ui-menu{
1317
+ margin: 0;
1318
+ padding: 6px;
1319
+ box-shadow: #ccc 0 1px 6px;
1320
+ z-index: 2;
1321
+ display: flex;
1322
+ flex-wrap: wrap;
1323
+ background: #fff;
1324
+ list-style: none;
1325
+ font-size: 1.25em;
1326
+ min-width: 200px;
1327
+ }
1328
+ .ui-menu .ui-menu-item{
1329
+ display: inline-block;
1330
+ margin: 0 0 2px;
1331
+ line-height: 30px;
1332
+ border: none;
1333
+ padding: 0 10px;
1334
+ text-indent: 0;
1335
+ border-radius: 2px;
1336
+ width: auto;
1337
+ cursor: pointer;
1338
+ color: #555;
1339
+ }
1340
+ .ui-menu .ui-menu-item::before{ display: none; }
1341
+ .ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
1342
+ .ui-state-active{
1343
+ padding: 0;
1344
+ border: none;
1345
+ background: none;
1346
+ color: inherit;
1347
+ }
1348
+ </style>
1349
+ <div style="position: relative;">
1350
+ <div id="container">
1351
+ <slot></slot>
1352
+ </div>
1353
+ <input
1354
+ id="inputTarget"
1355
+ type="hidden"
1356
+ name="${this.name}"
1357
+ />
1358
+ </div>
1359
+ `;
1360
+
1361
+ this.form?.addEventListener("reset", this.reset.bind(this));
1362
+
1363
+ this.containerTarget = this.shadowRoot.querySelector("#container");
1364
+ this.inputTarget = this.shadowRoot.querySelector("#inputTarget");
1365
+
1366
+ this.required = this.hasAttribute("required");
1367
+ this.multiple = this.hasAttribute("multiple");
1368
+
1369
+ const maxTags = this.multiple ? undefined : 1;
1370
+ const placeholder = this.inputTarget.getAttribute("placeholder");
1371
+
1372
+ this.inputTarget.value = "";
1373
+ this.inputTarget.id = "";
1374
+
1375
+ this._taggle = new Taggle(this, {
1376
+ inputContainer: this.containerTarget,
1377
+ preserveCase: true,
1378
+ hiddenInputName: this.name,
1379
+ maxTags: maxTags,
1380
+ placeholder: placeholder,
1381
+ onTagAdd: (event, tag) => this.onTagAdd(event, tag),
1382
+ onTagRemove: (event, tag) => this.onTagRemove(event, tag),
1383
+ });
1384
+ this._taggleInputTarget = this._taggle.getInput();
1385
+ this._taggleInputTarget.id = this.id;
1386
+ this._taggleInputTarget.autocomplete = "off";
1387
+ this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
1388
+ this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
1389
+
1390
+ // Set initial value after taggle is initialized
1391
+ this.value = this._taggle.getTagValues();
1392
+
1393
+ this.checkRequired();
1394
+
1395
+ this.buttonTarget = h(`<button class="add">+</button>`);
1396
+ this.buttonTarget.addEventListener("click", e => this._add(e));
1397
+ this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget);
1398
+
1399
+ this.autocompleteContainerTarget = h(`<ul>`);
1400
+ // Insert autocomplete container into the positioned wrapper div
1401
+ const wrapperDiv = this.shadowRoot.querySelector('div[style*="position: relative"]');
1402
+ wrapperDiv.appendChild(this.autocompleteContainerTarget);
1403
+
1404
+ this.setupAutocomplete();
1405
+
1406
+ this.observe(); // Start observing after taggle is set up
1407
+ this.initialized = true;
1408
+
1409
+ // Update visibility based on current state
1410
+ this.updateInputVisibility();
1411
+ }
1412
+
1413
+ setupAutocomplete() {
1414
+ const optionsWithLabels = this._getOptionsWithLabels();
1415
+
1416
+ autocomplete({
1417
+ input: this._taggleInputTarget,
1418
+ container: this.autocompleteContainerTarget,
1419
+ className: "ui-menu ui-autocomplete",
1420
+ fetch: (text, update) => {
1421
+ const currentTags = this._taggle.getTagValues();
1422
+ const suggestions = optionsWithLabels.filter(option =>
1423
+ option.label.toLowerCase().includes(text.toLowerCase()) &&
1424
+ !currentTags.includes(option.value)
1425
+ );
1426
+ // Store the suggestions for testing (can't assign to getter, tests read from DOM)
1427
+ update(suggestions);
1428
+ },
1429
+ render: item => h(`<li class="ui-menu-item" data-value="${item.value}">${item.label}</li>`),
1430
+ onSelect: item => {
1431
+ // Create a tag-option element with proper value/label separation
1432
+ const tagOption = document.createElement('tag-option');
1433
+ tagOption.setAttribute('value', item.value);
1434
+ tagOption.textContent = item.label;
1435
+ this.appendChild(tagOption);
1436
+
1437
+ // Clear input
1438
+ this._taggleInputTarget.value = '';
1439
+ },
1440
+ minLength: 1,
1441
+ customize: (input, inputRect, container, maxHeight) => {
1442
+ // Position autocomplete below the input-tag container, accounting for dynamic height
1443
+ this._updateAutocompletePosition(container);
1444
+
1445
+ // Store reference to update positioning when container height changes
1446
+ this._autocompleteContainer = container;
1447
+ }
1448
+ });
1449
+ }
1450
+
1451
+ disconnectedCallback() {
1452
+ this.form?.removeEventListener("reset", this.reset.bind(this));
1453
+ this.unobserve();
1454
+ }
1455
+
1456
+ attributeChangedCallback(name, oldValue, newValue) {
1457
+ if (oldValue === newValue) return;
1458
+
1459
+ // Only handle changes after the component is connected and initialized
1460
+ if (!this._taggle) return;
1461
+
1462
+ switch (name) {
1463
+ case 'name':
1464
+ this.handleNameChange(newValue);
1465
+ break;
1466
+ case 'multiple':
1467
+ this.handleMultipleChange(newValue !== null);
1468
+ break;
1469
+ case 'required':
1470
+ this.handleRequiredChange(newValue !== null);
1471
+ break;
1472
+ case 'list':
1473
+ this.handleListChange(newValue);
1474
+ break;
1475
+ }
1476
+ }
1477
+
1478
+ checkRequired() {
1479
+ const flag = this.required && this._taggle.getTagValues().length == 0;
1480
+ this._taggleInputTarget.required = flag;
1481
+
1482
+ // Update ElementInternals validity to match internal input
1483
+ if (flag) {
1484
+ this._internals.setValidity({ valueMissing: true }, 'Please fill out this field.', this._taggleInputTarget);
1485
+ } else {
1486
+ this._internals.setValidity({});
1487
+ }
1488
+ }
1489
+
1490
+ // monkeypatch support for android comma
1491
+ keyup(event) {
1492
+ const key = event.which || event.keyCode;
1493
+ const normalKeyboard = key != 229;
1494
+ if(normalKeyboard) return
1495
+ const value = this._taggleInputTarget.value;
1496
+
1497
+ // backspace
1498
+ if(value.length == 0) {
1499
+ const values = this._taggle.tag.values;
1500
+ this._taggle.remove(values[values.length - 1]);
1501
+ return
1502
+ }
1503
+
1504
+ // comma
1505
+ if(/,$/.test(value)) {
1506
+ const tag = value.replace(',', '');
1507
+ this._taggle.add(tag);
1508
+ this._taggleInputTarget.value = '';
1509
+ return
1510
+ }
1511
+ }
1512
+
1513
+ _add(event) {
1514
+ event.preventDefault();
1515
+ this._taggle.add(this._taggleInputTarget.value);
1516
+ this._taggleInputTarget.value = '';
1517
+ }
1518
+
1519
+ onTagAdd(event, tag) {
1520
+ if (!this.suppressEvents) {
1521
+ const isNew = !this.options.includes(tag);
1522
+ this.dispatchEvent(new CustomEvent("update", {
1523
+ detail: { tag, isNew },
1524
+ bubbles: true,
1525
+ composed: true,
1526
+ }));
1527
+ }
1528
+ this.syncValue();
1529
+ this.checkRequired();
1530
+ this.updateInputVisibility();
1531
+
1532
+ // Update autocomplete position if it's currently open
1533
+ if (this._autocompleteContainer) {
1534
+ // Use setTimeout to allow DOM to update first
1535
+ setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0);
1536
+ }
1537
+ }
1538
+
1539
+ onTagRemove(event, tag) {
1540
+ if (!this.suppressEvents) {
1541
+ this.dispatchEvent(new CustomEvent("update", {
1542
+ detail: { tag },
1543
+ bubbles: true,
1544
+ composed: true,
1545
+ }));
1546
+ }
1547
+ this.syncValue();
1548
+ this.checkRequired();
1549
+ this.updateInputVisibility();
1550
+
1551
+ // Update autocomplete position if it's currently open
1552
+ if (this._autocompleteContainer) {
1553
+ // Use setTimeout to allow DOM to update first
1554
+ setTimeout(() => this._updateAutocompletePosition(this._autocompleteContainer), 0);
1555
+ }
1556
+ }
1557
+
1558
+ syncValue() {
1559
+ // Directly update internals without triggering the setter
1560
+ const values = this._taggle.getTagValues();
1561
+ const oldValues = this._internals.value;
1562
+ this._internals.value = values;
1563
+
1564
+ const formData = new FormData();
1565
+ values.forEach(value => formData.append(this.name, value));
1566
+ // For single mode, append empty string when no values (like standard HTML inputs)
1567
+ // For multiple mode, leave empty (like standard HTML multiple selects)
1568
+ if (values.length === 0 && !this.hasAttribute('multiple')) {
1569
+ formData.append(this.name, "");
1570
+ }
1571
+ this._internals.setFormValue(formData);
1572
+
1573
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
1574
+ this.dispatchEvent(new CustomEvent("change", {
1575
+ bubbles: true,
1576
+ composed: true,
1577
+ }));
1578
+ }
1579
+ }
1580
+
1581
+ // Public API methods
1582
+ add(tags) {
1583
+ if (!this._taggle) return
1584
+ this._taggle.add(tags);
1585
+ }
1586
+
1587
+ remove(tag) {
1588
+ if (!this._taggle) return
1589
+ this._taggle.remove(tag);
1590
+ }
1591
+
1592
+ removeAll() {
1593
+ if (!this._taggle) return
1594
+ this._taggle.removeAll();
1595
+ }
1596
+
1597
+ has(tag) {
1598
+ if (!this._taggle) return false
1599
+ return this._taggle.getTagValues().includes(tag)
1600
+ }
1601
+
1602
+ get tags() {
1603
+ if (!this._taggle) return []
1604
+ return this._taggle.getTagValues()
1605
+ }
1606
+
1607
+ // Private getter for testing autocomplete suggestions
1608
+ get _autocompleteSuggestions() {
1609
+ if (!this.autocompleteContainerTarget) return []
1610
+ const items = this.autocompleteContainerTarget.querySelectorAll('.ui-menu-item');
1611
+ return Array.from(items).map(item => item.textContent.trim())
1612
+ }
1613
+
1614
+ // Update autocomplete position based on current container height
1615
+ _updateAutocompletePosition(container) {
1616
+ if (!container) return
1617
+
1618
+ const inputTagRect = this.containerTarget.getBoundingClientRect();
1619
+
1620
+ container.style.setProperty('position', 'absolute', 'important');
1621
+ container.style.setProperty('top', `${inputTagRect.height}px`, 'important');
1622
+ container.style.setProperty('left', '0', 'important');
1623
+ container.style.setProperty('right', '0', 'important');
1624
+ container.style.setProperty('width', '100%', 'important');
1625
+ container.style.setProperty('z-index', '1000', 'important');
1626
+ }
1627
+
1628
+ updateInputVisibility() {
1629
+ if (!this._taggleInputTarget || !this.buttonTarget) return;
1630
+
1631
+ const isMultiple = this.hasAttribute('multiple');
1632
+ const hasTags = this._taggle && this._taggle.getTagValues().length > 0;
1633
+
1634
+ if (isMultiple) {
1635
+ // Multiple mode: always show input and button
1636
+ this._taggleInputTarget.style.display = '';
1637
+ this.buttonTarget.style.display = '';
1638
+ } else {
1639
+ // Single mode: hide input and button when tag exists
1640
+ if (hasTags) {
1641
+ this._taggleInputTarget.style.display = 'none';
1642
+ this.buttonTarget.style.display = 'none';
1643
+ } else {
1644
+ this._taggleInputTarget.style.display = '';
1645
+ this.buttonTarget.style.display = '';
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ addAt(tag, index) {
1651
+ if (!this._taggle) return
1652
+ this._taggle.add(tag, index);
1653
+ }
1654
+
1655
+ disable() {
1656
+ if (this._taggle) {
1657
+ this._taggle.disable();
1658
+ }
1659
+ }
1660
+
1661
+ enable() {
1662
+ if (this._taggle) {
1663
+ this._taggle.enable();
1664
+ }
1665
+ }
1666
+
1667
+ focus() {
1668
+ if (this._taggleInputTarget) {
1669
+ this._taggleInputTarget.focus();
1670
+ }
1671
+ }
1672
+
1673
+ checkValidity() {
1674
+ if (this._taggle) {
1675
+ this.checkRequired();
1676
+ }
1677
+ return this._internals.checkValidity()
1678
+ }
1679
+
1680
+ reportValidity() {
1681
+ if (this._taggle) {
1682
+ this.checkRequired();
1683
+ }
1684
+ return this._internals.reportValidity()
1685
+ }
1686
+
1687
+ handleNameChange(newName) {
1688
+ // Update the hidden input name to match
1689
+ const hiddenInput = this._shadowRoot.querySelector('input[type="hidden"]');
1690
+ if (hiddenInput) {
1691
+ hiddenInput.name = newName || '';
1692
+ }
1693
+
1694
+ // Update the form value with the new name
1695
+ if (this._internals.value) {
1696
+ this.value = this._internals.value; // This will recreate FormData with new name
1697
+ }
1698
+ }
1699
+
1700
+ handleMultipleChange(isMultiple) {
1701
+ if (!this._taggle) return;
1702
+
1703
+ // Update the internal multiple state
1704
+ this.multiple = isMultiple;
1705
+
1706
+ // Get current tags
1707
+ const currentTags = this._taggle.getTagValues();
1708
+
1709
+ if (!isMultiple && currentTags.length > 1) {
1710
+ // Single mode: remove excess tag-option elements from DOM
1711
+ const tagOptions = Array.from(this.children);
1712
+ // Keep only the first tag-option element, remove the rest
1713
+ tagOptions.forEach((tagOption, i) => {
1714
+ if (i > 0 && tagOption) {
1715
+ this.removeChild(tagOption);
1716
+ }
1717
+ });
1718
+ }
1719
+
1720
+ // Reinitialize taggle with new multiple setting
1721
+ this.reinitializeTaggle();
1722
+
1723
+ // Restore tags, respecting the new multiple constraint
1724
+ if (isMultiple) {
1725
+ // Multiple mode: restore all remaining tags
1726
+ if (currentTags.length > 0) {
1727
+ this._taggle.add(currentTags);
1728
+ }
1729
+ } else {
1730
+ // Single mode: keep only the first tag
1731
+ if (currentTags.length > 0) {
1732
+ this._taggle.add(currentTags[0]);
1733
+ }
1734
+ }
1735
+
1736
+ this.updateValue();
1737
+ this.updateInputVisibility();
1738
+ }
1739
+
1740
+ handleRequiredChange(isRequired) {
1741
+ if (!this._taggle) return;
1742
+
1743
+ // Update the internal required state
1744
+ this.required = isRequired;
1745
+
1746
+ // Update validation
1747
+ this.checkRequired();
1748
+ }
1749
+
1750
+ handleListChange(newListId) {
1751
+ if (!this._taggle) return;
1752
+
1753
+ // Re-setup autocomplete with new datalist
1754
+ this.setupAutocomplete();
1755
+ }
1756
+
1757
+ reinitializeTaggle() {
1758
+ // Clean up existing taggle if it exists
1759
+ if (this._taggle && this._taggle.destroy) {
1760
+ this._taggle.destroy();
1761
+ }
1762
+
1763
+ // Get current configuration
1764
+ const maxTags = this.hasAttribute("multiple") ? undefined : 1;
1765
+ const placeholder = this.getAttribute("placeholder") || "";
1766
+
1767
+ // Create new taggle instance using original configuration pattern
1768
+ this._taggle = new Taggle(this, {
1769
+ inputContainer: this.containerTarget,
1770
+ preserveCase: true,
1771
+ hiddenInputName: this.name,
1772
+ maxTags: maxTags,
1773
+ placeholder: placeholder,
1774
+ onTagAdd: (event, tag) => this.onTagAdd(event, tag),
1775
+ onTagRemove: (event, tag) => this.onTagRemove(event, tag),
1776
+ });
1777
+
1778
+ // Re-get references since taggle was recreated
1779
+ this._taggleInputTarget = this._taggle.getInput();
1780
+ this._taggleInputTarget.id = this.id || "";
1781
+ this._taggleInputTarget.autocomplete = "off";
1782
+ this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
1783
+ this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
1784
+
1785
+ // Re-setup autocomplete
1786
+ this.setupAutocomplete();
1787
+
1788
+ // Re-process existing tag options
1789
+ this.processTagOptions();
1790
+ }
1791
+
1792
+ updateValue() {
1793
+ if (!this._taggle) return;
1794
+
1795
+ // Update the internal value to match taggle state
1796
+ const values = this._taggle.getTagValues();
1797
+ const oldValues = this._internals.value;
1798
+ this._internals.value = values;
1799
+
1800
+ const formData = new FormData();
1801
+ values.forEach(value => formData.append(this.name, value));
1802
+ // For single mode, append empty string when no values (like standard HTML inputs)
1803
+ // For multiple mode, leave empty (like standard HTML multiple selects)
1804
+ if (values.length === 0 && !this.hasAttribute('multiple')) {
1805
+ formData.append(this.name, "");
1806
+ }
1807
+ this._internals.setFormValue(formData);
1808
+
1809
+ // Check validity after updating
1810
+ this.checkRequired();
1811
+
1812
+ if(this.initialized && !this.suppressEvents && JSON.stringify(oldValues) !== JSON.stringify(values)) {
1813
+ this.dispatchEvent(new CustomEvent("change", {
1814
+ bubbles: true,
1815
+ composed: true,
1816
+ }));
1817
+ }
1818
+ }
1819
+ }
1820
+ customElements.define("input-tag", InputTag);
1821
+
1822
+
1823
+ function h(html) {
1824
+ const container = document.createElement("div");
1825
+ container.innerHTML = html;
1826
+ return container.firstElementChild
1827
+ }