bard-tag_field 0.4.1 → 0.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.
- checksums.yaml +4 -4
- data/Appraisals +5 -1
- data/CLAUDE.md +109 -0
- data/README.md +16 -16
- data/Rakefile +3 -3
- data/app/assets/javascripts/input-tag.js +482 -465
- data/gemfiles/rails_8.1.gemfile +7 -0
- data/input-tag/bun.lockb +0 -0
- data/input-tag/package.json +16 -0
- data/lib/bard/tag_field/form_builder.rb +4 -4
- data/lib/bard/tag_field/version.rb +1 -1
- metadata +9 -7
- data/bard-tag/bun.lockb +0 -0
- data/bard-tag/package.json +0 -15
- /data/{bard-tag → input-tag}/.gitignore +0 -0
- /data/{bard-tag → input-tag}/index.js +0 -0
- /data/{bard-tag → input-tag}/rollup.config.js +0 -0
|
@@ -543,459 +543,459 @@ class Taggle {
|
|
|
543
543
|
}
|
|
544
544
|
}
|
|
545
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
|
-
};
|
|
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
999
|
}
|
|
1000
1000
|
|
|
1001
1001
|
class TagOption extends HTMLElement {
|
|
@@ -1120,8 +1120,17 @@ class InputTag extends HTMLElement {
|
|
|
1120
1120
|
|
|
1121
1121
|
processTagOptions() {
|
|
1122
1122
|
if(!this._taggle || !this._taggle.tag) return
|
|
1123
|
-
|
|
1124
|
-
|
|
1123
|
+
let tagOptions = Array.from(this.children).filter(e => e.tagName === 'TAG-OPTION');
|
|
1124
|
+
let values = tagOptions.map(e => e.value).filter(value => value !== null && value !== undefined);
|
|
1125
|
+
|
|
1126
|
+
// Enforce maxTags constraint for single mode
|
|
1127
|
+
if (!this.multiple && values.length > 1) {
|
|
1128
|
+
// Remove excess tag-options from DOM (keep only the first one)
|
|
1129
|
+
tagOptions.slice(1).forEach(el => el.remove());
|
|
1130
|
+
tagOptions = tagOptions.slice(0, 1);
|
|
1131
|
+
values = values.slice(0, 1);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1125
1134
|
this._taggle.tag.elements = tagOptions;
|
|
1126
1135
|
this._taggle.tag.values = values;
|
|
1127
1136
|
this._inputPosition = this._taggle.tag.values.length;
|
|
@@ -1136,6 +1145,9 @@ class InputTag extends HTMLElement {
|
|
|
1136
1145
|
|
|
1137
1146
|
// Update internal value to match
|
|
1138
1147
|
this.updateValue();
|
|
1148
|
+
|
|
1149
|
+
// Ensure input visibility is updated when tags change via DOM
|
|
1150
|
+
this.updateInputVisibility();
|
|
1139
1151
|
}
|
|
1140
1152
|
|
|
1141
1153
|
get form() {
|
|
@@ -1146,9 +1158,13 @@ class InputTag extends HTMLElement {
|
|
|
1146
1158
|
return this.getAttribute("name");
|
|
1147
1159
|
}
|
|
1148
1160
|
|
|
1161
|
+
get multiple() {
|
|
1162
|
+
return this.hasAttribute('multiple');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1149
1165
|
get value() {
|
|
1150
1166
|
const internalValue = this._internals.value;
|
|
1151
|
-
if (this.
|
|
1167
|
+
if (this.multiple) {
|
|
1152
1168
|
return internalValue; // Return array for multiple mode
|
|
1153
1169
|
} else {
|
|
1154
1170
|
return internalValue.length > 0 ? internalValue[0] : ''; // Return string for single mode
|
|
@@ -1173,7 +1189,7 @@ class InputTag extends HTMLElement {
|
|
|
1173
1189
|
values.forEach(value => formData.append(this.name, value));
|
|
1174
1190
|
// For single mode, append empty string when no values (like standard HTML inputs)
|
|
1175
1191
|
// For multiple mode, leave empty (like standard HTML multiple selects)
|
|
1176
|
-
if (values.length === 0 && !this.
|
|
1192
|
+
if (values.length === 0 && !this.multiple) {
|
|
1177
1193
|
formData.append(this.name, "");
|
|
1178
1194
|
}
|
|
1179
1195
|
this._internals.setFormValue(formData);
|
|
@@ -1379,7 +1395,6 @@ class InputTag extends HTMLElement {
|
|
|
1379
1395
|
this.inputTarget = this.shadowRoot.querySelector("#inputTarget");
|
|
1380
1396
|
|
|
1381
1397
|
this.required = this.hasAttribute("required");
|
|
1382
|
-
this.multiple = this.hasAttribute("multiple");
|
|
1383
1398
|
|
|
1384
1399
|
const maxTags = this.multiple ? undefined : 1;
|
|
1385
1400
|
const placeholder = this.inputTarget.getAttribute("placeholder");
|
|
@@ -1443,6 +1458,12 @@ class InputTag extends HTMLElement {
|
|
|
1443
1458
|
},
|
|
1444
1459
|
render: item => h(`<li class="ui-menu-item" data-value="${item.value}">${item.label}</li>`),
|
|
1445
1460
|
onSelect: item => {
|
|
1461
|
+
// Prevent adding multiple tags in single mode
|
|
1462
|
+
if (!this.multiple && this._taggle.getTagValues().length > 0) {
|
|
1463
|
+
this._taggleInputTarget.value = '';
|
|
1464
|
+
return
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1446
1467
|
// Create a tag-option element with proper value/label separation
|
|
1447
1468
|
const tagOption = document.createElement('tag-option');
|
|
1448
1469
|
tagOption.setAttribute('value', item.value);
|
|
@@ -1580,7 +1601,7 @@ class InputTag extends HTMLElement {
|
|
|
1580
1601
|
values.forEach(value => formData.append(this.name, value));
|
|
1581
1602
|
// For single mode, append empty string when no values (like standard HTML inputs)
|
|
1582
1603
|
// For multiple mode, leave empty (like standard HTML multiple selects)
|
|
1583
|
-
if (values.length === 0 && !this.
|
|
1604
|
+
if (values.length === 0 && !this.multiple) {
|
|
1584
1605
|
formData.append(this.name, "");
|
|
1585
1606
|
}
|
|
1586
1607
|
this._internals.setFormValue(formData);
|
|
@@ -1643,10 +1664,9 @@ class InputTag extends HTMLElement {
|
|
|
1643
1664
|
updateInputVisibility() {
|
|
1644
1665
|
if (!this._taggleInputTarget || !this.buttonTarget) return;
|
|
1645
1666
|
|
|
1646
|
-
const isMultiple = this.hasAttribute('multiple');
|
|
1647
1667
|
const hasTags = this._taggle && this._taggle.getTagValues().length > 0;
|
|
1648
1668
|
|
|
1649
|
-
if (
|
|
1669
|
+
if (this.multiple) {
|
|
1650
1670
|
// Multiple mode: always show input and button
|
|
1651
1671
|
this._taggleInputTarget.style.display = '';
|
|
1652
1672
|
this.buttonTarget.style.display = '';
|
|
@@ -1715,9 +1735,6 @@ class InputTag extends HTMLElement {
|
|
|
1715
1735
|
handleMultipleChange(isMultiple) {
|
|
1716
1736
|
if (!this._taggle) return;
|
|
1717
1737
|
|
|
1718
|
-
// Update the internal multiple state
|
|
1719
|
-
this.multiple = isMultiple;
|
|
1720
|
-
|
|
1721
1738
|
// Get current tags
|
|
1722
1739
|
const currentTags = this._taggle.getTagValues();
|
|
1723
1740
|
|
|
@@ -1816,7 +1833,7 @@ class InputTag extends HTMLElement {
|
|
|
1816
1833
|
values.forEach(value => formData.append(this.name, value));
|
|
1817
1834
|
// For single mode, append empty string when no values (like standard HTML inputs)
|
|
1818
1835
|
// For multiple mode, leave empty (like standard HTML multiple selects)
|
|
1819
|
-
if (values.length === 0 && !this.
|
|
1836
|
+
if (values.length === 0 && !this.multiple) {
|
|
1820
1837
|
formData.append(this.name, "");
|
|
1821
1838
|
}
|
|
1822
1839
|
this._internals.setFormValue(formData);
|