hotwire_combobox 0.1.42 → 0.1.43
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/app/assets/javascripts/controllers/hw_combobox_controller.js +1 -1
- data/app/assets/javascripts/hotwire_combobox.esm.js +150 -80
- data/app/assets/javascripts/hotwire_combobox.umd.js +150 -80
- data/app/assets/javascripts/hw_combobox/models/combobox/async_loading.js +4 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +8 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/events.js +8 -3
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +19 -22
- data/app/assets/javascripts/hw_combobox/models/combobox/options.js +12 -4
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +95 -44
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +12 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/validity.js +1 -1
- data/app/presenters/hotwire_combobox/component.rb +2 -2
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d76e27ecc585f8ad3d4d596d2a6f8fe9916909734027d251cb35264604f192e
|
4
|
+
data.tar.gz: eda85b2dd3c21c407c806a5144c4d9b39c40a36c827c7d3b1986a5e27d432cf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cab9b83d4bf4caf3e741f5c12f336241e77f6eec0c97041f25162ecb4aade0d0c58b3ce59e5772b1e5edb89a2a6ad92051c8237f0d40bcee30e550e692add1b
|
7
|
+
data.tar.gz: 1d40dfa00ae3884e8c44693abfa542859ed27fb4fb124a90264c03a57ade1dcb31865f7d89078ea4304968f3adcaba51905d940c336a29ed860857fc5b6e5f40
|
@@ -80,7 +80,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
80
80
|
|
81
81
|
if (inputType && inputType !== "hw:lockInSelection") {
|
82
82
|
if (delay) await sleep(delay)
|
83
|
-
this.
|
83
|
+
this._selectBasedOnQuery({ inputType })
|
84
84
|
} else {
|
85
85
|
this._preselectOption()
|
86
86
|
}
|
@@ -37,6 +37,10 @@ Combobox.AsyncLoading = Base => class extends Base {
|
|
37
37
|
get _isAsync() {
|
38
38
|
return this.hasAsyncSrcValue
|
39
39
|
}
|
40
|
+
|
41
|
+
get _isSync() {
|
42
|
+
return !this._isAsync
|
43
|
+
}
|
40
44
|
};
|
41
45
|
|
42
46
|
function Concerns(Base, ...mixins) {
|
@@ -128,16 +132,18 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
128
132
|
}
|
129
133
|
}
|
130
134
|
|
131
|
-
|
132
|
-
|
135
|
+
_replaceFullQueryWithAutocompletedValue(option) {
|
136
|
+
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
137
|
+
|
138
|
+
this._fullQuery = autocompletedValue;
|
139
|
+
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
|
140
|
+
}
|
133
141
|
|
142
|
+
_autocompleteMissingPortion(option) {
|
134
143
|
const typedValue = this._typedQuery;
|
135
144
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
136
145
|
|
137
|
-
if (
|
138
|
-
this._fullQuery = autocompletedValue;
|
139
|
-
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
|
140
|
-
} else if (startsWith(autocompletedValue, typedValue)) {
|
146
|
+
if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
|
141
147
|
this._fullQuery = autocompletedValue;
|
142
148
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
|
143
149
|
}
|
@@ -236,8 +242,13 @@ Combobox.Dialog = Base => class extends Base {
|
|
236
242
|
};
|
237
243
|
|
238
244
|
Combobox.Events = Base => class extends Base {
|
239
|
-
_dispatchSelectionEvent({
|
240
|
-
|
245
|
+
_dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
|
246
|
+
if (previousValue !== this._value) {
|
247
|
+
dispatch("hw-combobox:selection", {
|
248
|
+
target: this.element,
|
249
|
+
detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
|
250
|
+
});
|
251
|
+
}
|
241
252
|
}
|
242
253
|
|
243
254
|
_dispatchClosedEvent() {
|
@@ -246,7 +257,7 @@ Combobox.Events = Base => class extends Base {
|
|
246
257
|
|
247
258
|
get _eventableDetails() {
|
248
259
|
return {
|
249
|
-
value: this.
|
260
|
+
value: this._value,
|
250
261
|
display: this._fullQuery,
|
251
262
|
query: this._typedQuery,
|
252
263
|
fieldName: this.hiddenFieldTarget.name,
|
@@ -533,20 +544,28 @@ async function get(url, options) {
|
|
533
544
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
534
545
|
|
535
546
|
Combobox.Filtering = Base => class extends Base {
|
536
|
-
|
547
|
+
filterAndSelect(event) {
|
548
|
+
this._filter(event);
|
549
|
+
|
550
|
+
if (this._isSync) {
|
551
|
+
this._selectBasedOnQuery(event);
|
552
|
+
}
|
553
|
+
}
|
554
|
+
|
555
|
+
_initializeFiltering() {
|
556
|
+
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
557
|
+
}
|
558
|
+
|
559
|
+
_filter(event) {
|
537
560
|
if (this._isAsync) {
|
538
561
|
this._debouncedFilterAsync(event);
|
539
562
|
} else {
|
540
|
-
this._filterSync(
|
563
|
+
this._filterSync();
|
541
564
|
}
|
542
565
|
|
543
566
|
this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
|
544
567
|
}
|
545
568
|
|
546
|
-
_initializeFiltering() {
|
547
|
-
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
548
|
-
}
|
549
|
-
|
550
569
|
_debouncedFilterAsync(event) {
|
551
570
|
this._filterAsync(event);
|
552
571
|
}
|
@@ -561,27 +580,14 @@ Combobox.Filtering = Base => class extends Base {
|
|
561
580
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
|
562
581
|
}
|
563
582
|
|
564
|
-
_filterSync(
|
583
|
+
_filterSync() {
|
565
584
|
this.open();
|
566
585
|
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
|
567
|
-
this._commitFilter(event);
|
568
|
-
}
|
569
|
-
|
570
|
-
_commitFilter(event) {
|
571
|
-
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
572
|
-
this._selectNew();
|
573
|
-
} else if (isDeleteEvent(event)) {
|
574
|
-
this._deselect();
|
575
|
-
} else if (event.inputType === "hw:lockInSelection") {
|
576
|
-
this._select(this._ensurableOption);
|
577
|
-
} else if (this._isOpen) {
|
578
|
-
this._select(this._visibleOptionElements[0]);
|
579
|
-
}
|
580
586
|
}
|
581
587
|
|
582
588
|
_clearQuery() {
|
583
589
|
this._fullQuery = "";
|
584
|
-
this.
|
590
|
+
this.filterAndSelect({ inputType: "deleteContentBackward" });
|
585
591
|
}
|
586
592
|
|
587
593
|
get _isQueried() {
|
@@ -675,9 +681,17 @@ Combobox.NewOptions = Base => class extends Base {
|
|
675
681
|
};
|
676
682
|
|
677
683
|
Combobox.Options = Base => class extends Base {
|
678
|
-
|
679
|
-
this._deselect();
|
680
|
-
|
684
|
+
_resetOptionsSilently() {
|
685
|
+
this._resetOptions(this._deselect.bind(this));
|
686
|
+
}
|
687
|
+
|
688
|
+
_resetOptionsAndNotify() {
|
689
|
+
this._resetOptions(this._deselectAndNotify.bind(this));
|
690
|
+
}
|
691
|
+
|
692
|
+
_resetOptions(deselectionStrategy) {
|
693
|
+
this._setName(this.originalNameValue);
|
694
|
+
deselectionStrategy();
|
681
695
|
}
|
682
696
|
|
683
697
|
get _allowNew() {
|
@@ -705,7 +719,7 @@ Combobox.Options = Base => class extends Base {
|
|
705
719
|
}
|
706
720
|
|
707
721
|
get _isUnjustifiablyBlank() {
|
708
|
-
const valueIsMissing = !this.
|
722
|
+
const valueIsMissing = !this._value;
|
709
723
|
const noBlankOptionSelected = !this._selectedOptionElement;
|
710
724
|
|
711
725
|
return valueIsMissing && noBlankOptionSelected
|
@@ -714,8 +728,7 @@ Combobox.Options = Base => class extends Base {
|
|
714
728
|
|
715
729
|
Combobox.Selection = Base => class extends Base {
|
716
730
|
selectOptionOnClick(event) {
|
717
|
-
this.
|
718
|
-
this._select(event.currentTarget, { forceAutocomplete: true });
|
731
|
+
this._forceSelectionAndFilter(event.currentTarget, event);
|
719
732
|
this.close();
|
720
733
|
}
|
721
734
|
|
@@ -725,91 +738,136 @@ Combobox.Selection = Base => class extends Base {
|
|
725
738
|
}
|
726
739
|
}
|
727
740
|
|
728
|
-
|
729
|
-
this.
|
730
|
-
|
731
|
-
if (
|
732
|
-
this.
|
733
|
-
|
734
|
-
this.
|
735
|
-
} else {
|
741
|
+
_selectBasedOnQuery(event) {
|
742
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
743
|
+
this._selectNew();
|
744
|
+
} else if (isDeleteEvent(event)) {
|
745
|
+
this._deselect();
|
746
|
+
} else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
|
747
|
+
this._selectAndAutocompleteMissingPortion(this._ensurableOption);
|
748
|
+
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
749
|
+
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0]);
|
750
|
+
} else if (this._isOpen) {
|
751
|
+
this._resetOptionsAndNotify();
|
736
752
|
this._markInvalid();
|
737
|
-
}
|
753
|
+
} else ;
|
738
754
|
}
|
739
755
|
|
740
|
-
|
741
|
-
|
756
|
+
_select(option, autocompleteStrategy) {
|
757
|
+
const previousValue = this._value;
|
742
758
|
|
743
|
-
|
744
|
-
this.hiddenFieldTarget.value = option.dataset.value;
|
745
|
-
option.scrollIntoView({ block: "nearest" });
|
746
|
-
}
|
759
|
+
this._resetOptionsSilently();
|
747
760
|
|
748
|
-
|
761
|
+
autocompleteStrategy(option);
|
762
|
+
|
763
|
+
this._setValue(option.dataset.value);
|
764
|
+
this._markSelected(option);
|
765
|
+
this._markValid();
|
766
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
767
|
+
|
768
|
+
option.scrollIntoView({ block: "nearest" });
|
749
769
|
}
|
750
770
|
|
751
|
-
|
752
|
-
|
753
|
-
option.classList.toggle(this.selectedClass, selected);
|
754
|
-
}
|
771
|
+
_selectNew() {
|
772
|
+
const previousValue = this._value;
|
755
773
|
|
756
|
-
|
757
|
-
this.
|
774
|
+
this._resetOptionsSilently();
|
775
|
+
this._setValue(this._fullQuery);
|
776
|
+
this._setName(this.nameWhenNewValue);
|
777
|
+
this._markValid();
|
778
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
|
758
779
|
}
|
759
780
|
|
760
781
|
_deselect() {
|
761
|
-
const
|
782
|
+
const previousValue = this._value;
|
762
783
|
|
763
|
-
if (
|
784
|
+
if (this._selectedOptionElement) {
|
785
|
+
this._markNotSelected(this._selectedOptionElement);
|
786
|
+
}
|
764
787
|
|
765
|
-
this.
|
788
|
+
this._setValue(null);
|
766
789
|
this._setActiveDescendant("");
|
767
790
|
|
768
|
-
|
791
|
+
return previousValue
|
769
792
|
}
|
770
793
|
|
771
|
-
|
772
|
-
this.
|
773
|
-
this.
|
774
|
-
|
775
|
-
this._markValid();
|
794
|
+
_deselectAndNotify() {
|
795
|
+
const previousValue = this._deselect();
|
796
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
797
|
+
}
|
776
798
|
|
777
|
-
|
799
|
+
_forceSelectionAndFilter(option, event) {
|
800
|
+
this._forceSelectionWithoutFiltering(option);
|
801
|
+
this._filter(event);
|
802
|
+
}
|
803
|
+
|
804
|
+
_forceSelectionWithoutFiltering(option) {
|
805
|
+
this._selectAndReplaceFullQuery(option);
|
778
806
|
}
|
779
807
|
|
780
808
|
_selectIndex(index) {
|
781
809
|
const option = wrapAroundAccess(this._visibleOptionElements, index);
|
782
|
-
this.
|
810
|
+
this._forceSelectionWithoutFiltering(option);
|
783
811
|
}
|
784
812
|
|
785
813
|
_preselectOption() {
|
786
814
|
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
787
815
|
const option = this._allOptions.find(option => {
|
788
|
-
return option.dataset.value === this.
|
816
|
+
return option.dataset.value === this._value
|
789
817
|
});
|
790
818
|
|
791
|
-
if (option) this._markSelected(option
|
819
|
+
if (option) this._markSelected(option);
|
792
820
|
}
|
793
821
|
}
|
794
822
|
|
823
|
+
_selectAndReplaceFullQuery(option) {
|
824
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
|
825
|
+
}
|
826
|
+
|
827
|
+
_selectAndAutocompleteMissingPortion(option) {
|
828
|
+
this._select(option, this._autocompleteMissingPortion.bind(this));
|
829
|
+
}
|
830
|
+
|
795
831
|
_lockInSelection() {
|
796
832
|
if (this._shouldLockInSelection) {
|
797
|
-
this.
|
798
|
-
this.filter({ inputType: "hw:lockInSelection" });
|
833
|
+
this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" });
|
799
834
|
}
|
835
|
+
}
|
800
836
|
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
837
|
+
_markSelected(option) {
|
838
|
+
if (this.hasSelectedClass) option.classList.add(this.selectedClass);
|
839
|
+
option.setAttribute("aria-selected", true);
|
840
|
+
this._setActiveDescendant(option.id);
|
841
|
+
}
|
842
|
+
|
843
|
+
_markNotSelected(option) {
|
844
|
+
if (this.hasSelectedClass) option.classList.remove(this.selectedClass);
|
845
|
+
option.removeAttribute("aria-selected");
|
846
|
+
this._removeActiveDescendant();
|
805
847
|
}
|
806
848
|
|
807
849
|
_setActiveDescendant(id) {
|
808
850
|
this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id));
|
809
851
|
}
|
810
852
|
|
853
|
+
_removeActiveDescendant() {
|
854
|
+
this._setActiveDescendant("");
|
855
|
+
}
|
856
|
+
|
857
|
+
_setValue(value) {
|
858
|
+
this.hiddenFieldTarget.value = value;
|
859
|
+
}
|
860
|
+
|
861
|
+
_setName(value) {
|
862
|
+
this.hiddenFieldTarget.name = value;
|
863
|
+
}
|
864
|
+
|
865
|
+
get _value() {
|
866
|
+
return this.hiddenFieldTarget.value
|
867
|
+
}
|
868
|
+
|
811
869
|
get _hasValueButNoSelection() {
|
812
|
-
return this.
|
870
|
+
return this._value && !this._selectedOptionElement
|
813
871
|
}
|
814
872
|
|
815
873
|
get _shouldLockInSelection() {
|
@@ -1103,7 +1161,10 @@ Combobox.Toggle = Base => class extends Base {
|
|
1103
1161
|
close() {
|
1104
1162
|
if (this._isOpen) {
|
1105
1163
|
this._lockInSelection();
|
1164
|
+
this._clearInvalidQuery();
|
1165
|
+
|
1106
1166
|
this.expandedValue = false;
|
1167
|
+
|
1107
1168
|
this._dispatchClosedEvent();
|
1108
1169
|
}
|
1109
1170
|
}
|
@@ -1173,6 +1234,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
1173
1234
|
this._actingCombobox.setAttribute("aria-expanded", true); // needs to happen after setting acting combobox
|
1174
1235
|
}
|
1175
1236
|
|
1237
|
+
// +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
|
1238
|
+
// it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
|
1176
1239
|
_collapse() {
|
1177
1240
|
this._actingCombobox.setAttribute("aria-expanded", false); // needs to happen before resetting acting combobox
|
1178
1241
|
|
@@ -1213,6 +1276,13 @@ Combobox.Toggle = Base => class extends Base {
|
|
1213
1276
|
enableBodyScroll(this.dialogListboxTarget);
|
1214
1277
|
}
|
1215
1278
|
|
1279
|
+
_clearInvalidQuery() {
|
1280
|
+
if (this._isUnjustifiablyBlank) {
|
1281
|
+
this._deselect();
|
1282
|
+
this._clearQuery();
|
1283
|
+
}
|
1284
|
+
}
|
1285
|
+
|
1216
1286
|
get _isOpen() {
|
1217
1287
|
return this.expandedValue
|
1218
1288
|
}
|
@@ -1256,7 +1326,7 @@ Combobox.Validity = Base => class extends Base {
|
|
1256
1326
|
// +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
|
1257
1327
|
// because the `required` attribute is only forwarded to the `comboboxTarget` element
|
1258
1328
|
get _valueIsInvalid() {
|
1259
|
-
const isRequiredAndEmpty = this.comboboxTarget.required && !this.
|
1329
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && !this._value;
|
1260
1330
|
return isRequiredAndEmpty
|
1261
1331
|
}
|
1262
1332
|
};
|
@@ -1339,7 +1409,7 @@ class HwComboboxController extends Concerns(...concerns) {
|
|
1339
1409
|
|
1340
1410
|
if (inputType && inputType !== "hw:lockInSelection") {
|
1341
1411
|
if (delay) await sleep(delay);
|
1342
|
-
this.
|
1412
|
+
this._selectBasedOnQuery({ inputType });
|
1343
1413
|
} else {
|
1344
1414
|
this._preselectOption();
|
1345
1415
|
}
|
@@ -41,6 +41,10 @@
|
|
41
41
|
get _isAsync() {
|
42
42
|
return this.hasAsyncSrcValue
|
43
43
|
}
|
44
|
+
|
45
|
+
get _isSync() {
|
46
|
+
return !this._isAsync
|
47
|
+
}
|
44
48
|
};
|
45
49
|
|
46
50
|
function Concerns(Base, ...mixins) {
|
@@ -132,16 +136,18 @@
|
|
132
136
|
}
|
133
137
|
}
|
134
138
|
|
135
|
-
|
136
|
-
|
139
|
+
_replaceFullQueryWithAutocompletedValue(option) {
|
140
|
+
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
141
|
+
|
142
|
+
this._fullQuery = autocompletedValue;
|
143
|
+
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
|
144
|
+
}
|
137
145
|
|
146
|
+
_autocompleteMissingPortion(option) {
|
138
147
|
const typedValue = this._typedQuery;
|
139
148
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
|
140
149
|
|
141
|
-
if (
|
142
|
-
this._fullQuery = autocompletedValue;
|
143
|
-
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
|
144
|
-
} else if (startsWith(autocompletedValue, typedValue)) {
|
150
|
+
if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
|
145
151
|
this._fullQuery = autocompletedValue;
|
146
152
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
|
147
153
|
}
|
@@ -240,8 +246,13 @@
|
|
240
246
|
};
|
241
247
|
|
242
248
|
Combobox.Events = Base => class extends Base {
|
243
|
-
_dispatchSelectionEvent({
|
244
|
-
|
249
|
+
_dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
|
250
|
+
if (previousValue !== this._value) {
|
251
|
+
dispatch("hw-combobox:selection", {
|
252
|
+
target: this.element,
|
253
|
+
detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
|
254
|
+
});
|
255
|
+
}
|
245
256
|
}
|
246
257
|
|
247
258
|
_dispatchClosedEvent() {
|
@@ -250,7 +261,7 @@
|
|
250
261
|
|
251
262
|
get _eventableDetails() {
|
252
263
|
return {
|
253
|
-
value: this.
|
264
|
+
value: this._value,
|
254
265
|
display: this._fullQuery,
|
255
266
|
query: this._typedQuery,
|
256
267
|
fieldName: this.hiddenFieldTarget.name,
|
@@ -537,20 +548,28 @@
|
|
537
548
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
538
549
|
|
539
550
|
Combobox.Filtering = Base => class extends Base {
|
540
|
-
|
551
|
+
filterAndSelect(event) {
|
552
|
+
this._filter(event);
|
553
|
+
|
554
|
+
if (this._isSync) {
|
555
|
+
this._selectBasedOnQuery(event);
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
_initializeFiltering() {
|
560
|
+
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
561
|
+
}
|
562
|
+
|
563
|
+
_filter(event) {
|
541
564
|
if (this._isAsync) {
|
542
565
|
this._debouncedFilterAsync(event);
|
543
566
|
} else {
|
544
|
-
this._filterSync(
|
567
|
+
this._filterSync();
|
545
568
|
}
|
546
569
|
|
547
570
|
this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
|
548
571
|
}
|
549
572
|
|
550
|
-
_initializeFiltering() {
|
551
|
-
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
|
552
|
-
}
|
553
|
-
|
554
573
|
_debouncedFilterAsync(event) {
|
555
574
|
this._filterAsync(event);
|
556
575
|
}
|
@@ -565,27 +584,14 @@
|
|
565
584
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
|
566
585
|
}
|
567
586
|
|
568
|
-
_filterSync(
|
587
|
+
_filterSync() {
|
569
588
|
this.open();
|
570
589
|
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
|
571
|
-
this._commitFilter(event);
|
572
|
-
}
|
573
|
-
|
574
|
-
_commitFilter(event) {
|
575
|
-
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
576
|
-
this._selectNew();
|
577
|
-
} else if (isDeleteEvent(event)) {
|
578
|
-
this._deselect();
|
579
|
-
} else if (event.inputType === "hw:lockInSelection") {
|
580
|
-
this._select(this._ensurableOption);
|
581
|
-
} else if (this._isOpen) {
|
582
|
-
this._select(this._visibleOptionElements[0]);
|
583
|
-
}
|
584
590
|
}
|
585
591
|
|
586
592
|
_clearQuery() {
|
587
593
|
this._fullQuery = "";
|
588
|
-
this.
|
594
|
+
this.filterAndSelect({ inputType: "deleteContentBackward" });
|
589
595
|
}
|
590
596
|
|
591
597
|
get _isQueried() {
|
@@ -679,9 +685,17 @@
|
|
679
685
|
};
|
680
686
|
|
681
687
|
Combobox.Options = Base => class extends Base {
|
682
|
-
|
683
|
-
this._deselect();
|
684
|
-
|
688
|
+
_resetOptionsSilently() {
|
689
|
+
this._resetOptions(this._deselect.bind(this));
|
690
|
+
}
|
691
|
+
|
692
|
+
_resetOptionsAndNotify() {
|
693
|
+
this._resetOptions(this._deselectAndNotify.bind(this));
|
694
|
+
}
|
695
|
+
|
696
|
+
_resetOptions(deselectionStrategy) {
|
697
|
+
this._setName(this.originalNameValue);
|
698
|
+
deselectionStrategy();
|
685
699
|
}
|
686
700
|
|
687
701
|
get _allowNew() {
|
@@ -709,7 +723,7 @@
|
|
709
723
|
}
|
710
724
|
|
711
725
|
get _isUnjustifiablyBlank() {
|
712
|
-
const valueIsMissing = !this.
|
726
|
+
const valueIsMissing = !this._value;
|
713
727
|
const noBlankOptionSelected = !this._selectedOptionElement;
|
714
728
|
|
715
729
|
return valueIsMissing && noBlankOptionSelected
|
@@ -718,8 +732,7 @@
|
|
718
732
|
|
719
733
|
Combobox.Selection = Base => class extends Base {
|
720
734
|
selectOptionOnClick(event) {
|
721
|
-
this.
|
722
|
-
this._select(event.currentTarget, { forceAutocomplete: true });
|
735
|
+
this._forceSelectionAndFilter(event.currentTarget, event);
|
723
736
|
this.close();
|
724
737
|
}
|
725
738
|
|
@@ -729,91 +742,136 @@
|
|
729
742
|
}
|
730
743
|
}
|
731
744
|
|
732
|
-
|
733
|
-
this.
|
734
|
-
|
735
|
-
if (
|
736
|
-
this.
|
737
|
-
|
738
|
-
this.
|
739
|
-
} else {
|
745
|
+
_selectBasedOnQuery(event) {
|
746
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
747
|
+
this._selectNew();
|
748
|
+
} else if (isDeleteEvent(event)) {
|
749
|
+
this._deselect();
|
750
|
+
} else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
|
751
|
+
this._selectAndAutocompleteMissingPortion(this._ensurableOption);
|
752
|
+
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
753
|
+
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0]);
|
754
|
+
} else if (this._isOpen) {
|
755
|
+
this._resetOptionsAndNotify();
|
740
756
|
this._markInvalid();
|
741
|
-
}
|
757
|
+
} else ;
|
742
758
|
}
|
743
759
|
|
744
|
-
|
745
|
-
|
760
|
+
_select(option, autocompleteStrategy) {
|
761
|
+
const previousValue = this._value;
|
746
762
|
|
747
|
-
|
748
|
-
this.hiddenFieldTarget.value = option.dataset.value;
|
749
|
-
option.scrollIntoView({ block: "nearest" });
|
750
|
-
}
|
763
|
+
this._resetOptionsSilently();
|
751
764
|
|
752
|
-
|
765
|
+
autocompleteStrategy(option);
|
766
|
+
|
767
|
+
this._setValue(option.dataset.value);
|
768
|
+
this._markSelected(option);
|
769
|
+
this._markValid();
|
770
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
771
|
+
|
772
|
+
option.scrollIntoView({ block: "nearest" });
|
753
773
|
}
|
754
774
|
|
755
|
-
|
756
|
-
|
757
|
-
option.classList.toggle(this.selectedClass, selected);
|
758
|
-
}
|
775
|
+
_selectNew() {
|
776
|
+
const previousValue = this._value;
|
759
777
|
|
760
|
-
|
761
|
-
this.
|
778
|
+
this._resetOptionsSilently();
|
779
|
+
this._setValue(this._fullQuery);
|
780
|
+
this._setName(this.nameWhenNewValue);
|
781
|
+
this._markValid();
|
782
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
|
762
783
|
}
|
763
784
|
|
764
785
|
_deselect() {
|
765
|
-
const
|
786
|
+
const previousValue = this._value;
|
766
787
|
|
767
|
-
if (
|
788
|
+
if (this._selectedOptionElement) {
|
789
|
+
this._markNotSelected(this._selectedOptionElement);
|
790
|
+
}
|
768
791
|
|
769
|
-
this.
|
792
|
+
this._setValue(null);
|
770
793
|
this._setActiveDescendant("");
|
771
794
|
|
772
|
-
|
795
|
+
return previousValue
|
773
796
|
}
|
774
797
|
|
775
|
-
|
776
|
-
this.
|
777
|
-
this.
|
778
|
-
|
779
|
-
this._markValid();
|
798
|
+
_deselectAndNotify() {
|
799
|
+
const previousValue = this._deselect();
|
800
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
|
801
|
+
}
|
780
802
|
|
781
|
-
|
803
|
+
_forceSelectionAndFilter(option, event) {
|
804
|
+
this._forceSelectionWithoutFiltering(option);
|
805
|
+
this._filter(event);
|
806
|
+
}
|
807
|
+
|
808
|
+
_forceSelectionWithoutFiltering(option) {
|
809
|
+
this._selectAndReplaceFullQuery(option);
|
782
810
|
}
|
783
811
|
|
784
812
|
_selectIndex(index) {
|
785
813
|
const option = wrapAroundAccess(this._visibleOptionElements, index);
|
786
|
-
this.
|
814
|
+
this._forceSelectionWithoutFiltering(option);
|
787
815
|
}
|
788
816
|
|
789
817
|
_preselectOption() {
|
790
818
|
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
791
819
|
const option = this._allOptions.find(option => {
|
792
|
-
return option.dataset.value === this.
|
820
|
+
return option.dataset.value === this._value
|
793
821
|
});
|
794
822
|
|
795
|
-
if (option) this._markSelected(option
|
823
|
+
if (option) this._markSelected(option);
|
796
824
|
}
|
797
825
|
}
|
798
826
|
|
827
|
+
_selectAndReplaceFullQuery(option) {
|
828
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
|
829
|
+
}
|
830
|
+
|
831
|
+
_selectAndAutocompleteMissingPortion(option) {
|
832
|
+
this._select(option, this._autocompleteMissingPortion.bind(this));
|
833
|
+
}
|
834
|
+
|
799
835
|
_lockInSelection() {
|
800
836
|
if (this._shouldLockInSelection) {
|
801
|
-
this.
|
802
|
-
this.filter({ inputType: "hw:lockInSelection" });
|
837
|
+
this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" });
|
803
838
|
}
|
839
|
+
}
|
804
840
|
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
841
|
+
_markSelected(option) {
|
842
|
+
if (this.hasSelectedClass) option.classList.add(this.selectedClass);
|
843
|
+
option.setAttribute("aria-selected", true);
|
844
|
+
this._setActiveDescendant(option.id);
|
845
|
+
}
|
846
|
+
|
847
|
+
_markNotSelected(option) {
|
848
|
+
if (this.hasSelectedClass) option.classList.remove(this.selectedClass);
|
849
|
+
option.removeAttribute("aria-selected");
|
850
|
+
this._removeActiveDescendant();
|
809
851
|
}
|
810
852
|
|
811
853
|
_setActiveDescendant(id) {
|
812
854
|
this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id));
|
813
855
|
}
|
814
856
|
|
857
|
+
_removeActiveDescendant() {
|
858
|
+
this._setActiveDescendant("");
|
859
|
+
}
|
860
|
+
|
861
|
+
_setValue(value) {
|
862
|
+
this.hiddenFieldTarget.value = value;
|
863
|
+
}
|
864
|
+
|
865
|
+
_setName(value) {
|
866
|
+
this.hiddenFieldTarget.name = value;
|
867
|
+
}
|
868
|
+
|
869
|
+
get _value() {
|
870
|
+
return this.hiddenFieldTarget.value
|
871
|
+
}
|
872
|
+
|
815
873
|
get _hasValueButNoSelection() {
|
816
|
-
return this.
|
874
|
+
return this._value && !this._selectedOptionElement
|
817
875
|
}
|
818
876
|
|
819
877
|
get _shouldLockInSelection() {
|
@@ -1107,7 +1165,10 @@
|
|
1107
1165
|
close() {
|
1108
1166
|
if (this._isOpen) {
|
1109
1167
|
this._lockInSelection();
|
1168
|
+
this._clearInvalidQuery();
|
1169
|
+
|
1110
1170
|
this.expandedValue = false;
|
1171
|
+
|
1111
1172
|
this._dispatchClosedEvent();
|
1112
1173
|
}
|
1113
1174
|
}
|
@@ -1177,6 +1238,8 @@
|
|
1177
1238
|
this._actingCombobox.setAttribute("aria-expanded", true); // needs to happen after setting acting combobox
|
1178
1239
|
}
|
1179
1240
|
|
1241
|
+
// +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
|
1242
|
+
// it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
|
1180
1243
|
_collapse() {
|
1181
1244
|
this._actingCombobox.setAttribute("aria-expanded", false); // needs to happen before resetting acting combobox
|
1182
1245
|
|
@@ -1217,6 +1280,13 @@
|
|
1217
1280
|
enableBodyScroll(this.dialogListboxTarget);
|
1218
1281
|
}
|
1219
1282
|
|
1283
|
+
_clearInvalidQuery() {
|
1284
|
+
if (this._isUnjustifiablyBlank) {
|
1285
|
+
this._deselect();
|
1286
|
+
this._clearQuery();
|
1287
|
+
}
|
1288
|
+
}
|
1289
|
+
|
1220
1290
|
get _isOpen() {
|
1221
1291
|
return this.expandedValue
|
1222
1292
|
}
|
@@ -1260,7 +1330,7 @@
|
|
1260
1330
|
// +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
|
1261
1331
|
// because the `required` attribute is only forwarded to the `comboboxTarget` element
|
1262
1332
|
get _valueIsInvalid() {
|
1263
|
-
const isRequiredAndEmpty = this.comboboxTarget.required && !this.
|
1333
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && !this._value;
|
1264
1334
|
return isRequiredAndEmpty
|
1265
1335
|
}
|
1266
1336
|
};
|
@@ -1343,7 +1413,7 @@
|
|
1343
1413
|
|
1344
1414
|
if (inputType && inputType !== "hw:lockInSelection") {
|
1345
1415
|
if (delay) await sleep(delay);
|
1346
|
-
this.
|
1416
|
+
this._selectBasedOnQuery({ inputType });
|
1347
1417
|
} else {
|
1348
1418
|
this._preselectOption();
|
1349
1419
|
}
|
@@ -8,16 +8,18 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
8
8
|
}
|
9
9
|
}
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
_replaceFullQueryWithAutocompletedValue(option) {
|
12
|
+
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
|
13
|
+
|
14
|
+
this._fullQuery = autocompletedValue
|
15
|
+
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
|
16
|
+
}
|
13
17
|
|
18
|
+
_autocompleteMissingPortion(option) {
|
14
19
|
const typedValue = this._typedQuery
|
15
20
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
|
16
21
|
|
17
|
-
if (
|
18
|
-
this._fullQuery = autocompletedValue
|
19
|
-
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
|
20
|
-
} else if (startsWith(autocompletedValue, typedValue)) {
|
22
|
+
if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
|
21
23
|
this._fullQuery = autocompletedValue
|
22
24
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
|
23
25
|
}
|
@@ -2,8 +2,13 @@ import Combobox from "hw_combobox/models/combobox/base"
|
|
2
2
|
import { dispatch } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Events = Base => class extends Base {
|
5
|
-
_dispatchSelectionEvent({
|
6
|
-
|
5
|
+
_dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
|
6
|
+
if (previousValue !== this._value) {
|
7
|
+
dispatch("hw-combobox:selection", {
|
8
|
+
target: this.element,
|
9
|
+
detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
|
10
|
+
})
|
11
|
+
}
|
7
12
|
}
|
8
13
|
|
9
14
|
_dispatchClosedEvent() {
|
@@ -12,7 +17,7 @@ Combobox.Events = Base => class extends Base {
|
|
12
17
|
|
13
18
|
get _eventableDetails() {
|
14
19
|
return {
|
15
|
-
value: this.
|
20
|
+
value: this._value,
|
16
21
|
display: this._fullQuery,
|
17
22
|
query: this._typedQuery,
|
18
23
|
fieldName: this.hiddenFieldTarget.name,
|
@@ -1,23 +1,33 @@
|
|
1
1
|
|
2
2
|
import Combobox from "hw_combobox/models/combobox/base"
|
3
|
-
import { applyFilter, debounce,
|
3
|
+
import { applyFilter, debounce, unselectedPortion } from "hw_combobox/helpers"
|
4
4
|
import { get } from "hw_combobox/vendor/requestjs"
|
5
5
|
|
6
6
|
Combobox.Filtering = Base => class extends Base {
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
filterAndSelect(event) {
|
8
|
+
this._filter(event)
|
9
|
+
|
10
|
+
if (this._isSync) {
|
11
|
+
this._selectBasedOnQuery(event)
|
10
12
|
} else {
|
11
|
-
|
13
|
+
// noop, async selection is handled by stimulus callbacks
|
12
14
|
}
|
13
|
-
|
14
|
-
this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
|
15
15
|
}
|
16
16
|
|
17
17
|
_initializeFiltering() {
|
18
18
|
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
|
19
19
|
}
|
20
20
|
|
21
|
+
_filter(event) {
|
22
|
+
if (this._isAsync) {
|
23
|
+
this._debouncedFilterAsync(event)
|
24
|
+
} else {
|
25
|
+
this._filterSync()
|
26
|
+
}
|
27
|
+
|
28
|
+
this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
|
29
|
+
}
|
30
|
+
|
21
31
|
_debouncedFilterAsync(event) {
|
22
32
|
this._filterAsync(event)
|
23
33
|
}
|
@@ -32,27 +42,14 @@ Combobox.Filtering = Base => class extends Base {
|
|
32
42
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
|
33
43
|
}
|
34
44
|
|
35
|
-
_filterSync(
|
45
|
+
_filterSync() {
|
36
46
|
this.open()
|
37
47
|
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }))
|
38
|
-
this._commitFilter(event)
|
39
|
-
}
|
40
|
-
|
41
|
-
_commitFilter(event) {
|
42
|
-
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
43
|
-
this._selectNew()
|
44
|
-
} else if (isDeleteEvent(event)) {
|
45
|
-
this._deselect()
|
46
|
-
} else if (event.inputType === "hw:lockInSelection") {
|
47
|
-
this._select(this._ensurableOption)
|
48
|
-
} else if (this._isOpen) {
|
49
|
-
this._select(this._visibleOptionElements[0])
|
50
|
-
}
|
51
48
|
}
|
52
49
|
|
53
50
|
_clearQuery() {
|
54
51
|
this._fullQuery = ""
|
55
|
-
this.
|
52
|
+
this.filterAndSelect({ inputType: "deleteContentBackward" })
|
56
53
|
}
|
57
54
|
|
58
55
|
get _isQueried() {
|
@@ -2,9 +2,17 @@ import Combobox from "hw_combobox/models/combobox/base"
|
|
2
2
|
import { visible } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Options = Base => class extends Base {
|
5
|
-
|
6
|
-
this._deselect()
|
7
|
-
|
5
|
+
_resetOptionsSilently() {
|
6
|
+
this._resetOptions(this._deselect.bind(this))
|
7
|
+
}
|
8
|
+
|
9
|
+
_resetOptionsAndNotify() {
|
10
|
+
this._resetOptions(this._deselectAndNotify.bind(this))
|
11
|
+
}
|
12
|
+
|
13
|
+
_resetOptions(deselectionStrategy) {
|
14
|
+
this._setName(this.originalNameValue)
|
15
|
+
deselectionStrategy()
|
8
16
|
}
|
9
17
|
|
10
18
|
get _allowNew() {
|
@@ -32,7 +40,7 @@ Combobox.Options = Base => class extends Base {
|
|
32
40
|
}
|
33
41
|
|
34
42
|
get _isUnjustifiablyBlank() {
|
35
|
-
const valueIsMissing = !this.
|
43
|
+
const valueIsMissing = !this._value
|
36
44
|
const noBlankOptionSelected = !this._selectedOptionElement
|
37
45
|
|
38
46
|
return valueIsMissing && noBlankOptionSelected
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox/base"
|
2
|
-
import { wrapAroundAccess } from "hw_combobox/helpers"
|
2
|
+
import { wrapAroundAccess, isDeleteEvent } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Selection = Base => class extends Base {
|
5
5
|
selectOptionOnClick(event) {
|
6
|
-
this.
|
7
|
-
this._select(event.currentTarget, { forceAutocomplete: true })
|
6
|
+
this._forceSelectionAndFilter(event.currentTarget, event)
|
8
7
|
this.close()
|
9
8
|
}
|
10
9
|
|
@@ -14,91 +13,143 @@ Combobox.Selection = Base => class extends Base {
|
|
14
13
|
}
|
15
14
|
}
|
16
15
|
|
17
|
-
|
18
|
-
this.
|
19
|
-
|
20
|
-
if (
|
21
|
-
this.
|
22
|
-
|
23
|
-
this.
|
24
|
-
} else {
|
16
|
+
_selectBasedOnQuery(event) {
|
17
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
|
18
|
+
this._selectNew()
|
19
|
+
} else if (isDeleteEvent(event)) {
|
20
|
+
this._deselect()
|
21
|
+
} else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
|
22
|
+
this._selectAndAutocompleteMissingPortion(this._ensurableOption)
|
23
|
+
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
24
|
+
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0])
|
25
|
+
} else if (this._isOpen) {
|
26
|
+
this._resetOptionsAndNotify()
|
25
27
|
this._markInvalid()
|
28
|
+
} else {
|
29
|
+
// When selecting from an async dialog listbox: selection is forced, the listbox is filtered,
|
30
|
+
// and the dialog is closed. Filtering ends with an `endOfOptionsStream` target connected
|
31
|
+
// to the now invisible combobox, which is now closed because Turbo waits for "nextRepaint"
|
32
|
+
// before rendering turbo streams. This ultimately calls +_selectBasedOnQuery+. We do want
|
33
|
+
// to call +_selectBasedOnQuery+ in this case to account for e.g. selection of
|
34
|
+
// new options. But we will noop here if it's none of the cases checked above.
|
26
35
|
}
|
27
36
|
}
|
28
37
|
|
29
|
-
|
30
|
-
|
38
|
+
_select(option, autocompleteStrategy) {
|
39
|
+
const previousValue = this._value
|
31
40
|
|
32
|
-
|
33
|
-
this.hiddenFieldTarget.value = option.dataset.value
|
34
|
-
option.scrollIntoView({ block: "nearest" })
|
35
|
-
}
|
41
|
+
this._resetOptionsSilently()
|
36
42
|
|
37
|
-
|
43
|
+
autocompleteStrategy(option)
|
44
|
+
|
45
|
+
this._setValue(option.dataset.value)
|
46
|
+
this._markSelected(option)
|
47
|
+
this._markValid()
|
48
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
49
|
+
|
50
|
+
option.scrollIntoView({ block: "nearest" })
|
38
51
|
}
|
39
52
|
|
40
|
-
|
41
|
-
|
42
|
-
option.classList.toggle(this.selectedClass, selected)
|
43
|
-
}
|
53
|
+
_selectNew() {
|
54
|
+
const previousValue = this._value
|
44
55
|
|
45
|
-
|
46
|
-
this.
|
56
|
+
this._resetOptionsSilently()
|
57
|
+
this._setValue(this._fullQuery)
|
58
|
+
this._setName(this.nameWhenNewValue)
|
59
|
+
this._markValid()
|
60
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue })
|
47
61
|
}
|
48
62
|
|
49
63
|
_deselect() {
|
50
|
-
const
|
64
|
+
const previousValue = this._value
|
51
65
|
|
52
|
-
if (
|
66
|
+
if (this._selectedOptionElement) {
|
67
|
+
this._markNotSelected(this._selectedOptionElement)
|
68
|
+
}
|
53
69
|
|
54
|
-
this.
|
70
|
+
this._setValue(null)
|
55
71
|
this._setActiveDescendant("")
|
56
72
|
|
57
|
-
|
73
|
+
return previousValue
|
58
74
|
}
|
59
75
|
|
60
|
-
|
61
|
-
this.
|
62
|
-
this.
|
63
|
-
|
64
|
-
|
76
|
+
_deselectAndNotify() {
|
77
|
+
const previousValue = this._deselect()
|
78
|
+
this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
79
|
+
}
|
80
|
+
|
81
|
+
_forceSelectionAndFilter(option, event) {
|
82
|
+
this._forceSelectionWithoutFiltering(option)
|
83
|
+
this._filter(event)
|
84
|
+
}
|
65
85
|
|
66
|
-
|
86
|
+
_forceSelectionWithoutFiltering(option) {
|
87
|
+
this._selectAndReplaceFullQuery(option)
|
67
88
|
}
|
68
89
|
|
69
90
|
_selectIndex(index) {
|
70
91
|
const option = wrapAroundAccess(this._visibleOptionElements, index)
|
71
|
-
this.
|
92
|
+
this._forceSelectionWithoutFiltering(option)
|
72
93
|
}
|
73
94
|
|
74
95
|
_preselectOption() {
|
75
96
|
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
76
97
|
const option = this._allOptions.find(option => {
|
77
|
-
return option.dataset.value === this.
|
98
|
+
return option.dataset.value === this._value
|
78
99
|
})
|
79
100
|
|
80
|
-
if (option) this._markSelected(option
|
101
|
+
if (option) this._markSelected(option)
|
81
102
|
}
|
82
103
|
}
|
83
104
|
|
105
|
+
_selectAndReplaceFullQuery(option) {
|
106
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this))
|
107
|
+
}
|
108
|
+
|
109
|
+
_selectAndAutocompleteMissingPortion(option) {
|
110
|
+
this._select(option, this._autocompleteMissingPortion.bind(this))
|
111
|
+
}
|
112
|
+
|
84
113
|
_lockInSelection() {
|
85
114
|
if (this._shouldLockInSelection) {
|
86
|
-
this.
|
87
|
-
this.filter({ inputType: "hw:lockInSelection" })
|
115
|
+
this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" })
|
88
116
|
}
|
117
|
+
}
|
89
118
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
119
|
+
_markSelected(option) {
|
120
|
+
if (this.hasSelectedClass) option.classList.add(this.selectedClass)
|
121
|
+
option.setAttribute("aria-selected", true)
|
122
|
+
this._setActiveDescendant(option.id)
|
123
|
+
}
|
124
|
+
|
125
|
+
_markNotSelected(option) {
|
126
|
+
if (this.hasSelectedClass) option.classList.remove(this.selectedClass)
|
127
|
+
option.removeAttribute("aria-selected")
|
128
|
+
this._removeActiveDescendant()
|
94
129
|
}
|
95
130
|
|
96
131
|
_setActiveDescendant(id) {
|
97
132
|
this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id))
|
98
133
|
}
|
99
134
|
|
135
|
+
_removeActiveDescendant() {
|
136
|
+
this._setActiveDescendant("")
|
137
|
+
}
|
138
|
+
|
139
|
+
_setValue(value) {
|
140
|
+
this.hiddenFieldTarget.value = value
|
141
|
+
}
|
142
|
+
|
143
|
+
_setName(value) {
|
144
|
+
this.hiddenFieldTarget.name = value
|
145
|
+
}
|
146
|
+
|
147
|
+
get _value() {
|
148
|
+
return this.hiddenFieldTarget.value
|
149
|
+
}
|
150
|
+
|
100
151
|
get _hasValueButNoSelection() {
|
101
|
-
return this.
|
152
|
+
return this._value && !this._selectedOptionElement
|
102
153
|
}
|
103
154
|
|
104
155
|
get _shouldLockInSelection() {
|
@@ -9,7 +9,10 @@ Combobox.Toggle = Base => class extends Base {
|
|
9
9
|
close() {
|
10
10
|
if (this._isOpen) {
|
11
11
|
this._lockInSelection()
|
12
|
+
this._clearInvalidQuery()
|
13
|
+
|
12
14
|
this.expandedValue = false
|
15
|
+
|
13
16
|
this._dispatchClosedEvent()
|
14
17
|
}
|
15
18
|
}
|
@@ -79,6 +82,8 @@ Combobox.Toggle = Base => class extends Base {
|
|
79
82
|
this._actingCombobox.setAttribute("aria-expanded", true) // needs to happen after setting acting combobox
|
80
83
|
}
|
81
84
|
|
85
|
+
// +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
|
86
|
+
// it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
|
82
87
|
_collapse() {
|
83
88
|
this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
|
84
89
|
|
@@ -119,6 +124,13 @@ Combobox.Toggle = Base => class extends Base {
|
|
119
124
|
enableBodyScroll(this.dialogListboxTarget)
|
120
125
|
}
|
121
126
|
|
127
|
+
_clearInvalidQuery() {
|
128
|
+
if (this._isUnjustifiablyBlank) {
|
129
|
+
this._deselect()
|
130
|
+
this._clearQuery()
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
122
134
|
get _isOpen() {
|
123
135
|
return this.expandedValue
|
124
136
|
}
|
@@ -34,7 +34,7 @@ Combobox.Validity = Base => class extends Base {
|
|
34
34
|
// +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
|
35
35
|
// because the `required` attribute is only forwarded to the `comboboxTarget` element
|
36
36
|
get _valueIsInvalid() {
|
37
|
-
const isRequiredAndEmpty = this.comboboxTarget.required && !this.
|
37
|
+
const isRequiredAndEmpty = this.comboboxTarget.required && !this._value
|
38
38
|
return isRequiredAndEmpty
|
39
39
|
}
|
40
40
|
}
|
@@ -266,7 +266,7 @@ class HotwireCombobox::Component
|
|
266
266
|
combobox_attrs.fetch(:data, {}).merge \
|
267
267
|
action: "
|
268
268
|
focus->hw-combobox#open
|
269
|
-
input->hw-combobox#
|
269
|
+
input->hw-combobox#filterAndSelect
|
270
270
|
keydown->hw-combobox#navigate
|
271
271
|
click@window->hw-combobox#closeOnClickOutside
|
272
272
|
focusin@window->hw-combobox#closeOnFocusOutside
|
@@ -316,7 +316,7 @@ class HotwireCombobox::Component
|
|
316
316
|
def dialog_input_data
|
317
317
|
{
|
318
318
|
action: "
|
319
|
-
input->hw-combobox#
|
319
|
+
input->hw-combobox#filterAndSelect
|
320
320
|
keydown->hw-combobox#navigate
|
321
321
|
click@window->hw-combobox#closeOnClickOutside".squish,
|
322
322
|
hw_combobox_target: "dialogCombobox"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hotwire_combobox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.43
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jose Farias
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|