hotwire_combobox 0.1.43 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +25 -3
- data/app/assets/javascripts/hotwire_combobox.esm.js +438 -104
- data/app/assets/javascripts/hotwire_combobox.umd.js +438 -104
- data/app/assets/javascripts/hw_combobox/helpers.js +16 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/announcements.js +7 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +1 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/events.js +21 -11
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +20 -12
- data/app/assets/javascripts/hw_combobox/models/combobox/form_field.js +74 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +160 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/navigation.js +15 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/options.js +19 -7
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +50 -49
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +33 -16
- data/app/assets/javascripts/hw_combobox/models/combobox/validity.js +1 -1
- data/app/assets/javascripts/hw_combobox/models/combobox.js +3 -0
- data/app/assets/stylesheets/hotwire_combobox.css +84 -18
- data/app/presenters/hotwire_combobox/component/customizable.rb +9 -1
- data/app/presenters/hotwire_combobox/component.rb +93 -26
- data/app/presenters/hotwire_combobox/listbox/group.rb +45 -0
- data/app/presenters/hotwire_combobox/listbox/item.rb +104 -0
- data/app/presenters/hotwire_combobox/listbox/option.rb +9 -4
- data/app/views/hotwire_combobox/_component.html.erb +1 -0
- data/app/views/hotwire_combobox/_selection_chip.turbo_stream.erb +8 -0
- data/app/views/hotwire_combobox/layouts/_selection_chip.turbo_stream.erb +7 -0
- data/lib/hotwire_combobox/helper.rb +111 -86
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +9 -2
@@ -1,3 +1,6 @@
|
|
1
|
+
/*!
|
2
|
+
HotwireCombobox 0.2.0
|
3
|
+
*/
|
1
4
|
(function (global, factory) {
|
2
5
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@hotwired/stimulus')) :
|
3
6
|
typeof define === 'function' && define.amd ? define(['@hotwired/stimulus'], factory) :
|
@@ -37,6 +40,12 @@
|
|
37
40
|
}
|
38
41
|
};
|
39
42
|
|
43
|
+
Combobox.Announcements = Base => class extends Base {
|
44
|
+
_announceToScreenReader(display, action) {
|
45
|
+
this.announcerTarget.innerText = `${display} ${action}`;
|
46
|
+
}
|
47
|
+
};
|
48
|
+
|
40
49
|
Combobox.AsyncLoading = Base => class extends Base {
|
41
50
|
get _isAsync() {
|
42
51
|
return this.hasAsyncSrcValue
|
@@ -129,6 +138,22 @@
|
|
129
138
|
return event
|
130
139
|
}
|
131
140
|
|
141
|
+
function nextRepaint() {
|
142
|
+
if (document.visibilityState === "hidden") {
|
143
|
+
return nextEventLoopTick()
|
144
|
+
} else {
|
145
|
+
return nextAnimationFrame()
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
function nextAnimationFrame() {
|
150
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()))
|
151
|
+
}
|
152
|
+
|
153
|
+
function nextEventLoopTick() {
|
154
|
+
return new Promise((resolve) => setTimeout(() => resolve(), 0))
|
155
|
+
}
|
156
|
+
|
132
157
|
Combobox.Autocomplete = Base => class extends Base {
|
133
158
|
_connectListAutocomplete() {
|
134
159
|
if (!this._autocompletesList) {
|
@@ -236,7 +261,7 @@
|
|
236
261
|
this.dialogFocusTrapTarget.focus();
|
237
262
|
}
|
238
263
|
|
239
|
-
get
|
264
|
+
get _isSmallViewport() {
|
240
265
|
return window.matchMedia(`(max-width: ${this.smallViewportMaxWidthValue})`).matches
|
241
266
|
}
|
242
267
|
|
@@ -246,25 +271,35 @@
|
|
246
271
|
};
|
247
272
|
|
248
273
|
Combobox.Events = Base => class extends Base {
|
249
|
-
|
250
|
-
if (previousValue
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
}
|
255
|
-
}
|
274
|
+
_dispatchPreselectionEvent({ isNewAndAllowed, previousValue }) {
|
275
|
+
if (previousValue === this._incomingFieldValueString) return
|
276
|
+
|
277
|
+
dispatch("hw-combobox:preselection", {
|
278
|
+
target: this.element,
|
279
|
+
detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
|
280
|
+
});
|
281
|
+
}
|
282
|
+
|
283
|
+
_dispatchSelectionEvent() {
|
284
|
+
dispatch("hw-combobox:selection", {
|
285
|
+
target: this.element,
|
286
|
+
detail: this._eventableDetails
|
287
|
+
});
|
256
288
|
}
|
257
289
|
|
258
|
-
|
259
|
-
dispatch("hw-combobox:
|
290
|
+
_dispatchRemovalEvent({ removedDisplay, removedValue }) {
|
291
|
+
dispatch("hw-combobox:removal", {
|
292
|
+
target: this.element,
|
293
|
+
detail: { ...this._eventableDetails, removedDisplay, removedValue }
|
294
|
+
});
|
260
295
|
}
|
261
296
|
|
262
297
|
get _eventableDetails() {
|
263
298
|
return {
|
264
|
-
value: this.
|
299
|
+
value: this._incomingFieldValueString,
|
265
300
|
display: this._fullQuery,
|
266
301
|
query: this._typedQuery,
|
267
|
-
fieldName: this.
|
302
|
+
fieldName: this._fieldName,
|
268
303
|
isValid: this._valueIsValid
|
269
304
|
}
|
270
305
|
}
|
@@ -548,11 +583,11 @@
|
|
548
583
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
549
584
|
|
550
585
|
Combobox.Filtering = Base => class extends Base {
|
551
|
-
filterAndSelect(
|
552
|
-
this._filter(
|
586
|
+
filterAndSelect({ inputType }) {
|
587
|
+
this._filter(inputType);
|
553
588
|
|
554
589
|
if (this._isSync) {
|
555
|
-
this.
|
590
|
+
this._selectOnQuery(inputType);
|
556
591
|
}
|
557
592
|
}
|
558
593
|
|
@@ -560,24 +595,24 @@
|
|
560
595
|
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
561
596
|
}
|
562
597
|
|
563
|
-
_filter(
|
598
|
+
_filter(inputType) {
|
564
599
|
if (this._isAsync) {
|
565
|
-
this._debouncedFilterAsync(
|
600
|
+
this._debouncedFilterAsync(inputType);
|
566
601
|
} else {
|
567
602
|
this._filterSync();
|
568
603
|
}
|
569
604
|
|
570
|
-
this.
|
605
|
+
this._markQueried();
|
571
606
|
}
|
572
607
|
|
573
|
-
_debouncedFilterAsync(
|
574
|
-
this._filterAsync(
|
608
|
+
_debouncedFilterAsync(inputType) {
|
609
|
+
this._filterAsync(inputType);
|
575
610
|
}
|
576
611
|
|
577
|
-
async _filterAsync(
|
612
|
+
async _filterAsync(inputType) {
|
578
613
|
const query = {
|
579
614
|
q: this._fullQuery,
|
580
|
-
input_type:
|
615
|
+
input_type: inputType,
|
581
616
|
for_id: this.element.dataset.asyncId
|
582
617
|
};
|
583
618
|
|
@@ -585,8 +620,12 @@
|
|
585
620
|
}
|
586
621
|
|
587
622
|
_filterSync() {
|
588
|
-
this.
|
589
|
-
|
623
|
+
this._allFilterableOptionElements.forEach(
|
624
|
+
applyFilter(
|
625
|
+
this._fullQuery,
|
626
|
+
{ matching: this.filterableAttributeValue }
|
627
|
+
)
|
628
|
+
);
|
590
629
|
}
|
591
630
|
|
592
631
|
_clearQuery() {
|
@@ -594,6 +633,10 @@
|
|
594
633
|
this.filterAndSelect({ inputType: "deleteContentBackward" });
|
595
634
|
}
|
596
635
|
|
636
|
+
_markQueried() {
|
637
|
+
this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
|
638
|
+
}
|
639
|
+
|
597
640
|
get _isQueried() {
|
598
641
|
return this._fullQuery.length > 0
|
599
642
|
}
|
@@ -611,20 +654,255 @@
|
|
611
654
|
}
|
612
655
|
};
|
613
656
|
|
657
|
+
Combobox.FormField = Base => class extends Base {
|
658
|
+
get _fieldValue() {
|
659
|
+
if (this._isMultiselect) {
|
660
|
+
const currentValue = this.hiddenFieldTarget.value;
|
661
|
+
const arrayFromValue = currentValue ? currentValue.split(",") : [];
|
662
|
+
|
663
|
+
return new Set(arrayFromValue)
|
664
|
+
} else {
|
665
|
+
return this.hiddenFieldTarget.value
|
666
|
+
}
|
667
|
+
}
|
668
|
+
|
669
|
+
get _fieldValueString() {
|
670
|
+
if (this._isMultiselect) {
|
671
|
+
return this._fieldValueArray.join(",")
|
672
|
+
} else {
|
673
|
+
return this.hiddenFieldTarget.value
|
674
|
+
}
|
675
|
+
}
|
676
|
+
|
677
|
+
get _incomingFieldValueString() {
|
678
|
+
if (this._isMultiselect) {
|
679
|
+
const array = this._fieldValueArray;
|
680
|
+
|
681
|
+
if (this.hiddenFieldTarget.dataset.valueForMultiselect) {
|
682
|
+
array.push(this.hiddenFieldTarget.dataset.valueForMultiselect);
|
683
|
+
}
|
684
|
+
|
685
|
+
return array.join(",")
|
686
|
+
} else {
|
687
|
+
return this.hiddenFieldTarget.value
|
688
|
+
}
|
689
|
+
}
|
690
|
+
|
691
|
+
get _fieldValueArray() {
|
692
|
+
if (this._isMultiselect) {
|
693
|
+
return Array.from(this._fieldValue)
|
694
|
+
} else {
|
695
|
+
return [ this.hiddenFieldTarget.value ]
|
696
|
+
}
|
697
|
+
}
|
698
|
+
|
699
|
+
set _fieldValue(value) {
|
700
|
+
if (this._isMultiselect) {
|
701
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect = value?.replace(/,/g, "");
|
702
|
+
this.hiddenFieldTarget.dataset.displayForMultiselect = this._fullQuery;
|
703
|
+
} else {
|
704
|
+
this.hiddenFieldTarget.value = value;
|
705
|
+
}
|
706
|
+
}
|
707
|
+
|
708
|
+
get _hasEmptyFieldValue() {
|
709
|
+
if (this._isMultiselect) {
|
710
|
+
return this.hiddenFieldTarget.dataset.valueForMultiselect == "" ||
|
711
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect == "undefined"
|
712
|
+
} else {
|
713
|
+
return this.hiddenFieldTarget.value === ""
|
714
|
+
}
|
715
|
+
}
|
716
|
+
|
717
|
+
get _hasFieldValue() {
|
718
|
+
return !this._hasEmptyFieldValue
|
719
|
+
}
|
720
|
+
|
721
|
+
get _fieldName() {
|
722
|
+
return this.hiddenFieldTarget.name
|
723
|
+
}
|
724
|
+
|
725
|
+
set _fieldName(value) {
|
726
|
+
this.hiddenFieldTarget.name = value;
|
727
|
+
}
|
728
|
+
};
|
729
|
+
|
730
|
+
Combobox.Multiselect = Base => class extends Base {
|
731
|
+
navigateChip(event) {
|
732
|
+
this._chipKeyHandlers[event.key]?.call(this, event);
|
733
|
+
}
|
734
|
+
|
735
|
+
removeChip({ currentTarget, params }) {
|
736
|
+
let display;
|
737
|
+
const option = this._optionElementWithValue(params.value);
|
738
|
+
|
739
|
+
if (option) {
|
740
|
+
display = option.getAttribute(this.autocompletableAttributeValue);
|
741
|
+
this._markNotSelected(option);
|
742
|
+
this._markNotMultiselected(option);
|
743
|
+
} else {
|
744
|
+
display = params.value; // for new options
|
745
|
+
}
|
746
|
+
|
747
|
+
this._removeFromFieldValue(params.value);
|
748
|
+
this._filter("hw:multiselectSync");
|
749
|
+
|
750
|
+
currentTarget.closest("[data-hw-combobox-chip]").remove();
|
751
|
+
|
752
|
+
if (!this._isSmallViewport) {
|
753
|
+
this.openByFocusing();
|
754
|
+
}
|
755
|
+
|
756
|
+
this._announceToScreenReader(display, "removed");
|
757
|
+
this._dispatchRemovalEvent({ removedDisplay: display, removedValue: params.value });
|
758
|
+
}
|
759
|
+
|
760
|
+
hideChipsForCache() {
|
761
|
+
this.element.querySelectorAll("[data-hw-combobox-chip]").forEach(chip => chip.hidden = true);
|
762
|
+
}
|
763
|
+
|
764
|
+
_chipKeyHandlers = {
|
765
|
+
Backspace: (event) => {
|
766
|
+
this.removeChip(event);
|
767
|
+
cancel(event);
|
768
|
+
},
|
769
|
+
Enter: (event) => {
|
770
|
+
this.removeChip(event);
|
771
|
+
cancel(event);
|
772
|
+
},
|
773
|
+
Space: (event) => {
|
774
|
+
this.removeChip(event);
|
775
|
+
cancel(event);
|
776
|
+
},
|
777
|
+
Escape: (event) => {
|
778
|
+
this.openByFocusing();
|
779
|
+
cancel(event);
|
780
|
+
}
|
781
|
+
}
|
782
|
+
|
783
|
+
_initializeMultiselect() {
|
784
|
+
if (!this._isMultiPreselected) {
|
785
|
+
this._preselectMultiple();
|
786
|
+
this._markMultiPreselected();
|
787
|
+
}
|
788
|
+
}
|
789
|
+
|
790
|
+
async _createChip(shouldReopen) {
|
791
|
+
if (!this._isMultiselect) return
|
792
|
+
|
793
|
+
this._beforeClearingMultiselectQuery(async (display, value) => {
|
794
|
+
this._fullQuery = "";
|
795
|
+
this._filter("hw:multiselectSync");
|
796
|
+
this._requestChips(value);
|
797
|
+
this._addToFieldValue(value);
|
798
|
+
if (shouldReopen) {
|
799
|
+
await nextRepaint();
|
800
|
+
this.openByFocusing();
|
801
|
+
}
|
802
|
+
this._announceToScreenReader(display, "multi-selected. Press Shift + Tab, then Enter to remove.");
|
803
|
+
});
|
804
|
+
}
|
805
|
+
|
806
|
+
async _requestChips(values) {
|
807
|
+
await get(this.selectionChipSrcValue, {
|
808
|
+
responseKind: "turbo-stream",
|
809
|
+
query: {
|
810
|
+
for_id: this.element.dataset.asyncId,
|
811
|
+
combobox_values: values
|
812
|
+
}
|
813
|
+
});
|
814
|
+
}
|
815
|
+
|
816
|
+
_beforeClearingMultiselectQuery(callback) {
|
817
|
+
const display = this.hiddenFieldTarget.dataset.displayForMultiselect;
|
818
|
+
const value = this.hiddenFieldTarget.dataset.valueForMultiselect;
|
819
|
+
|
820
|
+
if (value && !this._fieldValue.has(value)) {
|
821
|
+
callback(display, value);
|
822
|
+
}
|
823
|
+
|
824
|
+
this.hiddenFieldTarget.dataset.displayForMultiselect = "";
|
825
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect = "";
|
826
|
+
}
|
827
|
+
|
828
|
+
_resetMultiselectionMarks() {
|
829
|
+
if (!this._isMultiselect) return
|
830
|
+
|
831
|
+
this._fieldValueArray.forEach(value => {
|
832
|
+
const option = this._optionElementWithValue(value);
|
833
|
+
|
834
|
+
if (option) {
|
835
|
+
option.setAttribute("data-multiselected", "");
|
836
|
+
option.hidden = true;
|
837
|
+
}
|
838
|
+
});
|
839
|
+
}
|
840
|
+
|
841
|
+
_markNotMultiselected(option) {
|
842
|
+
if (!this._isMultiselect) return
|
843
|
+
|
844
|
+
option.removeAttribute("data-multiselected");
|
845
|
+
option.hidden = false;
|
846
|
+
}
|
847
|
+
|
848
|
+
_addToFieldValue(value) {
|
849
|
+
const newValue = this._fieldValue;
|
850
|
+
|
851
|
+
newValue.add(String(value));
|
852
|
+
this.hiddenFieldTarget.value = Array.from(newValue).join(",");
|
853
|
+
|
854
|
+
if (this._isSync) this._resetMultiselectionMarks();
|
855
|
+
}
|
856
|
+
|
857
|
+
_removeFromFieldValue(value) {
|
858
|
+
const newValue = this._fieldValue;
|
859
|
+
|
860
|
+
newValue.delete(String(value));
|
861
|
+
this.hiddenFieldTarget.value = Array.from(newValue).join(",");
|
862
|
+
|
863
|
+
if (this._isSync) this._resetMultiselectionMarks();
|
864
|
+
}
|
865
|
+
|
866
|
+
_focusLastChipDismisser() {
|
867
|
+
this.chipDismisserTargets[this.chipDismisserTargets.length - 1]?.focus();
|
868
|
+
}
|
869
|
+
|
870
|
+
_markMultiPreselected() {
|
871
|
+
this.element.dataset.multiPreselected = "";
|
872
|
+
}
|
873
|
+
|
874
|
+
get _isMultiselect() {
|
875
|
+
return this.hasSelectionChipSrcValue
|
876
|
+
}
|
877
|
+
|
878
|
+
get _isSingleSelect() {
|
879
|
+
return !this._isMultiselect
|
880
|
+
}
|
881
|
+
|
882
|
+
get _isMultiPreselected() {
|
883
|
+
return this.element.hasAttribute("data-multi-preselected")
|
884
|
+
}
|
885
|
+
};
|
886
|
+
|
614
887
|
Combobox.Navigation = Base => class extends Base {
|
615
888
|
navigate(event) {
|
616
889
|
if (this._autocompletesList) {
|
617
|
-
this.
|
890
|
+
this._navigationKeyHandlers[event.key]?.call(this, event);
|
618
891
|
}
|
619
892
|
}
|
620
893
|
|
621
|
-
|
894
|
+
_navigationKeyHandlers = {
|
622
895
|
ArrowUp: (event) => {
|
623
896
|
this._selectIndex(this._selectedOptionIndex - 1);
|
624
897
|
cancel(event);
|
625
898
|
},
|
626
899
|
ArrowDown: (event) => {
|
627
900
|
this._selectIndex(this._selectedOptionIndex + 1);
|
901
|
+
|
902
|
+
if (this._selectedOptionIndex === 0) {
|
903
|
+
this._actingListbox.scrollTop = 0;
|
904
|
+
}
|
905
|
+
|
628
906
|
cancel(event);
|
629
907
|
},
|
630
908
|
Home: (event) => {
|
@@ -636,14 +914,18 @@
|
|
636
914
|
cancel(event);
|
637
915
|
},
|
638
916
|
Enter: (event) => {
|
639
|
-
this.
|
640
|
-
this._actingCombobox.blur();
|
917
|
+
this._closeAndBlur("hw:keyHandler:enter");
|
641
918
|
cancel(event);
|
642
919
|
},
|
643
920
|
Escape: (event) => {
|
644
|
-
this.
|
645
|
-
this._actingCombobox.blur();
|
921
|
+
this._closeAndBlur("hw:keyHandler:escape");
|
646
922
|
cancel(event);
|
923
|
+
},
|
924
|
+
Backspace: (event) => {
|
925
|
+
if (this._isMultiselect && !this._fullQuery) {
|
926
|
+
this._focusLastChipDismisser();
|
927
|
+
cancel(event);
|
928
|
+
}
|
647
929
|
}
|
648
930
|
}
|
649
931
|
};
|
@@ -694,28 +976,40 @@
|
|
694
976
|
}
|
695
977
|
|
696
978
|
_resetOptions(deselectionStrategy) {
|
697
|
-
this.
|
979
|
+
this._fieldName = this.originalNameValue;
|
698
980
|
deselectionStrategy();
|
699
981
|
}
|
700
982
|
|
983
|
+
_optionElementWithValue(value) {
|
984
|
+
return this._actingListbox.querySelector(`[${this.filterableAttributeValue}][data-value='${value}']`)
|
985
|
+
}
|
986
|
+
|
987
|
+
_displayForOptionElement(element) {
|
988
|
+
return element.getAttribute(this.autocompletableAttributeValue)
|
989
|
+
}
|
990
|
+
|
701
991
|
get _allowNew() {
|
702
992
|
return !!this.nameWhenNewValue
|
703
993
|
}
|
704
994
|
|
705
995
|
get _allOptions() {
|
706
|
-
return Array.from(this.
|
996
|
+
return Array.from(this._allFilterableOptionElements)
|
707
997
|
}
|
708
998
|
|
709
|
-
get
|
710
|
-
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]`)
|
999
|
+
get _allFilterableOptionElements() {
|
1000
|
+
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]:not([data-multiselected])`)
|
711
1001
|
}
|
712
1002
|
|
713
1003
|
get _visibleOptionElements() {
|
714
|
-
return [ ...this.
|
1004
|
+
return [ ...this._allFilterableOptionElements ].filter(visible)
|
715
1005
|
}
|
716
1006
|
|
717
1007
|
get _selectedOptionElement() {
|
718
|
-
return this._actingListbox.querySelector("[role=option][aria-selected=true]")
|
1008
|
+
return this._actingListbox.querySelector("[role=option][aria-selected=true]:not([data-multiselected])")
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
get _multiselectedOptionElements() {
|
1012
|
+
return this._actingListbox.querySelectorAll("[role=option][data-multiselected]")
|
719
1013
|
}
|
720
1014
|
|
721
1015
|
get _selectedOptionIndex() {
|
@@ -723,7 +1017,7 @@
|
|
723
1017
|
}
|
724
1018
|
|
725
1019
|
get _isUnjustifiablyBlank() {
|
726
|
-
const valueIsMissing =
|
1020
|
+
const valueIsMissing = this._hasEmptyFieldValue;
|
727
1021
|
const noBlankOptionSelected = !this._selectedOptionElement;
|
728
1022
|
|
729
1023
|
return valueIsMissing && noBlankOptionSelected
|
@@ -731,23 +1025,24 @@
|
|
731
1025
|
};
|
732
1026
|
|
733
1027
|
Combobox.Selection = Base => class extends Base {
|
734
|
-
|
735
|
-
this._forceSelectionAndFilter(
|
736
|
-
this.
|
1028
|
+
selectOnClick({ currentTarget, inputType }) {
|
1029
|
+
this._forceSelectionAndFilter(currentTarget, inputType);
|
1030
|
+
this._closeAndBlur("hw:optionRoleClick");
|
737
1031
|
}
|
738
1032
|
|
739
1033
|
_connectSelection() {
|
740
1034
|
if (this.hasPrefilledDisplayValue) {
|
741
1035
|
this._fullQuery = this.prefilledDisplayValue;
|
1036
|
+
this._markQueried();
|
742
1037
|
}
|
743
1038
|
}
|
744
1039
|
|
745
|
-
|
746
|
-
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(
|
1040
|
+
_selectOnQuery(inputType) {
|
1041
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent({ inputType: inputType }))) {
|
747
1042
|
this._selectNew();
|
748
|
-
} else if (isDeleteEvent(
|
1043
|
+
} else if (isDeleteEvent({ inputType: inputType })) {
|
749
1044
|
this._deselect();
|
750
|
-
} else if (
|
1045
|
+
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
|
751
1046
|
this._selectAndAutocompleteMissingPortion(this._ensurableOption);
|
752
1047
|
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
753
1048
|
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0]);
|
@@ -758,38 +1053,38 @@
|
|
758
1053
|
}
|
759
1054
|
|
760
1055
|
_select(option, autocompleteStrategy) {
|
761
|
-
const previousValue = this.
|
1056
|
+
const previousValue = this._fieldValueString;
|
762
1057
|
|
763
1058
|
this._resetOptionsSilently();
|
764
1059
|
|
765
1060
|
autocompleteStrategy(option);
|
766
1061
|
|
767
|
-
this.
|
1062
|
+
this._fieldValue = option.dataset.value;
|
768
1063
|
this._markSelected(option);
|
769
1064
|
this._markValid();
|
770
|
-
this.
|
1065
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
771
1066
|
|
772
1067
|
option.scrollIntoView({ block: "nearest" });
|
773
1068
|
}
|
774
1069
|
|
775
1070
|
_selectNew() {
|
776
|
-
const previousValue = this.
|
1071
|
+
const previousValue = this._fieldValueString;
|
777
1072
|
|
778
1073
|
this._resetOptionsSilently();
|
779
|
-
this.
|
780
|
-
this.
|
1074
|
+
this._fieldValue = this._fullQuery;
|
1075
|
+
this._fieldName = this.nameWhenNewValue;
|
781
1076
|
this._markValid();
|
782
|
-
this.
|
1077
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
|
783
1078
|
}
|
784
1079
|
|
785
1080
|
_deselect() {
|
786
|
-
const previousValue = this.
|
1081
|
+
const previousValue = this._fieldValueString;
|
787
1082
|
|
788
1083
|
if (this._selectedOptionElement) {
|
789
1084
|
this._markNotSelected(this._selectedOptionElement);
|
790
1085
|
}
|
791
1086
|
|
792
|
-
this.
|
1087
|
+
this._fieldValue = "";
|
793
1088
|
this._setActiveDescendant("");
|
794
1089
|
|
795
1090
|
return previousValue
|
@@ -797,16 +1092,7 @@
|
|
797
1092
|
|
798
1093
|
_deselectAndNotify() {
|
799
1094
|
const previousValue = this._deselect();
|
800
|
-
this.
|
801
|
-
}
|
802
|
-
|
803
|
-
_forceSelectionAndFilter(option, event) {
|
804
|
-
this._forceSelectionWithoutFiltering(option);
|
805
|
-
this._filter(event);
|
806
|
-
}
|
807
|
-
|
808
|
-
_forceSelectionWithoutFiltering(option) {
|
809
|
-
this._selectAndReplaceFullQuery(option);
|
1095
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
810
1096
|
}
|
811
1097
|
|
812
1098
|
_selectIndex(index) {
|
@@ -814,27 +1100,40 @@
|
|
814
1100
|
this._forceSelectionWithoutFiltering(option);
|
815
1101
|
}
|
816
1102
|
|
817
|
-
|
818
|
-
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
819
|
-
const option = this.
|
820
|
-
return option.dataset.value === this._value
|
821
|
-
});
|
822
|
-
|
1103
|
+
_preselectSingle() {
|
1104
|
+
if (this._isSingleSelect && this._hasValueButNoSelection && this._allOptions.length < 100) {
|
1105
|
+
const option = this._optionElementWithValue(this._fieldValue);
|
823
1106
|
if (option) this._markSelected(option);
|
824
1107
|
}
|
825
1108
|
}
|
826
1109
|
|
827
|
-
|
828
|
-
this.
|
1110
|
+
_preselectMultiple() {
|
1111
|
+
if (this._isMultiselect && this._hasValueButNoSelection) {
|
1112
|
+
this._requestChips(this._fieldValueString);
|
1113
|
+
this._resetMultiselectionMarks();
|
1114
|
+
}
|
829
1115
|
}
|
830
1116
|
|
831
1117
|
_selectAndAutocompleteMissingPortion(option) {
|
832
1118
|
this._select(option, this._autocompleteMissingPortion.bind(this));
|
833
1119
|
}
|
834
1120
|
|
1121
|
+
_selectAndAutocompleteFullQuery(option) {
|
1122
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
|
1123
|
+
}
|
1124
|
+
|
1125
|
+
_forceSelectionAndFilter(option, inputType) {
|
1126
|
+
this._forceSelectionWithoutFiltering(option);
|
1127
|
+
this._filter(inputType);
|
1128
|
+
}
|
1129
|
+
|
1130
|
+
_forceSelectionWithoutFiltering(option) {
|
1131
|
+
this._selectAndAutocompleteFullQuery(option);
|
1132
|
+
}
|
1133
|
+
|
835
1134
|
_lockInSelection() {
|
836
1135
|
if (this._shouldLockInSelection) {
|
837
|
-
this._forceSelectionAndFilter(this._ensurableOption,
|
1136
|
+
this._forceSelectionAndFilter(this._ensurableOption, "hw:lockInSelection");
|
838
1137
|
}
|
839
1138
|
}
|
840
1139
|
|
@@ -858,20 +1157,16 @@
|
|
858
1157
|
this._setActiveDescendant("");
|
859
1158
|
}
|
860
1159
|
|
861
|
-
|
862
|
-
this.
|
863
|
-
}
|
864
|
-
|
865
|
-
_setName(value) {
|
866
|
-
this.hiddenFieldTarget.name = value;
|
867
|
-
}
|
868
|
-
|
869
|
-
get _value() {
|
870
|
-
return this.hiddenFieldTarget.value
|
1160
|
+
get _hasValueButNoSelection() {
|
1161
|
+
return this._hasFieldValue && !this._hasSelection
|
871
1162
|
}
|
872
1163
|
|
873
|
-
get
|
874
|
-
|
1164
|
+
get _hasSelection() {
|
1165
|
+
if (this._isSingleSelect) {
|
1166
|
+
this._selectedOptionElement;
|
1167
|
+
} else {
|
1168
|
+
this._multiselectedOptionElements.length > 0;
|
1169
|
+
}
|
875
1170
|
}
|
876
1171
|
|
877
1172
|
get _shouldLockInSelection() {
|
@@ -1162,22 +1457,40 @@
|
|
1162
1457
|
this.expandedValue = true;
|
1163
1458
|
}
|
1164
1459
|
|
1165
|
-
|
1460
|
+
openByFocusing() {
|
1461
|
+
this._actingCombobox.focus();
|
1462
|
+
}
|
1463
|
+
|
1464
|
+
close(inputType) {
|
1166
1465
|
if (this._isOpen) {
|
1466
|
+
const shouldReopen = this._isMultiselect &&
|
1467
|
+
this._isSync &&
|
1468
|
+
!this._isSmallViewport &&
|
1469
|
+
inputType != "hw:clickOutside" &&
|
1470
|
+
inputType != "hw:focusOutside";
|
1471
|
+
|
1167
1472
|
this._lockInSelection();
|
1168
1473
|
this._clearInvalidQuery();
|
1169
1474
|
|
1170
1475
|
this.expandedValue = false;
|
1171
1476
|
|
1172
|
-
this.
|
1477
|
+
this._dispatchSelectionEvent();
|
1478
|
+
|
1479
|
+
if (inputType != "hw:keyHandler:escape") {
|
1480
|
+
this._createChip(shouldReopen);
|
1481
|
+
}
|
1482
|
+
|
1483
|
+
if (this._isSingleSelect && this._selectedOptionElement) {
|
1484
|
+
this._announceToScreenReader(this._displayForOptionElement(this._selectedOptionElement), "selected");
|
1485
|
+
}
|
1173
1486
|
}
|
1174
1487
|
}
|
1175
1488
|
|
1176
1489
|
toggle() {
|
1177
1490
|
if (this.expandedValue) {
|
1178
|
-
this.
|
1491
|
+
this._closeAndBlur("hw:toggle");
|
1179
1492
|
} else {
|
1180
|
-
this.
|
1493
|
+
this.openByFocusing();
|
1181
1494
|
}
|
1182
1495
|
}
|
1183
1496
|
|
@@ -1188,14 +1501,14 @@
|
|
1188
1501
|
if (this.mainWrapperTarget.contains(target) && !this._isDialogDismisser(target)) return
|
1189
1502
|
if (this._withinElementBounds(event)) return
|
1190
1503
|
|
1191
|
-
this.
|
1504
|
+
this._closeAndBlur("hw:clickOutside");
|
1192
1505
|
}
|
1193
1506
|
|
1194
1507
|
closeOnFocusOutside({ target }) {
|
1195
1508
|
if (!this._isOpen) return
|
1196
1509
|
if (this.element.contains(target)) return
|
1197
1510
|
|
1198
|
-
this.
|
1511
|
+
this._closeAndBlur("hw:focusOutside");
|
1199
1512
|
}
|
1200
1513
|
|
1201
1514
|
clearOrToggleOnHandleClick() {
|
@@ -1207,6 +1520,11 @@
|
|
1207
1520
|
}
|
1208
1521
|
}
|
1209
1522
|
|
1523
|
+
_closeAndBlur(inputType) {
|
1524
|
+
this.close(inputType);
|
1525
|
+
this._actingCombobox.blur();
|
1526
|
+
}
|
1527
|
+
|
1210
1528
|
// Some browser extensions like 1Password overlay elements on top of the combobox.
|
1211
1529
|
// Hovering over these elements emits a click event for some reason.
|
1212
1530
|
// These events don't contain any telling information, so we use `_withinElementBounds`
|
@@ -1218,18 +1536,16 @@
|
|
1218
1536
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
|
1219
1537
|
}
|
1220
1538
|
|
1221
|
-
_openByFocusing() {
|
1222
|
-
this._actingCombobox.focus();
|
1223
|
-
}
|
1224
|
-
|
1225
1539
|
_isDialogDismisser(target) {
|
1226
1540
|
return target.closest("dialog") && target.role != "combobox"
|
1227
1541
|
}
|
1228
1542
|
|
1229
1543
|
_expand() {
|
1230
|
-
if (this.
|
1544
|
+
if (this._isSync) {
|
1545
|
+
this._preselectSingle();
|
1546
|
+
}
|
1231
1547
|
|
1232
|
-
if (this._autocompletesList && this.
|
1548
|
+
if (this._autocompletesList && this._isSmallViewport) {
|
1233
1549
|
this._openInDialog();
|
1234
1550
|
} else {
|
1235
1551
|
this._openInline();
|
@@ -1290,10 +1606,6 @@
|
|
1290
1606
|
get _isOpen() {
|
1291
1607
|
return this.expandedValue
|
1292
1608
|
}
|
1293
|
-
|
1294
|
-
get _preselectOnExpansion() {
|
1295
|
-
return !this._isAsync // async comboboxes preselect based on callbacks
|
1296
|
-
}
|
1297
1609
|
};
|
1298
1610
|
|
1299
1611
|
Combobox.Validity = Base => class extends Base {
|
@@ -1330,7 +1642,7 @@
|
|
1330
1642
|
// +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
|
1331
1643
|
// because the `required` attribute is only forwarded to the `comboboxTarget` element
|
1332
1644
|
get _valueIsInvalid() {
|
1333
|
-
const isRequiredAndEmpty = this.comboboxTarget.required &&
|
1645
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && this._hasEmptyFieldValue;
|
1334
1646
|
return isRequiredAndEmpty
|
1335
1647
|
}
|
1336
1648
|
};
|
@@ -1340,11 +1652,14 @@
|
|
1340
1652
|
const concerns = [
|
1341
1653
|
stimulus.Controller,
|
1342
1654
|
Combobox.Actors,
|
1655
|
+
Combobox.Announcements,
|
1343
1656
|
Combobox.AsyncLoading,
|
1344
1657
|
Combobox.Autocomplete,
|
1345
1658
|
Combobox.Dialog,
|
1346
1659
|
Combobox.Events,
|
1347
1660
|
Combobox.Filtering,
|
1661
|
+
Combobox.FormField,
|
1662
|
+
Combobox.Multiselect,
|
1348
1663
|
Combobox.Navigation,
|
1349
1664
|
Combobox.NewOptions,
|
1350
1665
|
Combobox.Options,
|
@@ -1360,7 +1675,10 @@
|
|
1360
1675
|
]
|
1361
1676
|
|
1362
1677
|
static targets = [
|
1678
|
+
"announcer",
|
1363
1679
|
"combobox",
|
1680
|
+
"chipDismisser",
|
1681
|
+
"closer",
|
1364
1682
|
"dialog",
|
1365
1683
|
"dialogCombobox",
|
1366
1684
|
"dialogFocusTrap",
|
@@ -1381,12 +1699,14 @@
|
|
1381
1699
|
nameWhenNew: String,
|
1382
1700
|
originalName: String,
|
1383
1701
|
prefilledDisplay: String,
|
1702
|
+
selectionChipSrc: String,
|
1384
1703
|
smallViewportMaxWidth: String
|
1385
1704
|
}
|
1386
1705
|
|
1387
1706
|
initialize() {
|
1388
1707
|
this._initializeActors();
|
1389
1708
|
this._initializeFiltering();
|
1709
|
+
this._initializeMultiselect();
|
1390
1710
|
}
|
1391
1711
|
|
1392
1712
|
connect() {
|
@@ -1411,13 +1731,27 @@
|
|
1411
1731
|
const inputType = element.dataset.inputType;
|
1412
1732
|
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
|
1413
1733
|
|
1414
|
-
|
1734
|
+
this._resetMultiselectionMarks();
|
1735
|
+
|
1736
|
+
if (inputType === "hw:multiselectSync") {
|
1737
|
+
this.openByFocusing();
|
1738
|
+
} else if (inputType && inputType !== "hw:lockInSelection") {
|
1415
1739
|
if (delay) await sleep(delay);
|
1416
|
-
this.
|
1740
|
+
this._selectOnQuery(inputType);
|
1417
1741
|
} else {
|
1418
|
-
this.
|
1742
|
+
this._preselectSingle();
|
1419
1743
|
}
|
1420
1744
|
}
|
1745
|
+
|
1746
|
+
closerTargetConnected() {
|
1747
|
+
this._closeAndBlur("hw:asyncCloser");
|
1748
|
+
}
|
1749
|
+
|
1750
|
+
// Use +_printStack+ for debugging purposes
|
1751
|
+
_printStack() {
|
1752
|
+
const err = new Error();
|
1753
|
+
console.log(err.stack || err.stacktrace);
|
1754
|
+
}
|
1421
1755
|
}
|
1422
1756
|
|
1423
1757
|
return HwComboboxController;
|