advanced_select 0.1.3 → 0.1.5
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 +132 -1
- data/app/assets/stylesheets/advanced_select/advanced_select.css +34 -0
- data/app/javascript/advanced_select/advanced_select_controller.js +137 -6
- data/app/views/advanced_select/_select.html.erb +28 -2
- data/app/views/advanced_select/_summary.html.erb +10 -5
- data/config/locales/en.yml +1 -0
- data/config/locales/tr.yml +1 -0
- data/lib/advanced_select/class_map.rb +3 -0
- data/lib/advanced_select/helper.rb +6 -1
- data/lib/advanced_select/version.rb +1 -1
- data/lib/generators/advanced_select/install/templates/advanced_select.css +34 -0
- data/lib/generators/advanced_select/install/templates/advanced_select_controller.js +140 -7
- 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: 43e9d7178d6a064d18c4243d1fad3ec15fbb935834eb2d07da8cd89d33c955a1
|
|
4
|
+
data.tar.gz: 389f74b51ecd1615a2d3b11c2794e10c57557ce6085595ebe06f3a296b88874a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: faf098b583a4d1ea48aeb788943b3da2400fdca7946f32f02419aef63ca34af371c7ce9cd97368205ff6dcb96e55b5e3044794e5f8f5cf8237faf0e53dd7e62b
|
|
7
|
+
data.tar.gz: 5c8fea29044aa4e5a9da1cb804b93f0124e9e302dbc82c0f63d8a216f767e2e13d518c5768704285874596ef6a809750f3033e7ff759176c35a4991f04ee87f0
|
data/README.md
CHANGED
|
@@ -16,6 +16,8 @@ AdvancedSelect is a small Rails engine for rendering an advanced select input wi
|
|
|
16
16
|
- [Basic Local Select](#basic-local-select)
|
|
17
17
|
- [Remote Search](#remote-search)
|
|
18
18
|
- [Multiple Select](#multiple-select)
|
|
19
|
+
- [Count Summary](#count-summary)
|
|
20
|
+
- [Selection Tooltip](#selection-tooltip)
|
|
19
21
|
- [Add Mode](#add-mode)
|
|
20
22
|
- [Dependent Fields](#dependent-fields)
|
|
21
23
|
- [Custom Option Content](#custom-option-content)
|
|
@@ -75,7 +77,7 @@ AdvancedSelect does not provide query objects, model concerns, authorization log
|
|
|
75
77
|
Add the gem to the host Rails app:
|
|
76
78
|
|
|
77
79
|
```ruby
|
|
78
|
-
gem "advanced_select", "~> 0.1.
|
|
80
|
+
gem "advanced_select", "~> 0.1.4"
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
Run the installer:
|
|
@@ -428,6 +430,79 @@ Pass `include_hidden: false` to opt out — for example, when your controller re
|
|
|
428
430
|
) %>
|
|
429
431
|
```
|
|
430
432
|
|
|
433
|
+
### Count Summary
|
|
434
|
+
|
|
435
|
+
By default a multiple select collapses its trigger to the first two selected labels plus an `& +N` token. For long selections — or when the detail belongs in a [tooltip](#selection-tooltip) — pass `summary_mode: :count` to render a single `"N selected"` label instead:
|
|
436
|
+
|
|
437
|
+
```erb
|
|
438
|
+
<%= advanced_select_tag(
|
|
439
|
+
"record[item_ids][]",
|
|
440
|
+
id: "record_item_ids",
|
|
441
|
+
selected: selected_options,
|
|
442
|
+
options: options,
|
|
443
|
+
placeholder: t(".items_placeholder"),
|
|
444
|
+
multiple: true,
|
|
445
|
+
summary_mode: :count
|
|
446
|
+
) %>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The label text comes from the `shared.advanced_select.selected_count` translation (`"%{count} selected"`), so override it per locale to localize or reword it. `summary_mode` only affects multiple selects; single selects always show the selected label.
|
|
450
|
+
|
|
451
|
+
### Selection Tooltip
|
|
452
|
+
|
|
453
|
+
Multiple selects can show an optional tooltip when the user hovers the trigger — handy together with `summary_mode: :count`, where the trigger only shows a count and the tooltip reveals the detail. The tooltip is hover-driven (no info icon) and is positioned with CSS below the trigger.
|
|
454
|
+
|
|
455
|
+
Pass `tooltip: true` for a built-in list of the selected display labels:
|
|
456
|
+
|
|
457
|
+
```erb
|
|
458
|
+
<%= advanced_select_tag(
|
|
459
|
+
"record[item_ids][]",
|
|
460
|
+
id: "record_item_ids",
|
|
461
|
+
selected: selected_options,
|
|
462
|
+
options: options,
|
|
463
|
+
placeholder: t(".items_placeholder"),
|
|
464
|
+
multiple: true,
|
|
465
|
+
summary_mode: :count,
|
|
466
|
+
tooltip: true
|
|
467
|
+
) %>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The built-in list stays in sync as the selection changes on the client.
|
|
471
|
+
|
|
472
|
+
For richer layouts — a table, badges, compatibility columns, etc. — pass `tooltip_partial:` instead. The partial receives `selected_options` and `options` as locals and may render any markup:
|
|
473
|
+
|
|
474
|
+
```erb
|
|
475
|
+
<%= advanced_select_tag(
|
|
476
|
+
"record[alternative_ids][]",
|
|
477
|
+
id: "record_alternative_ids",
|
|
478
|
+
selected: selected_options,
|
|
479
|
+
options: options,
|
|
480
|
+
placeholder: t(".alternatives_placeholder"),
|
|
481
|
+
multiple: true,
|
|
482
|
+
summary_mode: :count,
|
|
483
|
+
tooltip_partial: "alternatives/tooltip"
|
|
484
|
+
) %>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
```erb
|
|
488
|
+
<%# app/views/alternatives/_tooltip.html.erb %>
|
|
489
|
+
<table>
|
|
490
|
+
<thead>
|
|
491
|
+
<tr><th>Code & Name</th><th>Type</th></tr>
|
|
492
|
+
</thead>
|
|
493
|
+
<tbody>
|
|
494
|
+
<% selected_options.each do |option| %>
|
|
495
|
+
<tr>
|
|
496
|
+
<td><%= option.fetch(:display_label) %></td>
|
|
497
|
+
<td><%= option.fetch(:value) %></td>
|
|
498
|
+
</tr>
|
|
499
|
+
<% end %>
|
|
500
|
+
</tbody>
|
|
501
|
+
</table>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
A custom `tooltip_partial` is rendered once on the server, so it is best for display-oriented content. If your selection changes on the client and the custom tooltip must reflect it live, re-render the field (for example via a Turbo Stream) after the change. The built-in `tooltip: true` list updates client-side automatically.
|
|
505
|
+
|
|
431
506
|
### Add Mode
|
|
432
507
|
|
|
433
508
|
Set `add_mode: true` when users may submit a new typed value:
|
|
@@ -502,6 +577,48 @@ Use `dependent_fields` when a remote option endpoint depends on another field va
|
|
|
502
577
|
|
|
503
578
|
The remote request will include `parent_id=<current field value>`.
|
|
504
579
|
|
|
580
|
+
By default the field is **eager**: it listens for `change` on each dependent field and reloads its options immediately, without waiting for the dropdown to open. When a parent changes, the field clears its current value, fetches fresh options for the new parent, and (combined with auto select, below) can settle on a value while staying closed. It also loads once on initial render so a parent's default value is reflected right away. An existing server-rendered selection is left untouched.
|
|
581
|
+
|
|
582
|
+
Because the field re-broadcasts a `change` event whenever its own value changes, you can chain advanced selects: a child can depend on another advanced select via its hidden input id (`dependent_fields: { parent_id: "#record_parent_id" }`).
|
|
583
|
+
|
|
584
|
+
Pass `eager: false` for the lazy behaviour instead — options are only fetched when the dropdown opens, and the field does not react to parent changes until then:
|
|
585
|
+
|
|
586
|
+
```erb
|
|
587
|
+
<%= advanced_select_tag(
|
|
588
|
+
"record[item_id]",
|
|
589
|
+
id: "record_item_id",
|
|
590
|
+
selected: selected_option,
|
|
591
|
+
options: [],
|
|
592
|
+
placeholder: t(".item_placeholder"),
|
|
593
|
+
options_url: item_options_path,
|
|
594
|
+
dependent_fields: { parent_id: "#record_parent_id" },
|
|
595
|
+
eager: false
|
|
596
|
+
) %>
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Auto Select Single Option
|
|
600
|
+
|
|
601
|
+
When a remote option list is populated and resolves to exactly one selectable option, the field selects that option automatically. This is enabled by default and is meant for dependent fields where a parent value narrows the children down to a single valid choice.
|
|
602
|
+
|
|
603
|
+
Auto selection only runs when the dropdown is **populated through a remote request** (the cascade case): it fires after the options finish loading, either when the dropdown opens or — with the default eager dependent fields above — as soon as a parent value loads them, so a single valid option is selected without the user opening the field. It does **not** run for statically rendered local options on page load, and it never triggers while the user is actively typing a search.
|
|
604
|
+
|
|
605
|
+
It never overrides an existing selection and is skipped for `multiple` fields.
|
|
606
|
+
|
|
607
|
+
Pass `auto_select_single: false` to opt out:
|
|
608
|
+
|
|
609
|
+
```erb
|
|
610
|
+
<%= advanced_select_tag(
|
|
611
|
+
"record[item_id]",
|
|
612
|
+
id: "record_item_id",
|
|
613
|
+
selected: selected_option,
|
|
614
|
+
options: [],
|
|
615
|
+
placeholder: t(".item_placeholder"),
|
|
616
|
+
options_url: item_options_path,
|
|
617
|
+
dependent_fields: { parent_id: "#record_parent_id" },
|
|
618
|
+
auto_select_single: false
|
|
619
|
+
) %>
|
|
620
|
+
```
|
|
621
|
+
|
|
505
622
|
### Custom Option Content
|
|
506
623
|
|
|
507
624
|
Use a custom option content partial when an option needs richer content. The engine still renders the option button, Stimulus data attributes, and ARIA attributes:
|
|
@@ -631,12 +748,21 @@ advanced_select_tag(
|
|
|
631
748
|
add_mode: false,
|
|
632
749
|
dependent_fields: {},
|
|
633
750
|
include_hidden: true,
|
|
751
|
+
auto_select_single: true,
|
|
752
|
+
eager: true,
|
|
753
|
+
summary_mode: :tokens,
|
|
754
|
+
tooltip: false,
|
|
755
|
+
tooltip_partial: nil,
|
|
634
756
|
option_content_partial: nil,
|
|
635
757
|
classes: {},
|
|
636
758
|
append_classes: {}
|
|
637
759
|
)
|
|
638
760
|
```
|
|
639
761
|
|
|
762
|
+
`summary_mode:` controls how a multiple select renders its collapsed trigger label. The default `:tokens` shows the first two selected display labels followed by `& +N`. Pass `:count` to render a single localized `"N selected"` summary instead (see [Count Summary](#count-summary)).
|
|
763
|
+
|
|
764
|
+
`tooltip:` / `tooltip_partial:` enable an optional hover tooltip on the trigger (see [Selection Tooltip](#selection-tooltip)).
|
|
765
|
+
|
|
640
766
|
`advanced_select_options_tag`:
|
|
641
767
|
|
|
642
768
|
```ruby
|
|
@@ -694,8 +820,12 @@ shared:
|
|
|
694
820
|
empty: "No options found"
|
|
695
821
|
error: "Options could not be loaded"
|
|
696
822
|
loading: "Loading..."
|
|
823
|
+
search_placeholder: "Search..."
|
|
824
|
+
selected_count: "%{count} selected"
|
|
697
825
|
```
|
|
698
826
|
|
|
827
|
+
`selected_count` is used by the [count summary](#count-summary) (`summary_mode: :count`).
|
|
828
|
+
|
|
699
829
|
Override these keys in the host app as needed.
|
|
700
830
|
|
|
701
831
|
## Styling
|
|
@@ -807,6 +937,7 @@ Common styling hooks:
|
|
|
807
937
|
|
|
808
938
|
- `.ui-advanced-select-trigger` controls the visible input button, border, radius, height, background, and focus outline.
|
|
809
939
|
- `.ui-advanced-select-dropdown` controls the popup container, border, radius, shadow, width, and `z-index`.
|
|
940
|
+
- `.ui-advanced-select-tooltip`, `.ui-advanced-select-tooltip-list`, and `.ui-advanced-select-tooltip-item` control the optional [selection tooltip](#selection-tooltip) container and its built-in list. The tooltip is positioned with CSS (`position: absolute` below the trigger); adjust `top`, `min-width`, or `z-index` here if your layout needs it.
|
|
810
941
|
- `.ui-advanced-select-options` controls the scroll container and default `max-height`.
|
|
811
942
|
- `.ui-advanced-select-option` controls option row spacing, hover state, and font sizing.
|
|
812
943
|
- `.ui-advanced-select-option[aria-selected="true"]` controls selected option colors.
|
|
@@ -101,6 +101,40 @@
|
|
|
101
101
|
z-index: 20;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.ui-advanced-select-tooltip {
|
|
105
|
+
background: #ffffff;
|
|
106
|
+
border: 1px solid #e5e7eb;
|
|
107
|
+
border-radius: 0.75rem;
|
|
108
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
109
|
+
box-sizing: border-box;
|
|
110
|
+
color: #374151;
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
left: 0;
|
|
113
|
+
line-height: 1.25rem;
|
|
114
|
+
margin-top: 0.25rem;
|
|
115
|
+
min-width: 100%;
|
|
116
|
+
padding: 0.75rem 1rem;
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 100%;
|
|
119
|
+
width: max-content;
|
|
120
|
+
z-index: 30;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ui-advanced-select-tooltip-list {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: 0.25rem;
|
|
127
|
+
list-style: none;
|
|
128
|
+
margin: 0;
|
|
129
|
+
padding: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ui-advanced-select-tooltip-item {
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
|
|
104
138
|
.ui-advanced-select-search {
|
|
105
139
|
border: 1px solid #e5e7eb;
|
|
106
140
|
border-radius: 0.5rem;
|
|
@@ -2,11 +2,13 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
import { Turbo } from "@hotwired/turbo-rails"
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
-
static targets = ["hiddenFields", "trigger", "summary", "dropdown", "search", "options", "caret", "clear"]
|
|
5
|
+
static targets = ["hiddenFields", "trigger", "summary", "dropdown", "search", "options", "caret", "clear", "tooltip"]
|
|
6
6
|
static values = {
|
|
7
7
|
addMode: Boolean,
|
|
8
|
+
autoSelectSingle: { type: Boolean, default: true },
|
|
8
9
|
delay: { type: Number, default: 200 },
|
|
9
10
|
dependentFields: Object,
|
|
11
|
+
eager: { type: Boolean, default: true },
|
|
10
12
|
emptyText: String,
|
|
11
13
|
errorText: String,
|
|
12
14
|
includeHidden: { type: Boolean, default: true },
|
|
@@ -17,6 +19,8 @@ export default class extends Controller {
|
|
|
17
19
|
placeholder: String,
|
|
18
20
|
searchable: Boolean,
|
|
19
21
|
selected: Array,
|
|
22
|
+
selectedCountText: String,
|
|
23
|
+
summaryMode: { type: String, default: "tokens" },
|
|
20
24
|
targetId: String,
|
|
21
25
|
url: String
|
|
22
26
|
}
|
|
@@ -28,6 +32,7 @@ export default class extends Controller {
|
|
|
28
32
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
29
33
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
30
34
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
35
|
+
this.tooltipItemClass = this.element.dataset.advancedSelectTooltipItemClass || "ui-advanced-select-tooltip-item"
|
|
31
36
|
this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
|
|
32
37
|
this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
|
|
33
38
|
this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
|
|
@@ -41,11 +46,17 @@ export default class extends Controller {
|
|
|
41
46
|
}))
|
|
42
47
|
this.close = this.close.bind(this)
|
|
43
48
|
this.renderOptionsState()
|
|
49
|
+
this.setupEagerDependentFields()
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
disconnect() {
|
|
47
53
|
window.clearTimeout(this.timer)
|
|
54
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
48
55
|
document.removeEventListener("click", this.close)
|
|
56
|
+
|
|
57
|
+
if (this.dependentChangeListener) {
|
|
58
|
+
document.removeEventListener("change", this.dependentChangeListener)
|
|
59
|
+
}
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
toggle(event) {
|
|
@@ -59,11 +70,12 @@ export default class extends Controller {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
open() {
|
|
73
|
+
this.hideTooltip(true)
|
|
62
74
|
this.dropdownTarget.classList.remove("hidden")
|
|
63
75
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
64
76
|
document.addEventListener("click", this.close)
|
|
65
77
|
this.activate(-1)
|
|
66
|
-
this.fetchOptions({ selected: true })
|
|
78
|
+
this.fetchOptions({ selected: true, autoSelect: true })
|
|
67
79
|
|
|
68
80
|
if (this.searchableValue) {
|
|
69
81
|
requestAnimationFrame(() => this.searchTarget.focus())
|
|
@@ -186,7 +198,7 @@ export default class extends Controller {
|
|
|
186
198
|
return (text || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
fetchOptions({ selected = false } = {}) {
|
|
201
|
+
fetchOptions({ selected = false, autoSelect = false, eager = false } = {}) {
|
|
190
202
|
if (!this.urlValue) {
|
|
191
203
|
return
|
|
192
204
|
}
|
|
@@ -221,23 +233,27 @@ export default class extends Controller {
|
|
|
221
233
|
return response.text()
|
|
222
234
|
})
|
|
223
235
|
.then((html) => {
|
|
224
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
236
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
225
237
|
return
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
Turbo.renderStreamMessage(html)
|
|
229
241
|
requestAnimationFrame(() => {
|
|
230
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
242
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
231
243
|
return
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
this.currentOptionsTarget.setAttribute("aria-busy", "false")
|
|
235
247
|
this.renderOptionsState()
|
|
236
248
|
this.activate(-1)
|
|
249
|
+
|
|
250
|
+
if (autoSelect) {
|
|
251
|
+
this.autoSelectSingle()
|
|
252
|
+
}
|
|
237
253
|
})
|
|
238
254
|
})
|
|
239
255
|
.catch(() => {
|
|
240
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
256
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
241
257
|
return
|
|
242
258
|
}
|
|
243
259
|
|
|
@@ -291,9 +307,18 @@ export default class extends Controller {
|
|
|
291
307
|
renderSelection() {
|
|
292
308
|
this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
|
|
293
309
|
this.summaryTarget.replaceChildren(...this.selectionElements)
|
|
310
|
+
this.renderTooltip()
|
|
294
311
|
this.renderOptionsState()
|
|
295
312
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
296
313
|
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
|
|
314
|
+
this.dispatchValueChange()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
dispatchValueChange() {
|
|
318
|
+
const input = this.hiddenFieldsTarget.querySelector("input")
|
|
319
|
+
if (input) {
|
|
320
|
+
input.dispatchEvent(new Event("change", { bubbles: true }))
|
|
321
|
+
}
|
|
297
322
|
}
|
|
298
323
|
|
|
299
324
|
renderOptionsState() {
|
|
@@ -319,6 +344,54 @@ export default class extends Controller {
|
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
|
|
347
|
+
setupEagerDependentFields() {
|
|
348
|
+
if (!this.eagerValue || !this.urlValue) {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.dependentSelectors = [...new Set(Object.values(this.dependentFieldsValue))]
|
|
353
|
+
if (this.dependentSelectors.length === 0) {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.dependentChangeListener = (event) => {
|
|
358
|
+
if (event.target instanceof Element && this.dependentSelectors.some((selector) => event.target.matches(selector))) {
|
|
359
|
+
this.reloadDependentOptions()
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
document.addEventListener("change", this.dependentChangeListener)
|
|
363
|
+
|
|
364
|
+
if (this.selectedValue.length === 0) {
|
|
365
|
+
this.fetchOptions({ eager: true, autoSelect: true })
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
reloadDependentOptions() {
|
|
370
|
+
this.selectedValue = []
|
|
371
|
+
this.renderSelection()
|
|
372
|
+
this.clearSearch()
|
|
373
|
+
this.fetchOptions({ eager: true, autoSelect: true })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
autoSelectSingle() {
|
|
377
|
+
if (!this.autoSelectSingleValue || this.multipleValue || this.selectedValue.length > 0) {
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const options = this.selectableOptionElements
|
|
382
|
+
if (options.length !== 1) {
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const option = options[0]
|
|
387
|
+
this.selectOption(
|
|
388
|
+
option.dataset.advancedSelectValueParam,
|
|
389
|
+
option.dataset.advancedSelectLabelParam,
|
|
390
|
+
option.dataset.advancedSelectSubmitValueParam || option.dataset.advancedSelectValueParam,
|
|
391
|
+
{ displayLabel: option.dataset.advancedSelectDisplayLabelParam, refreshOptions: false }
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
|
|
322
395
|
chooseActiveOption() {
|
|
323
396
|
if (this.activeOption.hasAttribute("data-advanced-select-add-option")) {
|
|
324
397
|
this.addOption(
|
|
@@ -364,6 +437,10 @@ export default class extends Controller {
|
|
|
364
437
|
return this.optionElements[this.activeIndex]
|
|
365
438
|
}
|
|
366
439
|
|
|
440
|
+
get selectableOptionElements() {
|
|
441
|
+
return Array.from(this.currentOptionsTarget.querySelectorAll("[data-advanced-select-option]")).filter((option) => !option.classList.contains("hidden"))
|
|
442
|
+
}
|
|
443
|
+
|
|
367
444
|
get currentOptionsTarget() {
|
|
368
445
|
return document.getElementById(this.targetIdValue) || this.optionsTarget
|
|
369
446
|
}
|
|
@@ -400,6 +477,10 @@ export default class extends Controller {
|
|
|
400
477
|
return [this.textElement("span", this.valueClass, this.displayLabel(this.selectedValue[0]))]
|
|
401
478
|
}
|
|
402
479
|
|
|
480
|
+
if (this.summaryModeValue === "count") {
|
|
481
|
+
return [this.textElement("span", this.valueClass, this.selectedCountLabel(this.selectedValue.length))]
|
|
482
|
+
}
|
|
483
|
+
|
|
403
484
|
const tokens = this.selectedValue.slice(0, 2).map((option) => this.textElement("span", this.tokenClass, this.displayLabel(option)))
|
|
404
485
|
|
|
405
486
|
if (this.selectedValue.length > 2) {
|
|
@@ -409,10 +490,60 @@ export default class extends Controller {
|
|
|
409
490
|
return tokens
|
|
410
491
|
}
|
|
411
492
|
|
|
493
|
+
selectedCountLabel(count) {
|
|
494
|
+
return (this.selectedCountTextValue || "%{count}").replace("%{count}", count)
|
|
495
|
+
}
|
|
496
|
+
|
|
412
497
|
displayLabel(option) {
|
|
413
498
|
return option.displayLabel || option.label
|
|
414
499
|
}
|
|
415
500
|
|
|
501
|
+
showTooltip() {
|
|
502
|
+
if (!this.hasTooltipTarget || this.expanded || this.selectedValue.length === 0) {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
507
|
+
this.tooltipTarget.classList.remove("hidden")
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
hideTooltip(immediate = false) {
|
|
511
|
+
if (!this.hasTooltipTarget) {
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
516
|
+
|
|
517
|
+
if (immediate === true) {
|
|
518
|
+
this.tooltipTarget.classList.add("hidden")
|
|
519
|
+
} else {
|
|
520
|
+
this.tooltipHideTimer = window.setTimeout(() => this.tooltipTarget.classList.add("hidden"), 150)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
keepTooltip() {
|
|
525
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
renderTooltip() {
|
|
529
|
+
if (!this.hasTooltipTarget || this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
|
|
534
|
+
if (!list) {
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
list.replaceChildren(
|
|
539
|
+
...this.selectedValue.map((option) => this.textElement("li", this.tooltipItemClass, this.displayLabel(option)))
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if (this.selectedValue.length === 0) {
|
|
543
|
+
this.hideTooltip(true)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
416
547
|
textElement(tagName, className, text) {
|
|
417
548
|
const element = document.createElement(tagName)
|
|
418
549
|
element.className = className
|
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
data-advanced-select-searchable-value="<%= searchable %>"
|
|
14
14
|
data-advanced-select-add-mode-value="<%= add_mode %>"
|
|
15
15
|
data-advanced-select-include-hidden-value="<%= include_hidden %>"
|
|
16
|
+
data-advanced-select-auto-select-single-value="<%= auto_select_single %>"
|
|
17
|
+
data-advanced-select-eager-value="<%= eager %>"
|
|
18
|
+
data-advanced-select-summary-mode-value="<%= summary_mode %>"
|
|
19
|
+
data-advanced-select-selected-count-text-value="<%= t("shared.advanced_select.selected_count", count: "%{count}") %>"
|
|
16
20
|
data-advanced-select-placeholder-class="<%= advanced_select_class(class_map, :placeholder) %>"
|
|
17
21
|
data-advanced-select-value-class="<%= advanced_select_class(class_map, :value) %>"
|
|
18
22
|
data-advanced-select-token-class="<%= advanced_select_class(class_map, :token) %>"
|
|
23
|
+
data-advanced-select-tooltip-item-class="<%= advanced_select_class(class_map, :tooltip_item) %>"
|
|
19
24
|
data-advanced-select-empty-class="<%= advanced_select_class(class_map, :empty) %>"
|
|
20
25
|
data-advanced-select-loading-class="<%= advanced_select_class(class_map, :loading) %>"
|
|
21
26
|
data-advanced-select-error-class="<%= advanced_select_class(class_map, :error) %>"
|
|
@@ -36,6 +41,8 @@
|
|
|
36
41
|
<% end %>
|
|
37
42
|
</div>
|
|
38
43
|
|
|
44
|
+
<% tooltip_enabled = tooltip || tooltip_partial.present? %>
|
|
45
|
+
|
|
39
46
|
<button type="button"
|
|
40
47
|
id="<%= "#{id}_trigger" %>"
|
|
41
48
|
class="<%= advanced_select_class(class_map, :trigger) %>"
|
|
@@ -43,14 +50,33 @@
|
|
|
43
50
|
aria-expanded="false"
|
|
44
51
|
aria-controls="<%= "#{id}_dropdown" %>"
|
|
45
52
|
data-advanced-select-target="trigger"
|
|
46
|
-
data-action="advanced-select#toggle keydown->advanced-select#keydown">
|
|
53
|
+
data-action="advanced-select#toggle keydown->advanced-select#keydown<%= " mouseenter->advanced-select#showTooltip mouseleave->advanced-select#hideTooltip" if tooltip_enabled %>">
|
|
47
54
|
<span id="<%= "#{id}_summary" %>" class="<%= advanced_select_class(class_map, :summary) %>" data-advanced-select-target="summary">
|
|
48
|
-
<%= render partial: "advanced_select/summary", locals: { selected_options: selected_options, multiple: multiple, placeholder: placeholder, class_map: class_map } %>
|
|
55
|
+
<%= render partial: "advanced_select/summary", locals: { selected_options: selected_options, multiple: multiple, placeholder: placeholder, summary_mode: summary_mode, class_map: class_map } %>
|
|
49
56
|
</span>
|
|
50
57
|
<span id="<%= "#{id}_caret" %>" class="<%= [advanced_select_class(class_map, :caret), ("hidden" if selected_options.any?)].compact.join(" ") %>" data-advanced-select-target="caret">⌄</span>
|
|
51
58
|
<span id="<%= "#{id}_clear" %>" class="<%= [advanced_select_class(class_map, :clear), ("hidden" if selected_options.empty?)].compact.join(" ") %>" data-advanced-select-target="clear" data-action="click->advanced-select#clear">×</span>
|
|
52
59
|
</button>
|
|
53
60
|
|
|
61
|
+
<% if tooltip_enabled %>
|
|
62
|
+
<div id="<%= "#{id}_tooltip" %>"
|
|
63
|
+
class="<%= [advanced_select_class(class_map, :tooltip), "hidden"].join(" ") %>"
|
|
64
|
+
role="tooltip"
|
|
65
|
+
data-advanced-select-target="tooltip"
|
|
66
|
+
<%= "data-advanced-select-tooltip-custom" if tooltip_partial.present? %>
|
|
67
|
+
data-action="mouseenter->advanced-select#keepTooltip mouseleave->advanced-select#hideTooltip">
|
|
68
|
+
<% if tooltip_partial.present? %>
|
|
69
|
+
<%= render partial: tooltip_partial, locals: { selected_options: selected_options, options: options } %>
|
|
70
|
+
<% else %>
|
|
71
|
+
<ul class="<%= advanced_select_class(class_map, :tooltip_list) %>" data-advanced-select-tooltip-list>
|
|
72
|
+
<% selected_options.each do |option| %>
|
|
73
|
+
<li class="<%= advanced_select_class(class_map, :tooltip_item) %>"><%= option.fetch(:display_label) %></li>
|
|
74
|
+
<% end %>
|
|
75
|
+
</ul>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
|
|
54
80
|
<div id="<%= "#{id}_dropdown" %>"
|
|
55
81
|
class="<%= [advanced_select_class(class_map, :dropdown), "hidden"].join(" ") %>"
|
|
56
82
|
data-advanced-select-target="dropdown">
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
<% summary_mode = local_assigns.fetch(:summary_mode, :tokens) %>
|
|
1
2
|
<% if selected_options.any? %>
|
|
2
3
|
<% if multiple %>
|
|
3
|
-
<%
|
|
4
|
-
<span class="<%= advanced_select_class(class_map, :
|
|
5
|
-
<%
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
<% if summary_mode.to_s == "count" %>
|
|
5
|
+
<span class="<%= advanced_select_class(class_map, :value) %>"><%= t("shared.advanced_select.selected_count", count: selected_options.size) %></span>
|
|
6
|
+
<% else %>
|
|
7
|
+
<% selected_options.first(2).each do |option| %>
|
|
8
|
+
<span class="<%= advanced_select_class(class_map, :token) %>"><%= option.fetch(:display_label) %></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if selected_options.size > 2 %>
|
|
11
|
+
<span class="<%= advanced_select_class(class_map, :token) %>"><%= "& +#{selected_options.size - 2}" %></span>
|
|
12
|
+
<% end %>
|
|
8
13
|
<% end %>
|
|
9
14
|
<% else %>
|
|
10
15
|
<span class="<%= advanced_select_class(class_map, :value) %>"><%= selected_options.first.fetch(:display_label) %></span>
|
data/config/locales/en.yml
CHANGED
data/config/locales/tr.yml
CHANGED
|
@@ -9,6 +9,9 @@ module AdvancedSelect
|
|
|
9
9
|
token: "ui-advanced-select-token",
|
|
10
10
|
caret: "ui-advanced-select-caret",
|
|
11
11
|
clear: "ui-advanced-select-clear",
|
|
12
|
+
tooltip: "ui-advanced-select-tooltip",
|
|
13
|
+
tooltip_list: "ui-advanced-select-tooltip-list",
|
|
14
|
+
tooltip_item: "ui-advanced-select-tooltip-item",
|
|
12
15
|
dropdown: "ui-advanced-select-dropdown",
|
|
13
16
|
search: "ui-advanced-select-search",
|
|
14
17
|
options: "ui-advanced-select-options",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module AdvancedSelect
|
|
2
2
|
module Helper
|
|
3
|
-
def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_url: nil, multiple: false, searchable: true, add_mode: false, dependent_fields: {}, include_hidden: true, option_content_partial: nil, classes: {}, append_classes: {})
|
|
3
|
+
def advanced_select_tag(name, id:, selected:, options:, placeholder:, options_url: nil, multiple: false, searchable: true, add_mode: false, dependent_fields: {}, include_hidden: true, auto_select_single: true, eager: true, summary_mode: :tokens, tooltip: false, tooltip_partial: nil, option_content_partial: nil, classes: {}, append_classes: {})
|
|
4
4
|
selected_options = advanced_select_selected_options(selected)
|
|
5
5
|
class_map = advanced_select_class_map(classes, append_classes)
|
|
6
6
|
|
|
@@ -16,6 +16,11 @@ module AdvancedSelect
|
|
|
16
16
|
add_mode: add_mode,
|
|
17
17
|
dependent_fields: dependent_fields,
|
|
18
18
|
include_hidden: include_hidden,
|
|
19
|
+
auto_select_single: auto_select_single,
|
|
20
|
+
eager: eager,
|
|
21
|
+
summary_mode: summary_mode,
|
|
22
|
+
tooltip: tooltip,
|
|
23
|
+
tooltip_partial: tooltip_partial,
|
|
19
24
|
target_id: "#{id}_options",
|
|
20
25
|
option_content_partial: option_content_partial,
|
|
21
26
|
class_map: class_map
|
|
@@ -101,6 +101,40 @@
|
|
|
101
101
|
z-index: 20;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.ui-advanced-select-tooltip {
|
|
105
|
+
background: #ffffff;
|
|
106
|
+
border: 1px solid #e5e7eb;
|
|
107
|
+
border-radius: 0.75rem;
|
|
108
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
109
|
+
box-sizing: border-box;
|
|
110
|
+
color: #374151;
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
left: 0;
|
|
113
|
+
line-height: 1.25rem;
|
|
114
|
+
margin-top: 0.25rem;
|
|
115
|
+
min-width: 100%;
|
|
116
|
+
padding: 0.75rem 1rem;
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 100%;
|
|
119
|
+
width: max-content;
|
|
120
|
+
z-index: 30;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ui-advanced-select-tooltip-list {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: 0.25rem;
|
|
127
|
+
list-style: none;
|
|
128
|
+
margin: 0;
|
|
129
|
+
padding: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ui-advanced-select-tooltip-item {
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
|
|
104
138
|
.ui-advanced-select-search {
|
|
105
139
|
border: 1px solid #e5e7eb;
|
|
106
140
|
border-radius: 0.5rem;
|
|
@@ -2,11 +2,13 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
import { Turbo } from "@hotwired/turbo-rails"
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
|
-
static targets = ["hiddenFields", "trigger", "summary", "dropdown", "search", "options", "caret", "clear"]
|
|
5
|
+
static targets = ["hiddenFields", "trigger", "summary", "dropdown", "search", "options", "caret", "clear", "tooltip"]
|
|
6
6
|
static values = {
|
|
7
7
|
addMode: Boolean,
|
|
8
|
+
autoSelectSingle: { type: Boolean, default: true },
|
|
8
9
|
delay: { type: Number, default: 200 },
|
|
9
10
|
dependentFields: Object,
|
|
11
|
+
eager: { type: Boolean, default: true },
|
|
10
12
|
emptyText: String,
|
|
11
13
|
errorText: String,
|
|
12
14
|
includeHidden: { type: Boolean, default: true },
|
|
@@ -17,6 +19,8 @@ export default class extends Controller {
|
|
|
17
19
|
placeholder: String,
|
|
18
20
|
searchable: Boolean,
|
|
19
21
|
selected: Array,
|
|
22
|
+
selectedCountText: String,
|
|
23
|
+
summaryMode: { type: String, default: "tokens" },
|
|
20
24
|
targetId: String,
|
|
21
25
|
url: String
|
|
22
26
|
}
|
|
@@ -28,6 +32,7 @@ export default class extends Controller {
|
|
|
28
32
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
29
33
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
30
34
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
35
|
+
this.tooltipItemClass = this.element.dataset.advancedSelectTooltipItemClass || "ui-advanced-select-tooltip-item"
|
|
31
36
|
this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
|
|
32
37
|
this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
|
|
33
38
|
this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
|
|
@@ -41,11 +46,17 @@ export default class extends Controller {
|
|
|
41
46
|
}))
|
|
42
47
|
this.close = this.close.bind(this)
|
|
43
48
|
this.renderOptionsState()
|
|
49
|
+
this.setupEagerDependentFields()
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
disconnect() {
|
|
47
53
|
window.clearTimeout(this.timer)
|
|
54
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
48
55
|
document.removeEventListener("click", this.close)
|
|
56
|
+
|
|
57
|
+
if (this.dependentChangeListener) {
|
|
58
|
+
document.removeEventListener("change", this.dependentChangeListener)
|
|
59
|
+
}
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
toggle(event) {
|
|
@@ -59,11 +70,12 @@ export default class extends Controller {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
open() {
|
|
73
|
+
this.hideTooltip(true)
|
|
62
74
|
this.dropdownTarget.classList.remove("hidden")
|
|
63
75
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
64
76
|
document.addEventListener("click", this.close)
|
|
65
77
|
this.activate(-1)
|
|
66
|
-
this.fetchOptions({ selected: true })
|
|
78
|
+
this.fetchOptions({ selected: true, autoSelect: true })
|
|
67
79
|
|
|
68
80
|
if (this.searchableValue) {
|
|
69
81
|
requestAnimationFrame(() => this.searchTarget.focus())
|
|
@@ -186,7 +198,7 @@ export default class extends Controller {
|
|
|
186
198
|
return (text || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
|
187
199
|
}
|
|
188
200
|
|
|
189
|
-
fetchOptions({ selected = false } = {}) {
|
|
201
|
+
fetchOptions({ selected = false, autoSelect = false, eager = false } = {}) {
|
|
190
202
|
if (!this.urlValue) {
|
|
191
203
|
return
|
|
192
204
|
}
|
|
@@ -221,23 +233,27 @@ export default class extends Controller {
|
|
|
221
233
|
return response.text()
|
|
222
234
|
})
|
|
223
235
|
.then((html) => {
|
|
224
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
236
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
225
237
|
return
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
Turbo.renderStreamMessage(html)
|
|
229
241
|
requestAnimationFrame(() => {
|
|
230
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
242
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
231
243
|
return
|
|
232
244
|
}
|
|
233
245
|
|
|
234
246
|
this.currentOptionsTarget.setAttribute("aria-busy", "false")
|
|
235
247
|
this.renderOptionsState()
|
|
236
248
|
this.activate(-1)
|
|
249
|
+
|
|
250
|
+
if (autoSelect) {
|
|
251
|
+
this.autoSelectSingle()
|
|
252
|
+
}
|
|
237
253
|
})
|
|
238
254
|
})
|
|
239
255
|
.catch(() => {
|
|
240
|
-
if (!this.expanded || requestSequence !== this.requestSequence) {
|
|
256
|
+
if (!(eager || this.expanded) || requestSequence !== this.requestSequence) {
|
|
241
257
|
return
|
|
242
258
|
}
|
|
243
259
|
|
|
@@ -291,9 +307,18 @@ export default class extends Controller {
|
|
|
291
307
|
renderSelection() {
|
|
292
308
|
this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
|
|
293
309
|
this.summaryTarget.replaceChildren(...this.selectionElements)
|
|
310
|
+
this.renderTooltip()
|
|
294
311
|
this.renderOptionsState()
|
|
295
312
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
296
313
|
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
|
|
314
|
+
this.dispatchValueChange()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
dispatchValueChange() {
|
|
318
|
+
const input = this.hiddenFieldsTarget.querySelector("input")
|
|
319
|
+
if (input) {
|
|
320
|
+
input.dispatchEvent(new Event("change", { bubbles: true }))
|
|
321
|
+
}
|
|
297
322
|
}
|
|
298
323
|
|
|
299
324
|
renderOptionsState() {
|
|
@@ -319,6 +344,56 @@ export default class extends Controller {
|
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
|
|
347
|
+
setupEagerDependentFields() {
|
|
348
|
+
if (!this.eagerValue || !this.urlValue) {
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.dependentSelectors = [...new Set(Object.values(this.dependentFieldsValue))]
|
|
353
|
+
if (this.dependentSelectors.length === 0) {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Delegated on document so it keeps working after a parent advanced select
|
|
358
|
+
// rewrites its hidden input on each selection.
|
|
359
|
+
this.dependentChangeListener = (event) => {
|
|
360
|
+
if (event.target instanceof Element && this.dependentSelectors.some((selector) => event.target.matches(selector))) {
|
|
361
|
+
this.reloadDependentOptions()
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
document.addEventListener("change", this.dependentChangeListener)
|
|
365
|
+
|
|
366
|
+
if (this.selectedValue.length === 0) {
|
|
367
|
+
this.fetchOptions({ eager: true, autoSelect: true })
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
reloadDependentOptions() {
|
|
372
|
+
this.selectedValue = []
|
|
373
|
+
this.renderSelection()
|
|
374
|
+
this.clearSearch()
|
|
375
|
+
this.fetchOptions({ eager: true, autoSelect: true })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
autoSelectSingle() {
|
|
379
|
+
if (!this.autoSelectSingleValue || this.multipleValue || this.selectedValue.length > 0) {
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const options = this.selectableOptionElements
|
|
384
|
+
if (options.length !== 1) {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const option = options[0]
|
|
389
|
+
this.selectOption(
|
|
390
|
+
option.dataset.advancedSelectValueParam,
|
|
391
|
+
option.dataset.advancedSelectLabelParam,
|
|
392
|
+
option.dataset.advancedSelectSubmitValueParam || option.dataset.advancedSelectValueParam,
|
|
393
|
+
{ displayLabel: option.dataset.advancedSelectDisplayLabelParam, refreshOptions: false }
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
322
397
|
chooseActiveOption() {
|
|
323
398
|
if (this.activeOption.hasAttribute("data-advanced-select-add-option")) {
|
|
324
399
|
this.addOption(
|
|
@@ -364,6 +439,10 @@ export default class extends Controller {
|
|
|
364
439
|
return this.optionElements[this.activeIndex]
|
|
365
440
|
}
|
|
366
441
|
|
|
442
|
+
get selectableOptionElements() {
|
|
443
|
+
return Array.from(this.currentOptionsTarget.querySelectorAll("[data-advanced-select-option]")).filter((option) => !option.classList.contains("hidden"))
|
|
444
|
+
}
|
|
445
|
+
|
|
367
446
|
get currentOptionsTarget() {
|
|
368
447
|
return document.getElementById(this.targetIdValue) || this.optionsTarget
|
|
369
448
|
}
|
|
@@ -400,6 +479,10 @@ export default class extends Controller {
|
|
|
400
479
|
return [this.textElement("span", this.valueClass, this.displayLabel(this.selectedValue[0]))]
|
|
401
480
|
}
|
|
402
481
|
|
|
482
|
+
if (this.summaryModeValue === "count") {
|
|
483
|
+
return [this.textElement("span", this.valueClass, this.selectedCountLabel(this.selectedValue.length))]
|
|
484
|
+
}
|
|
485
|
+
|
|
403
486
|
const tokens = this.selectedValue.slice(0, 2).map((option) => this.textElement("span", this.tokenClass, this.displayLabel(option)))
|
|
404
487
|
|
|
405
488
|
if (this.selectedValue.length > 2) {
|
|
@@ -409,10 +492,60 @@ export default class extends Controller {
|
|
|
409
492
|
return tokens
|
|
410
493
|
}
|
|
411
494
|
|
|
495
|
+
selectedCountLabel(count) {
|
|
496
|
+
return (this.selectedCountTextValue || "%{count}").replace("%{count}", count)
|
|
497
|
+
}
|
|
498
|
+
|
|
412
499
|
displayLabel(option) {
|
|
413
500
|
return option.displayLabel || option.label
|
|
414
501
|
}
|
|
415
502
|
|
|
503
|
+
showTooltip() {
|
|
504
|
+
if (!this.hasTooltipTarget || this.expanded || this.selectedValue.length === 0) {
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
509
|
+
this.tooltipTarget.classList.remove("hidden")
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
hideTooltip(immediate = false) {
|
|
513
|
+
if (!this.hasTooltipTarget) {
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
518
|
+
|
|
519
|
+
if (immediate === true) {
|
|
520
|
+
this.tooltipTarget.classList.add("hidden")
|
|
521
|
+
} else {
|
|
522
|
+
this.tooltipHideTimer = window.setTimeout(() => this.tooltipTarget.classList.add("hidden"), 150)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
keepTooltip() {
|
|
527
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
renderTooltip() {
|
|
531
|
+
if (!this.hasTooltipTarget || this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
|
|
536
|
+
if (!list) {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
list.replaceChildren(
|
|
541
|
+
...this.selectedValue.map((option) => this.textElement("li", this.tooltipItemClass, this.displayLabel(option)))
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
if (this.selectedValue.length === 0) {
|
|
545
|
+
this.hideTooltip(true)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
416
549
|
textElement(tagName, className, text) {
|
|
417
550
|
const element = document.createElement(tagName)
|
|
418
551
|
element.className = className
|
|
@@ -448,4 +581,4 @@ export default class extends Controller {
|
|
|
448
581
|
get expanded() {
|
|
449
582
|
return this.triggerTarget.getAttribute("aria-expanded") === "true"
|
|
450
583
|
}
|
|
451
|
-
}
|
|
584
|
+
}
|