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
@@ -79,3 +79,19 @@ export function dispatch(eventName, { target, cancelable, detail } = {}) {
|
|
79
79
|
|
80
80
|
return event
|
81
81
|
}
|
82
|
+
|
83
|
+
export function nextRepaint() {
|
84
|
+
if (document.visibilityState === "hidden") {
|
85
|
+
return nextEventLoopTick()
|
86
|
+
} else {
|
87
|
+
return nextAnimationFrame()
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
export function nextAnimationFrame() {
|
92
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()))
|
93
|
+
}
|
94
|
+
|
95
|
+
export function nextEventLoopTick() {
|
96
|
+
return new Promise((resolve) => setTimeout(() => resolve(), 0))
|
97
|
+
}
|
@@ -2,25 +2,35 @@ 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
|
-
|
6
|
-
if (previousValue
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
}
|
11
|
-
}
|
5
|
+
_dispatchPreselectionEvent({ isNewAndAllowed, previousValue }) {
|
6
|
+
if (previousValue === this._incomingFieldValueString) return
|
7
|
+
|
8
|
+
dispatch("hw-combobox:preselection", {
|
9
|
+
target: this.element,
|
10
|
+
detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
|
11
|
+
})
|
12
|
+
}
|
13
|
+
|
14
|
+
_dispatchSelectionEvent() {
|
15
|
+
dispatch("hw-combobox:selection", {
|
16
|
+
target: this.element,
|
17
|
+
detail: this._eventableDetails
|
18
|
+
})
|
12
19
|
}
|
13
20
|
|
14
|
-
|
15
|
-
dispatch("hw-combobox:
|
21
|
+
_dispatchRemovalEvent({ removedDisplay, removedValue }) {
|
22
|
+
dispatch("hw-combobox:removal", {
|
23
|
+
target: this.element,
|
24
|
+
detail: { ...this._eventableDetails, removedDisplay, removedValue }
|
25
|
+
})
|
16
26
|
}
|
17
27
|
|
18
28
|
get _eventableDetails() {
|
19
29
|
return {
|
20
|
-
value: this.
|
30
|
+
value: this._incomingFieldValueString,
|
21
31
|
display: this._fullQuery,
|
22
32
|
query: this._typedQuery,
|
23
|
-
fieldName: this.
|
33
|
+
fieldName: this._fieldName,
|
24
34
|
isValid: this._valueIsValid
|
25
35
|
}
|
26
36
|
}
|
@@ -4,11 +4,11 @@ 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
|
-
filterAndSelect(
|
8
|
-
this._filter(
|
7
|
+
filterAndSelect({ inputType }) {
|
8
|
+
this._filter(inputType)
|
9
9
|
|
10
10
|
if (this._isSync) {
|
11
|
-
this.
|
11
|
+
this._selectOnQuery(inputType)
|
12
12
|
} else {
|
13
13
|
// noop, async selection is handled by stimulus callbacks
|
14
14
|
}
|
@@ -18,24 +18,24 @@ Combobox.Filtering = Base => class extends Base {
|
|
18
18
|
this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
|
19
19
|
}
|
20
20
|
|
21
|
-
_filter(
|
21
|
+
_filter(inputType) {
|
22
22
|
if (this._isAsync) {
|
23
|
-
this._debouncedFilterAsync(
|
23
|
+
this._debouncedFilterAsync(inputType)
|
24
24
|
} else {
|
25
25
|
this._filterSync()
|
26
26
|
}
|
27
27
|
|
28
|
-
this.
|
28
|
+
this._markQueried()
|
29
29
|
}
|
30
30
|
|
31
|
-
_debouncedFilterAsync(
|
32
|
-
this._filterAsync(
|
31
|
+
_debouncedFilterAsync(inputType) {
|
32
|
+
this._filterAsync(inputType)
|
33
33
|
}
|
34
34
|
|
35
|
-
async _filterAsync(
|
35
|
+
async _filterAsync(inputType) {
|
36
36
|
const query = {
|
37
37
|
q: this._fullQuery,
|
38
|
-
input_type:
|
38
|
+
input_type: inputType,
|
39
39
|
for_id: this.element.dataset.asyncId
|
40
40
|
}
|
41
41
|
|
@@ -43,8 +43,12 @@ Combobox.Filtering = Base => class extends Base {
|
|
43
43
|
}
|
44
44
|
|
45
45
|
_filterSync() {
|
46
|
-
this.
|
47
|
-
|
46
|
+
this._allFilterableOptionElements.forEach(
|
47
|
+
applyFilter(
|
48
|
+
this._fullQuery,
|
49
|
+
{ matching: this.filterableAttributeValue }
|
50
|
+
)
|
51
|
+
)
|
48
52
|
}
|
49
53
|
|
50
54
|
_clearQuery() {
|
@@ -52,6 +56,10 @@ Combobox.Filtering = Base => class extends Base {
|
|
52
56
|
this.filterAndSelect({ inputType: "deleteContentBackward" })
|
53
57
|
}
|
54
58
|
|
59
|
+
_markQueried() {
|
60
|
+
this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
|
61
|
+
}
|
62
|
+
|
55
63
|
get _isQueried() {
|
56
64
|
return this._fullQuery.length > 0
|
57
65
|
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import Combobox from "hw_combobox/models/combobox/base"
|
2
|
+
|
3
|
+
Combobox.FormField = Base => class extends Base {
|
4
|
+
get _fieldValue() {
|
5
|
+
if (this._isMultiselect) {
|
6
|
+
const currentValue = this.hiddenFieldTarget.value
|
7
|
+
const arrayFromValue = currentValue ? currentValue.split(",") : []
|
8
|
+
|
9
|
+
return new Set(arrayFromValue)
|
10
|
+
} else {
|
11
|
+
return this.hiddenFieldTarget.value
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
get _fieldValueString() {
|
16
|
+
if (this._isMultiselect) {
|
17
|
+
return this._fieldValueArray.join(",")
|
18
|
+
} else {
|
19
|
+
return this.hiddenFieldTarget.value
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
get _incomingFieldValueString() {
|
24
|
+
if (this._isMultiselect) {
|
25
|
+
const array = this._fieldValueArray
|
26
|
+
|
27
|
+
if (this.hiddenFieldTarget.dataset.valueForMultiselect) {
|
28
|
+
array.push(this.hiddenFieldTarget.dataset.valueForMultiselect)
|
29
|
+
}
|
30
|
+
|
31
|
+
return array.join(",")
|
32
|
+
} else {
|
33
|
+
return this.hiddenFieldTarget.value
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
get _fieldValueArray() {
|
38
|
+
if (this._isMultiselect) {
|
39
|
+
return Array.from(this._fieldValue)
|
40
|
+
} else {
|
41
|
+
return [ this.hiddenFieldTarget.value ]
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
set _fieldValue(value) {
|
46
|
+
if (this._isMultiselect) {
|
47
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect = value?.replace(/,/g, "")
|
48
|
+
this.hiddenFieldTarget.dataset.displayForMultiselect = this._fullQuery
|
49
|
+
} else {
|
50
|
+
this.hiddenFieldTarget.value = value
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
get _hasEmptyFieldValue() {
|
55
|
+
if (this._isMultiselect) {
|
56
|
+
return this.hiddenFieldTarget.dataset.valueForMultiselect == "" ||
|
57
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect == "undefined"
|
58
|
+
} else {
|
59
|
+
return this.hiddenFieldTarget.value === ""
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
get _hasFieldValue() {
|
64
|
+
return !this._hasEmptyFieldValue
|
65
|
+
}
|
66
|
+
|
67
|
+
get _fieldName() {
|
68
|
+
return this.hiddenFieldTarget.name
|
69
|
+
}
|
70
|
+
|
71
|
+
set _fieldName(value) {
|
72
|
+
this.hiddenFieldTarget.name = value
|
73
|
+
}
|
74
|
+
}
|
@@ -0,0 +1,160 @@
|
|
1
|
+
import Combobox from "hw_combobox/models/combobox/base"
|
2
|
+
import { cancel, nextRepaint } from "hw_combobox/helpers"
|
3
|
+
import { get } from "hw_combobox/vendor/requestjs"
|
4
|
+
|
5
|
+
Combobox.Multiselect = Base => class extends Base {
|
6
|
+
navigateChip(event) {
|
7
|
+
this._chipKeyHandlers[event.key]?.call(this, event)
|
8
|
+
}
|
9
|
+
|
10
|
+
removeChip({ currentTarget, params }) {
|
11
|
+
let display
|
12
|
+
const option = this._optionElementWithValue(params.value)
|
13
|
+
|
14
|
+
if (option) {
|
15
|
+
display = option.getAttribute(this.autocompletableAttributeValue)
|
16
|
+
this._markNotSelected(option)
|
17
|
+
this._markNotMultiselected(option)
|
18
|
+
} else {
|
19
|
+
display = params.value // for new options
|
20
|
+
}
|
21
|
+
|
22
|
+
this._removeFromFieldValue(params.value)
|
23
|
+
this._filter("hw:multiselectSync")
|
24
|
+
|
25
|
+
currentTarget.closest("[data-hw-combobox-chip]").remove()
|
26
|
+
|
27
|
+
if (!this._isSmallViewport) {
|
28
|
+
this.openByFocusing()
|
29
|
+
}
|
30
|
+
|
31
|
+
this._announceToScreenReader(display, "removed")
|
32
|
+
this._dispatchRemovalEvent({ removedDisplay: display, removedValue: params.value })
|
33
|
+
}
|
34
|
+
|
35
|
+
hideChipsForCache() {
|
36
|
+
this.element.querySelectorAll("[data-hw-combobox-chip]").forEach(chip => chip.hidden = true)
|
37
|
+
}
|
38
|
+
|
39
|
+
_chipKeyHandlers = {
|
40
|
+
Backspace: (event) => {
|
41
|
+
this.removeChip(event)
|
42
|
+
cancel(event)
|
43
|
+
},
|
44
|
+
Enter: (event) => {
|
45
|
+
this.removeChip(event)
|
46
|
+
cancel(event)
|
47
|
+
},
|
48
|
+
Space: (event) => {
|
49
|
+
this.removeChip(event)
|
50
|
+
cancel(event)
|
51
|
+
},
|
52
|
+
Escape: (event) => {
|
53
|
+
this.openByFocusing()
|
54
|
+
cancel(event)
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
_initializeMultiselect() {
|
59
|
+
if (!this._isMultiPreselected) {
|
60
|
+
this._preselectMultiple()
|
61
|
+
this._markMultiPreselected()
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
async _createChip(shouldReopen) {
|
66
|
+
if (!this._isMultiselect) return
|
67
|
+
|
68
|
+
this._beforeClearingMultiselectQuery(async (display, value) => {
|
69
|
+
this._fullQuery = ""
|
70
|
+
this._filter("hw:multiselectSync")
|
71
|
+
this._requestChips(value)
|
72
|
+
this._addToFieldValue(value)
|
73
|
+
if (shouldReopen) {
|
74
|
+
await nextRepaint()
|
75
|
+
this.openByFocusing()
|
76
|
+
}
|
77
|
+
this._announceToScreenReader(display, "multi-selected. Press Shift + Tab, then Enter to remove.")
|
78
|
+
})
|
79
|
+
}
|
80
|
+
|
81
|
+
async _requestChips(values) {
|
82
|
+
await get(this.selectionChipSrcValue, {
|
83
|
+
responseKind: "turbo-stream",
|
84
|
+
query: {
|
85
|
+
for_id: this.element.dataset.asyncId,
|
86
|
+
combobox_values: values
|
87
|
+
}
|
88
|
+
})
|
89
|
+
}
|
90
|
+
|
91
|
+
_beforeClearingMultiselectQuery(callback) {
|
92
|
+
const display = this.hiddenFieldTarget.dataset.displayForMultiselect
|
93
|
+
const value = this.hiddenFieldTarget.dataset.valueForMultiselect
|
94
|
+
|
95
|
+
if (value && !this._fieldValue.has(value)) {
|
96
|
+
callback(display, value)
|
97
|
+
}
|
98
|
+
|
99
|
+
this.hiddenFieldTarget.dataset.displayForMultiselect = ""
|
100
|
+
this.hiddenFieldTarget.dataset.valueForMultiselect = ""
|
101
|
+
}
|
102
|
+
|
103
|
+
_resetMultiselectionMarks() {
|
104
|
+
if (!this._isMultiselect) return
|
105
|
+
|
106
|
+
this._fieldValueArray.forEach(value => {
|
107
|
+
const option = this._optionElementWithValue(value)
|
108
|
+
|
109
|
+
if (option) {
|
110
|
+
option.setAttribute("data-multiselected", "")
|
111
|
+
option.hidden = true
|
112
|
+
}
|
113
|
+
})
|
114
|
+
}
|
115
|
+
|
116
|
+
_markNotMultiselected(option) {
|
117
|
+
if (!this._isMultiselect) return
|
118
|
+
|
119
|
+
option.removeAttribute("data-multiselected")
|
120
|
+
option.hidden = false
|
121
|
+
}
|
122
|
+
|
123
|
+
_addToFieldValue(value) {
|
124
|
+
const newValue = this._fieldValue
|
125
|
+
|
126
|
+
newValue.add(String(value))
|
127
|
+
this.hiddenFieldTarget.value = Array.from(newValue).join(",")
|
128
|
+
|
129
|
+
if (this._isSync) this._resetMultiselectionMarks()
|
130
|
+
}
|
131
|
+
|
132
|
+
_removeFromFieldValue(value) {
|
133
|
+
const newValue = this._fieldValue
|
134
|
+
|
135
|
+
newValue.delete(String(value))
|
136
|
+
this.hiddenFieldTarget.value = Array.from(newValue).join(",")
|
137
|
+
|
138
|
+
if (this._isSync) this._resetMultiselectionMarks()
|
139
|
+
}
|
140
|
+
|
141
|
+
_focusLastChipDismisser() {
|
142
|
+
this.chipDismisserTargets[this.chipDismisserTargets.length - 1]?.focus()
|
143
|
+
}
|
144
|
+
|
145
|
+
_markMultiPreselected() {
|
146
|
+
this.element.dataset.multiPreselected = ""
|
147
|
+
}
|
148
|
+
|
149
|
+
get _isMultiselect() {
|
150
|
+
return this.hasSelectionChipSrcValue
|
151
|
+
}
|
152
|
+
|
153
|
+
get _isSingleSelect() {
|
154
|
+
return !this._isMultiselect
|
155
|
+
}
|
156
|
+
|
157
|
+
get _isMultiPreselected() {
|
158
|
+
return this.element.hasAttribute("data-multi-preselected")
|
159
|
+
}
|
160
|
+
}
|
@@ -4,17 +4,22 @@ import { cancel } from "hw_combobox/helpers"
|
|
4
4
|
Combobox.Navigation = Base => class extends Base {
|
5
5
|
navigate(event) {
|
6
6
|
if (this._autocompletesList) {
|
7
|
-
this.
|
7
|
+
this._navigationKeyHandlers[event.key]?.call(this, event)
|
8
8
|
}
|
9
9
|
}
|
10
10
|
|
11
|
-
|
11
|
+
_navigationKeyHandlers = {
|
12
12
|
ArrowUp: (event) => {
|
13
13
|
this._selectIndex(this._selectedOptionIndex - 1)
|
14
14
|
cancel(event)
|
15
15
|
},
|
16
16
|
ArrowDown: (event) => {
|
17
17
|
this._selectIndex(this._selectedOptionIndex + 1)
|
18
|
+
|
19
|
+
if (this._selectedOptionIndex === 0) {
|
20
|
+
this._actingListbox.scrollTop = 0
|
21
|
+
}
|
22
|
+
|
18
23
|
cancel(event)
|
19
24
|
},
|
20
25
|
Home: (event) => {
|
@@ -26,14 +31,18 @@ Combobox.Navigation = Base => class extends Base {
|
|
26
31
|
cancel(event)
|
27
32
|
},
|
28
33
|
Enter: (event) => {
|
29
|
-
this.
|
30
|
-
this._actingCombobox.blur()
|
34
|
+
this._closeAndBlur("hw:keyHandler:enter")
|
31
35
|
cancel(event)
|
32
36
|
},
|
33
37
|
Escape: (event) => {
|
34
|
-
this.
|
35
|
-
this._actingCombobox.blur()
|
38
|
+
this._closeAndBlur("hw:keyHandler:escape")
|
36
39
|
cancel(event)
|
40
|
+
},
|
41
|
+
Backspace: (event) => {
|
42
|
+
if (this._isMultiselect && !this._fullQuery) {
|
43
|
+
this._focusLastChipDismisser()
|
44
|
+
cancel(event)
|
45
|
+
}
|
37
46
|
}
|
38
47
|
}
|
39
48
|
}
|
@@ -11,28 +11,40 @@ Combobox.Options = Base => class extends Base {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
_resetOptions(deselectionStrategy) {
|
14
|
-
this.
|
14
|
+
this._fieldName = this.originalNameValue
|
15
15
|
deselectionStrategy()
|
16
16
|
}
|
17
17
|
|
18
|
+
_optionElementWithValue(value) {
|
19
|
+
return this._actingListbox.querySelector(`[${this.filterableAttributeValue}][data-value='${value}']`)
|
20
|
+
}
|
21
|
+
|
22
|
+
_displayForOptionElement(element) {
|
23
|
+
return element.getAttribute(this.autocompletableAttributeValue)
|
24
|
+
}
|
25
|
+
|
18
26
|
get _allowNew() {
|
19
27
|
return !!this.nameWhenNewValue
|
20
28
|
}
|
21
29
|
|
22
30
|
get _allOptions() {
|
23
|
-
return Array.from(this.
|
31
|
+
return Array.from(this._allFilterableOptionElements)
|
24
32
|
}
|
25
33
|
|
26
|
-
get
|
27
|
-
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]`)
|
34
|
+
get _allFilterableOptionElements() {
|
35
|
+
return this._actingListbox.querySelectorAll(`[${this.filterableAttributeValue}]:not([data-multiselected])`)
|
28
36
|
}
|
29
37
|
|
30
38
|
get _visibleOptionElements() {
|
31
|
-
return [ ...this.
|
39
|
+
return [ ...this._allFilterableOptionElements ].filter(visible)
|
32
40
|
}
|
33
41
|
|
34
42
|
get _selectedOptionElement() {
|
35
|
-
return this._actingListbox.querySelector("[role=option][aria-selected=true]")
|
43
|
+
return this._actingListbox.querySelector("[role=option][aria-selected=true]:not([data-multiselected])")
|
44
|
+
}
|
45
|
+
|
46
|
+
get _multiselectedOptionElements() {
|
47
|
+
return this._actingListbox.querySelectorAll("[role=option][data-multiselected]")
|
36
48
|
}
|
37
49
|
|
38
50
|
get _selectedOptionIndex() {
|
@@ -40,7 +52,7 @@ Combobox.Options = Base => class extends Base {
|
|
40
52
|
}
|
41
53
|
|
42
54
|
get _isUnjustifiablyBlank() {
|
43
|
-
const valueIsMissing =
|
55
|
+
const valueIsMissing = this._hasEmptyFieldValue
|
44
56
|
const noBlankOptionSelected = !this._selectedOptionElement
|
45
57
|
|
46
58
|
return valueIsMissing && noBlankOptionSelected
|
@@ -2,23 +2,24 @@ import Combobox from "hw_combobox/models/combobox/base"
|
|
2
2
|
import { wrapAroundAccess, isDeleteEvent } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Selection = Base => class extends Base {
|
5
|
-
|
6
|
-
this._forceSelectionAndFilter(
|
7
|
-
this.
|
5
|
+
selectOnClick({ currentTarget, inputType }) {
|
6
|
+
this._forceSelectionAndFilter(currentTarget, inputType)
|
7
|
+
this._closeAndBlur("hw:optionRoleClick")
|
8
8
|
}
|
9
9
|
|
10
10
|
_connectSelection() {
|
11
11
|
if (this.hasPrefilledDisplayValue) {
|
12
12
|
this._fullQuery = this.prefilledDisplayValue
|
13
|
+
this._markQueried()
|
13
14
|
}
|
14
15
|
}
|
15
16
|
|
16
|
-
|
17
|
-
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(
|
17
|
+
_selectOnQuery(inputType) {
|
18
|
+
if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent({ inputType: inputType }))) {
|
18
19
|
this._selectNew()
|
19
|
-
} else if (isDeleteEvent(
|
20
|
+
} else if (isDeleteEvent({ inputType: inputType })) {
|
20
21
|
this._deselect()
|
21
|
-
} else if (
|
22
|
+
} else if (inputType === "hw:lockInSelection" && this._ensurableOption) {
|
22
23
|
this._selectAndAutocompleteMissingPortion(this._ensurableOption)
|
23
24
|
} else if (this._isOpen && this._visibleOptionElements[0]) {
|
24
25
|
this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0])
|
@@ -29,45 +30,45 @@ Combobox.Selection = Base => class extends Base {
|
|
29
30
|
// When selecting from an async dialog listbox: selection is forced, the listbox is filtered,
|
30
31
|
// and the dialog is closed. Filtering ends with an `endOfOptionsStream` target connected
|
31
32
|
// to the now invisible combobox, which is now closed because Turbo waits for "nextRepaint"
|
32
|
-
// before rendering turbo streams. This ultimately calls +
|
33
|
-
// to call +
|
33
|
+
// before rendering turbo streams. This ultimately calls +_selectOnQuery+. We do want
|
34
|
+
// to call +_selectOnQuery+ in this case to account for e.g. selection of
|
34
35
|
// new options. But we will noop here if it's none of the cases checked above.
|
35
36
|
}
|
36
37
|
}
|
37
38
|
|
38
39
|
_select(option, autocompleteStrategy) {
|
39
|
-
const previousValue = this.
|
40
|
+
const previousValue = this._fieldValueString
|
40
41
|
|
41
42
|
this._resetOptionsSilently()
|
42
43
|
|
43
44
|
autocompleteStrategy(option)
|
44
45
|
|
45
|
-
this.
|
46
|
+
this._fieldValue = option.dataset.value
|
46
47
|
this._markSelected(option)
|
47
48
|
this._markValid()
|
48
|
-
this.
|
49
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
49
50
|
|
50
51
|
option.scrollIntoView({ block: "nearest" })
|
51
52
|
}
|
52
53
|
|
53
54
|
_selectNew() {
|
54
|
-
const previousValue = this.
|
55
|
+
const previousValue = this._fieldValueString
|
55
56
|
|
56
57
|
this._resetOptionsSilently()
|
57
|
-
this.
|
58
|
-
this.
|
58
|
+
this._fieldValue = this._fullQuery
|
59
|
+
this._fieldName = this.nameWhenNewValue
|
59
60
|
this._markValid()
|
60
|
-
this.
|
61
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: true, previousValue: previousValue })
|
61
62
|
}
|
62
63
|
|
63
64
|
_deselect() {
|
64
|
-
const previousValue = this.
|
65
|
+
const previousValue = this._fieldValueString
|
65
66
|
|
66
67
|
if (this._selectedOptionElement) {
|
67
68
|
this._markNotSelected(this._selectedOptionElement)
|
68
69
|
}
|
69
70
|
|
70
|
-
this.
|
71
|
+
this._fieldValue = ""
|
71
72
|
this._setActiveDescendant("")
|
72
73
|
|
73
74
|
return previousValue
|
@@ -75,16 +76,7 @@ Combobox.Selection = Base => class extends Base {
|
|
75
76
|
|
76
77
|
_deselectAndNotify() {
|
77
78
|
const previousValue = this._deselect()
|
78
|
-
this.
|
79
|
-
}
|
80
|
-
|
81
|
-
_forceSelectionAndFilter(option, event) {
|
82
|
-
this._forceSelectionWithoutFiltering(option)
|
83
|
-
this._filter(event)
|
84
|
-
}
|
85
|
-
|
86
|
-
_forceSelectionWithoutFiltering(option) {
|
87
|
-
this._selectAndReplaceFullQuery(option)
|
79
|
+
this._dispatchPreselectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
|
88
80
|
}
|
89
81
|
|
90
82
|
_selectIndex(index) {
|
@@ -92,27 +84,40 @@ Combobox.Selection = Base => class extends Base {
|
|
92
84
|
this._forceSelectionWithoutFiltering(option)
|
93
85
|
}
|
94
86
|
|
95
|
-
|
96
|
-
if (this._hasValueButNoSelection && this._allOptions.length < 100) {
|
97
|
-
const option = this.
|
98
|
-
return option.dataset.value === this._value
|
99
|
-
})
|
100
|
-
|
87
|
+
_preselectSingle() {
|
88
|
+
if (this._isSingleSelect && this._hasValueButNoSelection && this._allOptions.length < 100) {
|
89
|
+
const option = this._optionElementWithValue(this._fieldValue)
|
101
90
|
if (option) this._markSelected(option)
|
102
91
|
}
|
103
92
|
}
|
104
93
|
|
105
|
-
|
106
|
-
this.
|
94
|
+
_preselectMultiple() {
|
95
|
+
if (this._isMultiselect && this._hasValueButNoSelection) {
|
96
|
+
this._requestChips(this._fieldValueString)
|
97
|
+
this._resetMultiselectionMarks()
|
98
|
+
}
|
107
99
|
}
|
108
100
|
|
109
101
|
_selectAndAutocompleteMissingPortion(option) {
|
110
102
|
this._select(option, this._autocompleteMissingPortion.bind(this))
|
111
103
|
}
|
112
104
|
|
105
|
+
_selectAndAutocompleteFullQuery(option) {
|
106
|
+
this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this))
|
107
|
+
}
|
108
|
+
|
109
|
+
_forceSelectionAndFilter(option, inputType) {
|
110
|
+
this._forceSelectionWithoutFiltering(option)
|
111
|
+
this._filter(inputType)
|
112
|
+
}
|
113
|
+
|
114
|
+
_forceSelectionWithoutFiltering(option) {
|
115
|
+
this._selectAndAutocompleteFullQuery(option)
|
116
|
+
}
|
117
|
+
|
113
118
|
_lockInSelection() {
|
114
119
|
if (this._shouldLockInSelection) {
|
115
|
-
this._forceSelectionAndFilter(this._ensurableOption,
|
120
|
+
this._forceSelectionAndFilter(this._ensurableOption, "hw:lockInSelection")
|
116
121
|
}
|
117
122
|
}
|
118
123
|
|
@@ -136,20 +141,16 @@ Combobox.Selection = Base => class extends Base {
|
|
136
141
|
this._setActiveDescendant("")
|
137
142
|
}
|
138
143
|
|
139
|
-
|
140
|
-
this.
|
141
|
-
}
|
142
|
-
|
143
|
-
_setName(value) {
|
144
|
-
this.hiddenFieldTarget.name = value
|
145
|
-
}
|
146
|
-
|
147
|
-
get _value() {
|
148
|
-
return this.hiddenFieldTarget.value
|
144
|
+
get _hasValueButNoSelection() {
|
145
|
+
return this._hasFieldValue && !this._hasSelection
|
149
146
|
}
|
150
147
|
|
151
|
-
get
|
152
|
-
|
148
|
+
get _hasSelection() {
|
149
|
+
if (this._isSingleSelect) {
|
150
|
+
this._selectedOptionElement
|
151
|
+
} else {
|
152
|
+
this._multiselectedOptionElements.length > 0
|
153
|
+
}
|
153
154
|
}
|
154
155
|
|
155
156
|
get _shouldLockInSelection() {
|