hotwire_combobox 0.1.43 → 0.2.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/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;
|