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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9be8d05d537d5e36a565546c0f821c68a6a62cd5e3978cbbe23d3ef7451c1f2a
4
- data.tar.gz: c6c4c0667ef096fd55e57024621eb9a21d2338ecbef2c2305e2bf2edab0fad4d
3
+ metadata.gz: 12aa609bf191df8feb5cdbcbe6c56052cf1e41737bd87d4122b99d65d8788009
4
+ data.tar.gz: d49f89a642d10f28aa141c6ef7728335adbc5168fef7f3ba6aba828f164b2477
5
5
  SHA512:
6
- metadata.gz: 492cee1cf22c71e9c6e328052bb3ff7c49d519816c06ebe8610f2c0d07d2cbcfd9ebe09383ac3a69e696497295a34015aa467f004d46e40194ae4cc7d7538473
7
- data.tar.gz: 65b3749af47dc024f475284c0998657190f5c68c27da5578d93dfd35669a4eb9c319d3b1b6df1a07fb16157eb0f8897ed4154ab7b54f5e49d8ad7dda3d1bec84
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.4"
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 &amp; 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">&#8964;</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">&times;</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
- <% selected_options.first(2).each do |option| %>
4
- <span class="<%= advanced_select_class(class_map, :token) %>"><%= option.fetch(:display_label) %></span>
5
- <% end %>
6
- <% if selected_options.size > 2 %>
7
- <span class="<%= advanced_select_class(class_map, :token) %>"><%= "& +#{selected_options.size - 2}" %></span>
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>
@@ -6,3 +6,4 @@ en:
6
6
  error: "Options could not be loaded"
7
7
  loading: "Loading..."
8
8
  search_placeholder: "Search..."
9
+ selected_count: "%{count} selected"
@@ -6,3 +6,4 @@ tr:
6
6
  error: "Seçenekler yüklenemedi"
7
7
  loading: "Yükleniyor..."
8
8
  search_placeholder: "Ara..."
9
+ selected_count: "%{count} seçili"
@@ -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
@@ -1,3 +1,3 @@
1
1
  module AdvancedSelect
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: advanced_select
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik