advanced_select 0.1.4 → 0.1.6
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 +100 -1
- data/app/assets/stylesheets/advanced_select/advanced_select.css +34 -0
- data/app/javascript/advanced_select/advanced_select_controller.js +124 -1
- data/app/views/advanced_select/_select.html.erb +26 -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 +4 -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 +61 -1
- 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: 12aa609bf191df8feb5cdbcbe6c56052cf1e41737bd87d4122b99d65d8788009
|
|
4
|
+
data.tar.gz: d49f89a642d10f28aa141c6ef7728335adbc5168fef7f3ba6aba828f164b2477
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53132d110065de787848345926574700a2cc2a4b8666d4e0f740c85f4a552950f268378e9aa73d04e1b404612e481f1c364fd3a07cd5a397dfff3c18b6d78122
|
|
7
|
+
data.tar.gz: 3e07f94cffddcdbdeb08576c54903ff5d1a5498db40224f5341a5826c3bacbcad59c9fb5254ccbf36449254702102681c16a6547ff9acc4f0e613bb08c72fe94
|
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.5"
|
|
79
81
|
```
|
|
80
82
|
|
|
81
83
|
Run the installer:
|
|
@@ -428,6 +430,91 @@ 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
|
+
The partial is rendered on the server for the initial selection, and the controller keeps it in sync client-side as the selection changes — no Turbo Stream round-trip. To opt in, mark the row container and provide a one-row `<template>` the controller clones per selected option:
|
|
488
|
+
|
|
489
|
+
- `data-advanced-select-tooltip-list` on the element that holds the rows (the same hook the built-in list uses).
|
|
490
|
+
- `<template data-advanced-select-tooltip-template>` containing the markup for a single selected row.
|
|
491
|
+
- `data-advanced-select-tooltip-field="…"` on the elements inside the template that should be filled in. Available fields are `id`, `value`, `label`, and `display_label`; values are written with `textContent` (text only, no markup).
|
|
492
|
+
|
|
493
|
+
```erb
|
|
494
|
+
<%# app/views/alternatives/_tooltip.html.erb %>
|
|
495
|
+
<table>
|
|
496
|
+
<thead>
|
|
497
|
+
<tr><th>Code & Name</th><th>Type</th></tr>
|
|
498
|
+
</thead>
|
|
499
|
+
<tbody data-advanced-select-tooltip-list>
|
|
500
|
+
<% selected_options.each do |option| %>
|
|
501
|
+
<tr>
|
|
502
|
+
<td><%= option.fetch(:display_label) %></td>
|
|
503
|
+
<td><%= option.fetch(:value) %></td>
|
|
504
|
+
</tr>
|
|
505
|
+
<% end %>
|
|
506
|
+
</tbody>
|
|
507
|
+
<template data-advanced-select-tooltip-template>
|
|
508
|
+
<tr>
|
|
509
|
+
<td data-advanced-select-tooltip-field="display_label"></td>
|
|
510
|
+
<td data-advanced-select-tooltip-field="value"></td>
|
|
511
|
+
</tr>
|
|
512
|
+
</template>
|
|
513
|
+
</table>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
The initial server-rendered rows stay until the first change, then the controller rebuilds them from the template — so just like `tooltip: true`, the custom tooltip updates client-side automatically. A custom partial without a `<template>` still renders once and stays static, which is fine for display-only content.
|
|
517
|
+
|
|
431
518
|
### Add Mode
|
|
432
519
|
|
|
433
520
|
Set `add_mode: true` when users may submit a new typed value:
|
|
@@ -675,12 +762,19 @@ advanced_select_tag(
|
|
|
675
762
|
include_hidden: true,
|
|
676
763
|
auto_select_single: true,
|
|
677
764
|
eager: true,
|
|
765
|
+
summary_mode: :tokens,
|
|
766
|
+
tooltip: false,
|
|
767
|
+
tooltip_partial: nil,
|
|
678
768
|
option_content_partial: nil,
|
|
679
769
|
classes: {},
|
|
680
770
|
append_classes: {}
|
|
681
771
|
)
|
|
682
772
|
```
|
|
683
773
|
|
|
774
|
+
`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)).
|
|
775
|
+
|
|
776
|
+
`tooltip:` / `tooltip_partial:` enable an optional hover tooltip on the trigger (see [Selection Tooltip](#selection-tooltip)).
|
|
777
|
+
|
|
684
778
|
`advanced_select_options_tag`:
|
|
685
779
|
|
|
686
780
|
```ruby
|
|
@@ -738,8 +832,12 @@ shared:
|
|
|
738
832
|
empty: "No options found"
|
|
739
833
|
error: "Options could not be loaded"
|
|
740
834
|
loading: "Loading..."
|
|
835
|
+
search_placeholder: "Search..."
|
|
836
|
+
selected_count: "%{count} selected"
|
|
741
837
|
```
|
|
742
838
|
|
|
839
|
+
`selected_count` is used by the [count summary](#count-summary) (`summary_mode: :count`).
|
|
840
|
+
|
|
743
841
|
Override these keys in the host app as needed.
|
|
744
842
|
|
|
745
843
|
## Styling
|
|
@@ -851,6 +949,7 @@ Common styling hooks:
|
|
|
851
949
|
|
|
852
950
|
- `.ui-advanced-select-trigger` controls the visible input button, border, radius, height, background, and focus outline.
|
|
853
951
|
- `.ui-advanced-select-dropdown` controls the popup container, border, radius, shadow, width, and `z-index`.
|
|
952
|
+
- `.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.
|
|
854
953
|
- `.ui-advanced-select-options` controls the scroll container and default `max-height`.
|
|
855
954
|
- `.ui-advanced-select-option` controls option row spacing, hover state, and font sizing.
|
|
856
955
|
- `.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,7 +2,7 @@ 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
8
|
autoSelectSingle: { type: Boolean, default: true },
|
|
@@ -19,6 +19,8 @@ export default class extends Controller {
|
|
|
19
19
|
placeholder: String,
|
|
20
20
|
searchable: Boolean,
|
|
21
21
|
selected: Array,
|
|
22
|
+
selectedCountText: String,
|
|
23
|
+
summaryMode: { type: String, default: "tokens" },
|
|
22
24
|
targetId: String,
|
|
23
25
|
url: String
|
|
24
26
|
}
|
|
@@ -30,6 +32,7 @@ export default class extends Controller {
|
|
|
30
32
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
31
33
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
32
34
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
35
|
+
this.tooltipItemClass = this.element.dataset.advancedSelectTooltipItemClass || "ui-advanced-select-tooltip-item"
|
|
33
36
|
this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
|
|
34
37
|
this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
|
|
35
38
|
this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
|
|
@@ -48,6 +51,7 @@ export default class extends Controller {
|
|
|
48
51
|
|
|
49
52
|
disconnect() {
|
|
50
53
|
window.clearTimeout(this.timer)
|
|
54
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
51
55
|
document.removeEventListener("click", this.close)
|
|
52
56
|
|
|
53
57
|
if (this.dependentChangeListener) {
|
|
@@ -66,6 +70,7 @@ export default class extends Controller {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
open() {
|
|
73
|
+
this.hideTooltip(true)
|
|
69
74
|
this.dropdownTarget.classList.remove("hidden")
|
|
70
75
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
71
76
|
document.addEventListener("click", this.close)
|
|
@@ -302,6 +307,7 @@ export default class extends Controller {
|
|
|
302
307
|
renderSelection() {
|
|
303
308
|
this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
|
|
304
309
|
this.summaryTarget.replaceChildren(...this.selectionElements)
|
|
310
|
+
this.renderTooltip()
|
|
305
311
|
this.renderOptionsState()
|
|
306
312
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
307
313
|
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
|
|
@@ -471,6 +477,10 @@ export default class extends Controller {
|
|
|
471
477
|
return [this.textElement("span", this.valueClass, this.displayLabel(this.selectedValue[0]))]
|
|
472
478
|
}
|
|
473
479
|
|
|
480
|
+
if (this.summaryModeValue === "count") {
|
|
481
|
+
return [this.textElement("span", this.valueClass, this.selectedCountLabel(this.selectedValue.length))]
|
|
482
|
+
}
|
|
483
|
+
|
|
474
484
|
const tokens = this.selectedValue.slice(0, 2).map((option) => this.textElement("span", this.tokenClass, this.displayLabel(option)))
|
|
475
485
|
|
|
476
486
|
if (this.selectedValue.length > 2) {
|
|
@@ -480,10 +490,123 @@ export default class extends Controller {
|
|
|
480
490
|
return tokens
|
|
481
491
|
}
|
|
482
492
|
|
|
493
|
+
selectedCountLabel(count) {
|
|
494
|
+
return (this.selectedCountTextValue || "%{count}").replace("%{count}", count)
|
|
495
|
+
}
|
|
496
|
+
|
|
483
497
|
displayLabel(option) {
|
|
484
498
|
return option.displayLabel || option.label
|
|
485
499
|
}
|
|
486
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) {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
|
|
534
|
+
this.renderCustomTooltip()
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.renderBuiltInTooltip()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
renderBuiltInTooltip() {
|
|
542
|
+
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
|
|
543
|
+
if (!list) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
list.replaceChildren(
|
|
548
|
+
...this.selectedValue.map((option) => this.textElement("li", this.tooltipItemClass, this.displayLabel(option)))
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if (this.selectedValue.length === 0) {
|
|
552
|
+
this.hideTooltip(true)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
renderCustomTooltip() {
|
|
557
|
+
const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
|
|
558
|
+
const template = this.customTooltipTemplate()
|
|
559
|
+
|
|
560
|
+
if (!list || !template) {
|
|
561
|
+
if (this.selectedValue.length === 0) {
|
|
562
|
+
this.hideTooltip(true)
|
|
563
|
+
}
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
list.replaceChildren(
|
|
568
|
+
...this.selectedValue.map((option) => this.customTooltipElement(template, option))
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
if (this.selectedValue.length === 0) {
|
|
572
|
+
this.hideTooltip(true)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
customTooltipTemplate() {
|
|
577
|
+
if (this.customTooltipTemplateContent) {
|
|
578
|
+
return this.customTooltipTemplateContent
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const template = this.tooltipTarget.querySelector("template[data-advanced-select-tooltip-template]")
|
|
582
|
+
if (!template) {
|
|
583
|
+
return null
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
this.customTooltipTemplateContent = template.content.cloneNode(true)
|
|
587
|
+
template.remove()
|
|
588
|
+
return this.customTooltipTemplateContent
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
customTooltipElement(template, option) {
|
|
592
|
+
const fragment = template.cloneNode(true)
|
|
593
|
+
fragment.querySelectorAll("[data-advanced-select-tooltip-field]").forEach((element) => {
|
|
594
|
+
element.textContent = this.tooltipFieldValue(option, element.dataset.advancedSelectTooltipField)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
return fragment
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
tooltipFieldValue(option, field) {
|
|
601
|
+
if (field === "display_label" || field === "displayLabel") {
|
|
602
|
+
return this.displayLabel(option)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const camelizedField = field.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
|
|
606
|
+
const value = option[field] ?? option[camelizedField]
|
|
607
|
+
return value === undefined || value === null ? "" : value.toString()
|
|
608
|
+
}
|
|
609
|
+
|
|
487
610
|
textElement(tagName, className, text) {
|
|
488
611
|
const element = document.createElement(tagName)
|
|
489
612
|
element.className = className
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
data-advanced-select-include-hidden-value="<%= include_hidden %>"
|
|
16
16
|
data-advanced-select-auto-select-single-value="<%= auto_select_single %>"
|
|
17
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}") %>"
|
|
18
20
|
data-advanced-select-placeholder-class="<%= advanced_select_class(class_map, :placeholder) %>"
|
|
19
21
|
data-advanced-select-value-class="<%= advanced_select_class(class_map, :value) %>"
|
|
20
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) %>"
|
|
21
24
|
data-advanced-select-empty-class="<%= advanced_select_class(class_map, :empty) %>"
|
|
22
25
|
data-advanced-select-loading-class="<%= advanced_select_class(class_map, :loading) %>"
|
|
23
26
|
data-advanced-select-error-class="<%= advanced_select_class(class_map, :error) %>"
|
|
@@ -38,6 +41,8 @@
|
|
|
38
41
|
<% end %>
|
|
39
42
|
</div>
|
|
40
43
|
|
|
44
|
+
<% tooltip_enabled = tooltip || tooltip_partial.present? %>
|
|
45
|
+
|
|
41
46
|
<button type="button"
|
|
42
47
|
id="<%= "#{id}_trigger" %>"
|
|
43
48
|
class="<%= advanced_select_class(class_map, :trigger) %>"
|
|
@@ -45,14 +50,33 @@
|
|
|
45
50
|
aria-expanded="false"
|
|
46
51
|
aria-controls="<%= "#{id}_dropdown" %>"
|
|
47
52
|
data-advanced-select-target="trigger"
|
|
48
|
-
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 %>">
|
|
49
54
|
<span id="<%= "#{id}_summary" %>" class="<%= advanced_select_class(class_map, :summary) %>" data-advanced-select-target="summary">
|
|
50
|
-
<%= 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 } %>
|
|
51
56
|
</span>
|
|
52
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>
|
|
53
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>
|
|
54
59
|
</button>
|
|
55
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
|
+
|
|
56
80
|
<div id="<%= "#{id}_dropdown" %>"
|
|
57
81
|
class="<%= [advanced_select_class(class_map, :dropdown), "hidden"].join(" ") %>"
|
|
58
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, auto_select_single: true, eager: 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
|
|
|
@@ -18,6 +18,9 @@ module AdvancedSelect
|
|
|
18
18
|
include_hidden: include_hidden,
|
|
19
19
|
auto_select_single: auto_select_single,
|
|
20
20
|
eager: eager,
|
|
21
|
+
summary_mode: summary_mode,
|
|
22
|
+
tooltip: tooltip,
|
|
23
|
+
tooltip_partial: tooltip_partial,
|
|
21
24
|
target_id: "#{id}_options",
|
|
22
25
|
option_content_partial: option_content_partial,
|
|
23
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,7 +2,7 @@ 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
8
|
autoSelectSingle: { type: Boolean, default: true },
|
|
@@ -19,6 +19,8 @@ export default class extends Controller {
|
|
|
19
19
|
placeholder: String,
|
|
20
20
|
searchable: Boolean,
|
|
21
21
|
selected: Array,
|
|
22
|
+
selectedCountText: String,
|
|
23
|
+
summaryMode: { type: String, default: "tokens" },
|
|
22
24
|
targetId: String,
|
|
23
25
|
url: String
|
|
24
26
|
}
|
|
@@ -30,6 +32,7 @@ export default class extends Controller {
|
|
|
30
32
|
this.placeholderClass = this.element.dataset.advancedSelectPlaceholderClass || "ui-advanced-select-placeholder"
|
|
31
33
|
this.valueClass = this.element.dataset.advancedSelectValueClass || "ui-advanced-select-value"
|
|
32
34
|
this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
|
|
35
|
+
this.tooltipItemClass = this.element.dataset.advancedSelectTooltipItemClass || "ui-advanced-select-tooltip-item"
|
|
33
36
|
this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
|
|
34
37
|
this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
|
|
35
38
|
this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
|
|
@@ -48,6 +51,7 @@ export default class extends Controller {
|
|
|
48
51
|
|
|
49
52
|
disconnect() {
|
|
50
53
|
window.clearTimeout(this.timer)
|
|
54
|
+
window.clearTimeout(this.tooltipHideTimer)
|
|
51
55
|
document.removeEventListener("click", this.close)
|
|
52
56
|
|
|
53
57
|
if (this.dependentChangeListener) {
|
|
@@ -66,6 +70,7 @@ export default class extends Controller {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
open() {
|
|
73
|
+
this.hideTooltip(true)
|
|
69
74
|
this.dropdownTarget.classList.remove("hidden")
|
|
70
75
|
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
71
76
|
document.addEventListener("click", this.close)
|
|
@@ -302,6 +307,7 @@ export default class extends Controller {
|
|
|
302
307
|
renderSelection() {
|
|
303
308
|
this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
|
|
304
309
|
this.summaryTarget.replaceChildren(...this.selectionElements)
|
|
310
|
+
this.renderTooltip()
|
|
305
311
|
this.renderOptionsState()
|
|
306
312
|
this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
|
|
307
313
|
this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
|
|
@@ -473,6 +479,10 @@ export default class extends Controller {
|
|
|
473
479
|
return [this.textElement("span", this.valueClass, this.displayLabel(this.selectedValue[0]))]
|
|
474
480
|
}
|
|
475
481
|
|
|
482
|
+
if (this.summaryModeValue === "count") {
|
|
483
|
+
return [this.textElement("span", this.valueClass, this.selectedCountLabel(this.selectedValue.length))]
|
|
484
|
+
}
|
|
485
|
+
|
|
476
486
|
const tokens = this.selectedValue.slice(0, 2).map((option) => this.textElement("span", this.tokenClass, this.displayLabel(option)))
|
|
477
487
|
|
|
478
488
|
if (this.selectedValue.length > 2) {
|
|
@@ -482,10 +492,60 @@ export default class extends Controller {
|
|
|
482
492
|
return tokens
|
|
483
493
|
}
|
|
484
494
|
|
|
495
|
+
selectedCountLabel(count) {
|
|
496
|
+
return (this.selectedCountTextValue || "%{count}").replace("%{count}", count)
|
|
497
|
+
}
|
|
498
|
+
|
|
485
499
|
displayLabel(option) {
|
|
486
500
|
return option.displayLabel || option.label
|
|
487
501
|
}
|
|
488
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
|
+
|
|
489
549
|
textElement(tagName, className, text) {
|
|
490
550
|
const element = document.createElement(tagName)
|
|
491
551
|
element.className = className
|