advanced_select 0.1.10 → 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 +110 -2
- data/app/javascript/advanced_select/advanced_select_controller.js +115 -4
- data/app/views/advanced_select/_select.html.erb +4 -0
- data/lib/advanced_select/version.rb +1 -1
- data/lib/generators/advanced_select/install/templates/advanced_select_controller.js +115 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be334c14eaabfa7e2d3685deb4f1404b7187cb51c94d8a8dcb17b17e70bc5435
|
|
4
|
+
data.tar.gz: b9a676e2e4a64345631612285e8a362034b4efaaf60650cd201eadb2fa41b5a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be666dac14558aa5437cdf536c4bac3e8b392c61694904794a9d935de038152d6852c19e1d537a833f80836ae1101671890b707ea17732773905f185a81bc007
|
|
7
|
+
data.tar.gz: 5ac9dd843c6e837480dbc2fd6a00ecb6013e150a4c278e46ad75dafa1d8476973ac3e081125cd5ef678f5ba5fa5c6d2eb54676fc30fa258c870cd72833aa8a0d
|
data/README.md
CHANGED
|
@@ -23,6 +23,8 @@ AdvancedSelect is a small Rails engine for rendering an advanced select input wi
|
|
|
23
23
|
- [Custom Option Content](#custom-option-content)
|
|
24
24
|
- [Option Contract](#option-contract)
|
|
25
25
|
- [Events](#events)
|
|
26
|
+
- [Reading And Writing The Value](#reading-and-writing-the-value)
|
|
27
|
+
- [Driving The Option List](#driving-the-option-list)
|
|
26
28
|
- [Disabled State](#disabled-state)
|
|
27
29
|
- [API Reference](#api-reference)
|
|
28
30
|
- [Local Development](#local-development)
|
|
@@ -205,6 +207,30 @@ application.register("advanced-select", AdvancedSelectController)
|
|
|
205
207
|
|
|
206
208
|
This keeps local custom behavior small while allowing future gem fixes to flow through the base controller.
|
|
207
209
|
|
|
210
|
+
Two methods exist as extension points for host apps whose options endpoint does not speak Turbo
|
|
211
|
+
Streams. `readOptionsResponse(response)` turns the `fetch` response into a payload and defaults to
|
|
212
|
+
`response.text()`; `renderOptionsResponse(payload)` puts that payload on the page and defaults to
|
|
213
|
+
`Turbo.renderStreamMessage(payload)`. Overriding them keeps the request sequencing, the stale
|
|
214
|
+
response guard, and the post-render bookkeeping in the base controller:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
export default class extends AdvancedSelectController {
|
|
218
|
+
readOptionsResponse(response) {
|
|
219
|
+
return response.headers.get("content-type")?.includes("application/json")
|
|
220
|
+
? response.json()
|
|
221
|
+
: super.readOptionsResponse(response)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
renderOptionsResponse(payload) {
|
|
225
|
+
if (Array.isArray(payload)) {
|
|
226
|
+
this.replaceOptions(payload)
|
|
227
|
+
} else {
|
|
228
|
+
super.renderOptionsResponse(payload)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
208
234
|
For `jsbundling-rails` and other bundlers, the installer copies the full controller because bundlers do not resolve Rails engine JavaScript assets automatically. In that setup the copied file is host-owned.
|
|
209
235
|
|
|
210
236
|
### jsbundling/Propshaft Example
|
|
@@ -756,6 +782,22 @@ Every value change dispatches two events:
|
|
|
756
782
|
| `change` | the first hidden input | native form behaviour and [dependent fields](#dependent-fields) |
|
|
757
783
|
| `advanced-select:change` | the root element | application listeners that need the whole selection |
|
|
758
784
|
|
|
785
|
+
A remote field also announces when its options finish rendering:
|
|
786
|
+
|
|
787
|
+
| Event | Target | Purpose |
|
|
788
|
+
| --- | --- | --- |
|
|
789
|
+
| `advanced-select:options-loaded` | the root element | application listeners that react to the option list itself |
|
|
790
|
+
|
|
791
|
+
It fires after every successful [remote](#remote-search) load — including eager
|
|
792
|
+
[dependent](#dependent-fields) loads — once the options are in the DOM and any single-option
|
|
793
|
+
auto-selection has been applied. Its `detail` carries the field `name`, the resulting `value`, and
|
|
794
|
+
the `count` of selectable options, which is enough to react to an empty or single-option result
|
|
795
|
+
without reading the DOM:
|
|
796
|
+
|
|
797
|
+
```erb
|
|
798
|
+
data-action="advanced-select:options-loaded->my-controller#optionsChanged"
|
|
799
|
+
```
|
|
800
|
+
|
|
759
801
|
Both bubble. `advanced-select:change` carries a `detail` payload:
|
|
760
802
|
|
|
761
803
|
```js
|
|
@@ -784,14 +826,70 @@ Prefer it over `change` on multiple selects. `change` is dispatched from the fir
|
|
|
784
826
|
it does not fire at all once the last value is cleared from a field rendered with
|
|
785
827
|
`include_hidden: false` — at that point the field has no inputs left.
|
|
786
828
|
|
|
787
|
-
The current value is also readable from the controller
|
|
829
|
+
The current value is also readable from the controller — see [Reading And Writing The Value](#reading-and-writing-the-value).
|
|
830
|
+
|
|
831
|
+
### Reading And Writing The Value
|
|
832
|
+
|
|
833
|
+
The controller exposes the selection as a small programmatic API, so host code can drive a field
|
|
834
|
+
without reaching into its markup:
|
|
788
835
|
|
|
789
836
|
```js
|
|
790
837
|
const select = application.getControllerForElementAndIdentifier(element, "advanced-select")
|
|
791
838
|
|
|
792
|
-
select.
|
|
839
|
+
select.getValue() // => "7", or ["3", "7"] on a multiple select
|
|
840
|
+
select.setValue("7")
|
|
841
|
+
select.setValue(["3", "7"]) // multiple selects also accept a single value
|
|
842
|
+
select.setValue("7", { silent: true }) // assign without broadcasting a change
|
|
843
|
+
select.refresh() // re-render from the current selection
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
`getValue` returns the submit value — the option's `value` when it defines one, otherwise its `id`.
|
|
847
|
+
It is a string for single selects, an array for multiple selects, and empty when nothing is
|
|
848
|
+
selected.
|
|
849
|
+
|
|
850
|
+
`setValue` resolves each value against the options currently in the list, matching either the
|
|
851
|
+
option's id or its submit value, so `setValue("identity-7")` and `setValue("submit-7")` select the
|
|
852
|
+
same row. A value that matches no option is still assigned and submitted, using the value itself as
|
|
853
|
+
the label — this keeps a server-assigned value from being silently dropped. Passing `""`, `null`, or
|
|
854
|
+
`[]` clears the field. On a single select only the first value of an array is kept.
|
|
855
|
+
|
|
856
|
+
`silent: true` suppresses the `change` and `advanced-select:change` events for that one assignment.
|
|
857
|
+
Use it when the value is being restored rather than chosen — replacing options after a Turbo Stream
|
|
858
|
+
update, for example — so dependent fields do not cascade off a change the user did not make. The
|
|
859
|
+
suppression lasts for a single render; the next assignment broadcasts normally.
|
|
860
|
+
|
|
861
|
+
### Driving The Option List
|
|
862
|
+
|
|
863
|
+
The option list can be driven from the browser as well, for host apps that load options from
|
|
864
|
+
somewhere the Turbo Stream flow does not cover:
|
|
865
|
+
|
|
866
|
+
```js
|
|
867
|
+
select.appendOption({ id: "9", label: "Item C" })
|
|
868
|
+
|
|
869
|
+
select.replaceOptions([
|
|
870
|
+
{ id: "3", label: "Item A" },
|
|
871
|
+
{ id: "7", label: "Item B" }
|
|
872
|
+
])
|
|
873
|
+
|
|
874
|
+
select.replaceOptions(options, { selected: "7" }) // replace the list and set the value
|
|
875
|
+
select.replaceOptions(options, { selected: null }) // replace the list and clear the value
|
|
876
|
+
select.replaceOptions(options, { silent: false }) // broadcast the resulting value
|
|
793
877
|
```
|
|
794
878
|
|
|
879
|
+
Both take options in the [option contract](#option-contract) shape and render them through
|
|
880
|
+
`optionElement`, so they carry the host's class map.
|
|
881
|
+
|
|
882
|
+
`appendOption` adds one option to the end of the list and does nothing if an option with that id is
|
|
883
|
+
already there. It does not select the option — that is `addOption`, which exists for
|
|
884
|
+
[Add Mode](#add-mode), where the user creates a value that was not in the list.
|
|
885
|
+
|
|
886
|
+
`replaceOptions` swaps the whole list. Leaving `selected` out keeps the current selection, even when
|
|
887
|
+
the new list no longer contains it — the common case after a search, where the selected record may
|
|
888
|
+
have fallen outside the page of results. Passing `selected` assigns it with `setValue` semantics.
|
|
889
|
+
|
|
890
|
+
Unlike `setValue`, `replaceOptions` is silent by default: replacing a list is not a value change, so
|
|
891
|
+
it should not cascade to dependent fields on its own. Pass `silent: false` when it should.
|
|
892
|
+
|
|
795
893
|
### Disabled State
|
|
796
894
|
|
|
797
895
|
Pass `disabled: true` to lock a field. It still renders its current selection —
|
|
@@ -1013,6 +1111,16 @@ Tailwind content scanning can usually see class strings when they are written li
|
|
|
1013
1111
|
|
|
1014
1112
|
The host app can still load the gem CSS through `application.css`. `classes:` entries replace the mapped default classes for that helper call; unmapped keys keep the gem defaults. `append_classes:` entries keep the resolved class and append host classes after it.
|
|
1015
1113
|
|
|
1114
|
+
The resolved `option`, `option_check`, `option_content`, and `option_description` classes are also published on the root element as data attributes, so options the controller builds in the browser carry the same classes as the ones the server renders. `optionElement(option)` returns such an element:
|
|
1115
|
+
|
|
1116
|
+
```js
|
|
1117
|
+
const select = application.getControllerForElementAndIdentifier(element, "advanced-select")
|
|
1118
|
+
|
|
1119
|
+
select.currentOptionsTarget.appendChild(select.optionElement({ id: "7", label: "Item B" }))
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
It produces the same markup as `advanced_select_option_tag`: the option contract's `id`, `value`, `label`, `display_label`, and `description` are all honored, and the resulting element is fully interactive — hovering activates it and clicking selects it.
|
|
1123
|
+
|
|
1016
1124
|
### CSS Overrides
|
|
1017
1125
|
|
|
1018
1126
|
Importmap/Sprockets host applications can put app-specific styling in a host-owned file such as:
|
|
@@ -30,6 +30,7 @@ export default class extends Controller {
|
|
|
30
30
|
this.timer = null
|
|
31
31
|
this.requestSequence = 0
|
|
32
32
|
this.activeIndex = -1
|
|
33
|
+
this.suppressChange = false
|
|
33
34
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
34
35
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
35
36
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
@@ -40,6 +41,10 @@ export default class extends Controller {
|
|
|
40
41
|
this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
|
|
41
42
|
this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
|
|
42
43
|
this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
|
|
44
|
+
this.optionClass = this.element.dataset.advancedSelectOptionClass || "ui-advanced-select-option"
|
|
45
|
+
this.optionCheckClass = this.element.dataset.advancedSelectOptionCheckClass || "ui-advanced-select-option-check"
|
|
46
|
+
this.optionContentClass = this.element.dataset.advancedSelectOptionContentClass || "ui-advanced-select-option-content"
|
|
47
|
+
this.optionDescriptionClass = this.element.dataset.advancedSelectOptionDescriptionClass || "ui-advanced-select-option-description"
|
|
43
48
|
this.selectedValue = this.selectedValue.map((option) => this.normalizeSelectedOption(option))
|
|
44
49
|
this.close = this.close.bind(this)
|
|
45
50
|
this.renderOptionsState()
|
|
@@ -164,6 +169,55 @@ export default class extends Controller {
|
|
|
164
169
|
this.close()
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
getValue() {
|
|
173
|
+
return this.currentValue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setValue(value, { silent = false } = {}) {
|
|
177
|
+
const requested = (Array.isArray(value) ? value : [value])
|
|
178
|
+
.filter((item) => item !== "" && item != null)
|
|
179
|
+
.map(String)
|
|
180
|
+
const values = this.multipleValue ? requested : requested.slice(0, 1)
|
|
181
|
+
const options = this.optionElements.map((element) => this.optionData(element))
|
|
182
|
+
|
|
183
|
+
this.selectedValue = values.map((item) => {
|
|
184
|
+
const option = options.find((candidate) => candidate.id === item || candidate.value === item)
|
|
185
|
+
|
|
186
|
+
return this.normalizeSelectedOption(option || { id: item, value: item, label: item })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
this.suppressChange = silent
|
|
190
|
+
this.renderSelection()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
appendOption(option) {
|
|
194
|
+
const normalized = this.normalizeSelectedOption(option)
|
|
195
|
+
const present = this.optionElements.some((element) => element.dataset.advancedSelectValueParam === normalized.id)
|
|
196
|
+
|
|
197
|
+
if (present) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.currentOptionsTarget.appendChild(this.optionElement(normalized))
|
|
202
|
+
this.renderOptionsState()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
replaceOptions(options, { selected, silent = true } = {}) {
|
|
206
|
+
this.currentOptionsTarget.replaceChildren(...options.map((option) => this.optionElement(option)))
|
|
207
|
+
|
|
208
|
+
if (selected === undefined) {
|
|
209
|
+
this.suppressChange = silent
|
|
210
|
+
this.renderSelection()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.setValue(selected, { silent })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
refresh() {
|
|
218
|
+
this.renderSelection()
|
|
219
|
+
}
|
|
220
|
+
|
|
167
221
|
keydown(event) {
|
|
168
222
|
if (event.key === "ArrowDown") {
|
|
169
223
|
event.preventDefault()
|
|
@@ -265,14 +319,14 @@ export default class extends Controller {
|
|
|
265
319
|
throw new Error("Advanced select options request failed")
|
|
266
320
|
}
|
|
267
321
|
|
|
268
|
-
return
|
|
322
|
+
return this.readOptionsResponse(response)
|
|
269
323
|
})
|
|
270
|
-
.then((
|
|
324
|
+
.then((payload) => {
|
|
271
325
|
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
272
326
|
return
|
|
273
327
|
}
|
|
274
328
|
|
|
275
|
-
|
|
329
|
+
this.renderOptionsResponse(payload)
|
|
276
330
|
requestAnimationFrame(() => {
|
|
277
331
|
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
278
332
|
return
|
|
@@ -285,6 +339,8 @@ export default class extends Controller {
|
|
|
285
339
|
if (autoSelect) {
|
|
286
340
|
this.autoSelectSingle()
|
|
287
341
|
}
|
|
342
|
+
|
|
343
|
+
this.dispatchOptionsLoaded()
|
|
288
344
|
})
|
|
289
345
|
})
|
|
290
346
|
.catch(() => {
|
|
@@ -350,6 +406,37 @@ export default class extends Controller {
|
|
|
350
406
|
}
|
|
351
407
|
}
|
|
352
408
|
|
|
409
|
+
optionElement(option) {
|
|
410
|
+
const normalized = this.normalizeSelectedOption(option)
|
|
411
|
+
const button = document.createElement("button")
|
|
412
|
+
|
|
413
|
+
button.type = "button"
|
|
414
|
+
button.className = this.optionClass
|
|
415
|
+
button.setAttribute("role", "option")
|
|
416
|
+
button.setAttribute("aria-selected", "false")
|
|
417
|
+
button.dataset.advancedSelectOption = ""
|
|
418
|
+
button.dataset.action = "mouseenter->advanced-select#activateOption mousedown->advanced-select#choose"
|
|
419
|
+
button.dataset.advancedSelectValueParam = normalized.id
|
|
420
|
+
button.dataset.advancedSelectSubmitValueParam = normalized.value
|
|
421
|
+
button.dataset.advancedSelectLabelParam = normalized.label
|
|
422
|
+
button.dataset.advancedSelectDisplayLabelParam = normalized.displayLabel
|
|
423
|
+
button.dataset.advancedSelectOptionParam = JSON.stringify(normalized)
|
|
424
|
+
|
|
425
|
+
const check = this.textElement("span", this.optionCheckClass, "")
|
|
426
|
+
check.dataset.advancedSelectOptionCheck = ""
|
|
427
|
+
|
|
428
|
+
const content = this.textElement("span", this.optionContentClass, "")
|
|
429
|
+
content.appendChild(this.textElement("span", "", normalized.label))
|
|
430
|
+
|
|
431
|
+
if (normalized.description) {
|
|
432
|
+
content.appendChild(this.textElement("span", this.optionDescriptionClass, normalized.description))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
button.append(check, content)
|
|
436
|
+
|
|
437
|
+
return button
|
|
438
|
+
}
|
|
439
|
+
|
|
353
440
|
optionData(element) {
|
|
354
441
|
const data = this.parseOptionData(element.dataset.advancedSelectOptionParam)
|
|
355
442
|
const value = element.dataset.advancedSelectValueParam
|
|
@@ -402,7 +489,31 @@ export default class extends Controller {
|
|
|
402
489
|
this.renderOptionsState()
|
|
403
490
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
404
491
|
this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
|
|
405
|
-
|
|
492
|
+
|
|
493
|
+
if (this.suppressChange) {
|
|
494
|
+
this.suppressChange = false
|
|
495
|
+
} else {
|
|
496
|
+
this.dispatchValueChange()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
readOptionsResponse(response) {
|
|
501
|
+
return response.text()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
renderOptionsResponse(payload) {
|
|
505
|
+
Turbo.renderStreamMessage(payload)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
dispatchOptionsLoaded() {
|
|
509
|
+
this.element.dispatchEvent(new CustomEvent("advanced-select:options-loaded", {
|
|
510
|
+
bubbles: true,
|
|
511
|
+
detail: {
|
|
512
|
+
name: this.nameValue,
|
|
513
|
+
value: this.currentValue,
|
|
514
|
+
count: this.selectableOptionElements.length
|
|
515
|
+
}
|
|
516
|
+
}))
|
|
406
517
|
}
|
|
407
518
|
|
|
408
519
|
dispatchValueChange() {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
data-advanced-select-option-active-class="<%= advanced_select_state_class(class_map, :option_active) %>"
|
|
30
30
|
data-advanced-select-add-option-active-class="<%= advanced_select_state_class(class_map, :add_option_active) %>"
|
|
31
31
|
data-advanced-select-option-selected-class="<%= advanced_select_state_class(class_map, :option_selected) %>"
|
|
32
|
+
data-advanced-select-option-class="<%= advanced_select_class(class_map, :option) %>"
|
|
33
|
+
data-advanced-select-option-check-class="<%= advanced_select_class(class_map, :option_check) %>"
|
|
34
|
+
data-advanced-select-option-content-class="<%= advanced_select_class(class_map, :option_content) %>"
|
|
35
|
+
data-advanced-select-option-description-class="<%= advanced_select_class(class_map, :option_description) %>"
|
|
32
36
|
data-advanced-select-selected-value="<%= advanced_select_selected_value(selected_options) %>">
|
|
33
37
|
<div data-advanced-select-target="hiddenFields">
|
|
34
38
|
<% if multiple %>
|
|
@@ -30,6 +30,7 @@ export default class extends Controller {
|
|
|
30
30
|
this.timer = null
|
|
31
31
|
this.requestSequence = 0
|
|
32
32
|
this.activeIndex = -1
|
|
33
|
+
this.suppressChange = false
|
|
33
34
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
34
35
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
35
36
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
@@ -40,6 +41,10 @@ export default class extends Controller {
|
|
|
40
41
|
this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
|
|
41
42
|
this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
|
|
42
43
|
this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
|
|
44
|
+
this.optionClass = this.element.dataset.advancedSelectOptionClass || "ui-advanced-select-option"
|
|
45
|
+
this.optionCheckClass = this.element.dataset.advancedSelectOptionCheckClass || "ui-advanced-select-option-check"
|
|
46
|
+
this.optionContentClass = this.element.dataset.advancedSelectOptionContentClass || "ui-advanced-select-option-content"
|
|
47
|
+
this.optionDescriptionClass = this.element.dataset.advancedSelectOptionDescriptionClass || "ui-advanced-select-option-description"
|
|
43
48
|
this.selectedValue = this.selectedValue.map((option) => this.normalizeSelectedOption(option))
|
|
44
49
|
this.close = this.close.bind(this)
|
|
45
50
|
this.renderOptionsState()
|
|
@@ -164,6 +169,55 @@ export default class extends Controller {
|
|
|
164
169
|
this.close()
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
getValue() {
|
|
173
|
+
return this.currentValue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setValue(value, { silent = false } = {}) {
|
|
177
|
+
const requested = (Array.isArray(value) ? value : [value])
|
|
178
|
+
.filter((item) => item !== "" && item != null)
|
|
179
|
+
.map(String)
|
|
180
|
+
const values = this.multipleValue ? requested : requested.slice(0, 1)
|
|
181
|
+
const options = this.optionElements.map((element) => this.optionData(element))
|
|
182
|
+
|
|
183
|
+
this.selectedValue = values.map((item) => {
|
|
184
|
+
const option = options.find((candidate) => candidate.id === item || candidate.value === item)
|
|
185
|
+
|
|
186
|
+
return this.normalizeSelectedOption(option || { id: item, value: item, label: item })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
this.suppressChange = silent
|
|
190
|
+
this.renderSelection()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
appendOption(option) {
|
|
194
|
+
const normalized = this.normalizeSelectedOption(option)
|
|
195
|
+
const present = this.optionElements.some((element) => element.dataset.advancedSelectValueParam === normalized.id)
|
|
196
|
+
|
|
197
|
+
if (present) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.currentOptionsTarget.appendChild(this.optionElement(normalized))
|
|
202
|
+
this.renderOptionsState()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
replaceOptions(options, { selected, silent = true } = {}) {
|
|
206
|
+
this.currentOptionsTarget.replaceChildren(...options.map((option) => this.optionElement(option)))
|
|
207
|
+
|
|
208
|
+
if (selected === undefined) {
|
|
209
|
+
this.suppressChange = silent
|
|
210
|
+
this.renderSelection()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.setValue(selected, { silent })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
refresh() {
|
|
218
|
+
this.renderSelection()
|
|
219
|
+
}
|
|
220
|
+
|
|
167
221
|
keydown(event) {
|
|
168
222
|
if (event.key === "ArrowDown") {
|
|
169
223
|
event.preventDefault()
|
|
@@ -265,14 +319,14 @@ export default class extends Controller {
|
|
|
265
319
|
throw new Error("Advanced select options request failed")
|
|
266
320
|
}
|
|
267
321
|
|
|
268
|
-
return
|
|
322
|
+
return this.readOptionsResponse(response)
|
|
269
323
|
})
|
|
270
|
-
.then((
|
|
324
|
+
.then((payload) => {
|
|
271
325
|
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
272
326
|
return
|
|
273
327
|
}
|
|
274
328
|
|
|
275
|
-
|
|
329
|
+
this.renderOptionsResponse(payload)
|
|
276
330
|
requestAnimationFrame(() => {
|
|
277
331
|
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
278
332
|
return
|
|
@@ -285,6 +339,8 @@ export default class extends Controller {
|
|
|
285
339
|
if (autoSelect) {
|
|
286
340
|
this.autoSelectSingle()
|
|
287
341
|
}
|
|
342
|
+
|
|
343
|
+
this.dispatchOptionsLoaded()
|
|
288
344
|
})
|
|
289
345
|
})
|
|
290
346
|
.catch(() => {
|
|
@@ -350,6 +406,37 @@ export default class extends Controller {
|
|
|
350
406
|
}
|
|
351
407
|
}
|
|
352
408
|
|
|
409
|
+
optionElement(option) {
|
|
410
|
+
const normalized = this.normalizeSelectedOption(option)
|
|
411
|
+
const button = document.createElement("button")
|
|
412
|
+
|
|
413
|
+
button.type = "button"
|
|
414
|
+
button.className = this.optionClass
|
|
415
|
+
button.setAttribute("role", "option")
|
|
416
|
+
button.setAttribute("aria-selected", "false")
|
|
417
|
+
button.dataset.advancedSelectOption = ""
|
|
418
|
+
button.dataset.action = "mouseenter->advanced-select#activateOption mousedown->advanced-select#choose"
|
|
419
|
+
button.dataset.advancedSelectValueParam = normalized.id
|
|
420
|
+
button.dataset.advancedSelectSubmitValueParam = normalized.value
|
|
421
|
+
button.dataset.advancedSelectLabelParam = normalized.label
|
|
422
|
+
button.dataset.advancedSelectDisplayLabelParam = normalized.displayLabel
|
|
423
|
+
button.dataset.advancedSelectOptionParam = JSON.stringify(normalized)
|
|
424
|
+
|
|
425
|
+
const check = this.textElement("span", this.optionCheckClass, "")
|
|
426
|
+
check.dataset.advancedSelectOptionCheck = ""
|
|
427
|
+
|
|
428
|
+
const content = this.textElement("span", this.optionContentClass, "")
|
|
429
|
+
content.appendChild(this.textElement("span", "", normalized.label))
|
|
430
|
+
|
|
431
|
+
if (normalized.description) {
|
|
432
|
+
content.appendChild(this.textElement("span", this.optionDescriptionClass, normalized.description))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
button.append(check, content)
|
|
436
|
+
|
|
437
|
+
return button
|
|
438
|
+
}
|
|
439
|
+
|
|
353
440
|
optionData(element) {
|
|
354
441
|
const data = this.parseOptionData(element.dataset.advancedSelectOptionParam)
|
|
355
442
|
const value = element.dataset.advancedSelectValueParam
|
|
@@ -402,7 +489,31 @@ export default class extends Controller {
|
|
|
402
489
|
this.renderOptionsState()
|
|
403
490
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
404
491
|
this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
|
|
405
|
-
|
|
492
|
+
|
|
493
|
+
if (this.suppressChange) {
|
|
494
|
+
this.suppressChange = false
|
|
495
|
+
} else {
|
|
496
|
+
this.dispatchValueChange()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
readOptionsResponse(response) {
|
|
501
|
+
return response.text()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
renderOptionsResponse(payload) {
|
|
505
|
+
Turbo.renderStreamMessage(payload)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
dispatchOptionsLoaded() {
|
|
509
|
+
this.element.dispatchEvent(new CustomEvent("advanced-select:options-loaded", {
|
|
510
|
+
bubbles: true,
|
|
511
|
+
detail: {
|
|
512
|
+
name: this.nameValue,
|
|
513
|
+
value: this.currentValue,
|
|
514
|
+
count: this.selectableOptionElements.length
|
|
515
|
+
}
|
|
516
|
+
}))
|
|
406
517
|
}
|
|
407
518
|
|
|
408
519
|
dispatchValueChange() {
|