advanced_select 0.1.8 → 0.1.10

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: b0d986f80cf90e0dc2aa254d9eb0a9b8d76b844f205ae8ca86f815c1a5b17bea
4
- data.tar.gz: 72722ff9f42bbed846afe1ec04e46362e9ec259d96c7bacb171207dd8ccbdc79
3
+ metadata.gz: 3268b609f1b4c08af63c6bc791459345dbbdfb1afa3a1e9974569392e27b417a
4
+ data.tar.gz: 525a23db2449fd8867d83fb7b1647d75d6d194a4791971fec0cb55de48a2d1c8
5
5
  SHA512:
6
- metadata.gz: d287e5b91c20d5dd629d55f802f1095fd2f016e3f4bdccff8f011fabf0e115914d56563ed00f5219a7a65452d1df4b143a4f2b3e5bf68b8ebb93898c83a7910d
7
- data.tar.gz: 1b9d260a2ace0a2ec7d132581da98e35a09fc26265420655afa40ff290454b716584bb1cf5062a5ce2fa1a432cae22c1da75e411546b3673e88e468b4295910e
6
+ metadata.gz: f66187ab2d1914f3de4c933fecbf3b9237684b4c8d911de1582426002f6d099820181401137d6c0896d3cdb99b1af38ecf12acbdd8dd29ff7aac6af965e2c587
7
+ data.tar.gz: 3a240d713742fcd791541b4272e499d6036d8ada44eb8697529628d9b1ceb9092678362bf435bcd25bc8b33d95e6f9a95d4a752e23b9b587fe814b738ddb9cd6
data/README.md CHANGED
@@ -22,6 +22,8 @@ AdvancedSelect is a small Rails engine for rendering an advanced select input wi
22
22
  - [Dependent Fields](#dependent-fields)
23
23
  - [Custom Option Content](#custom-option-content)
24
24
  - [Option Contract](#option-contract)
25
+ - [Events](#events)
26
+ - [Disabled State](#disabled-state)
25
27
  - [API Reference](#api-reference)
26
28
  - [Local Development](#local-development)
27
29
  - [i18n](#i18n)
@@ -745,6 +747,94 @@ Grouped options use this shape:
745
747
  ]
746
748
  ```
747
749
 
750
+ ### Events
751
+
752
+ Every value change dispatches two events:
753
+
754
+ | Event | Target | Purpose |
755
+ | --- | --- | --- |
756
+ | `change` | the first hidden input | native form behaviour and [dependent fields](#dependent-fields) |
757
+ | `advanced-select:change` | the root element | application listeners that need the whole selection |
758
+
759
+ Both bubble. `advanced-select:change` carries a `detail` payload:
760
+
761
+ ```js
762
+ {
763
+ name: "record[item_ids][]",
764
+ value: ["3", "7"],
765
+ options: [
766
+ { id: "3", value: "3", label: "Item A", display_label: "Item A" },
767
+ { id: "7", value: "7", label: "Item B", display_label: "Item B" }
768
+ ]
769
+ }
770
+ ```
771
+
772
+ `value` is the submit value — the option's `value` when it defines one, otherwise its `id`. It is an
773
+ array for multiple selects, a string for single selects, and empty when nothing is selected. A
774
+ multiple select lists the most recently chosen option first, matching the order of its hidden
775
+ inputs.
776
+
777
+ Bind it like any other event:
778
+
779
+ ```erb
780
+ data-action="advanced-select:change->my-controller#refresh"
781
+ ```
782
+
783
+ Prefer it over `change` on multiple selects. `change` is dispatched from the first hidden input, so
784
+ it does not fire at all once the last value is cleared from a field rendered with
785
+ `include_hidden: false` — at that point the field has no inputs left.
786
+
787
+ The current value is also readable from the controller:
788
+
789
+ ```js
790
+ const select = application.getControllerForElementAndIdentifier(element, "advanced-select")
791
+
792
+ select.currentValue // => ["3", "7"]
793
+ ```
794
+
795
+ ### Disabled State
796
+
797
+ Pass `disabled: true` to lock a field. It still renders its current selection —
798
+ the point is to show the value, not to hide it — but it cannot be opened,
799
+ searched, or cleared, and its value is **left out of the form**:
800
+
801
+ ```erb
802
+ <%= advanced_select_tag(
803
+ "record[item_id]",
804
+ id: "record_item_id",
805
+ selected: selected_option,
806
+ options: options,
807
+ placeholder: "Choose an item",
808
+ disabled: true
809
+ ) %>
810
+ ```
811
+
812
+ This matches a native `<select disabled>`: the browser omits a disabled field
813
+ from the submitted params, so an update never writes a value the user was not
814
+ allowed to change.
815
+
816
+ The trigger carries the `disabled` attribute, so mouse and keyboard are both
817
+ blocked by the browser rather than by CSS alone. The root element takes the
818
+ `disabled` class from the class map.
819
+
820
+ To toggle the state after render — from a Stimulus controller, or from a Turbo
821
+ Stream that flips the attribute — set the value on the root element:
822
+
823
+ ```js
824
+ element.dataset.advancedSelectDisabledValue = "true"
825
+ ```
826
+
827
+ The controller reacts on its own: it disables the hidden inputs, applies the
828
+ class, hides the clear control, and closes the dropdown if it happens to be
829
+ open. The same thing is available as a method:
830
+
831
+ ```js
832
+ const select = application.getControllerForElementAndIdentifier(element, "advanced-select")
833
+
834
+ select.disable()
835
+ select.enable()
836
+ ```
837
+
748
838
  ### API Reference
749
839
 
750
840
  `advanced_select_tag`:
@@ -760,6 +850,7 @@ advanced_select_tag(
760
850
  multiple: false,
761
851
  searchable: true,
762
852
  add_mode: false,
853
+ disabled: false,
763
854
  dependent_fields: {},
764
855
  include_hidden: true,
765
856
  auto_select_single: true,
@@ -777,6 +868,8 @@ advanced_select_tag(
777
868
 
778
869
  `tooltip:` / `tooltip_partial:` enable an optional hover tooltip on the trigger (see [Selection Tooltip](#selection-tooltip)).
779
870
 
871
+ `disabled:` locks the field (see [Disabled State](#disabled-state)).
872
+
780
873
  `advanced_select_options_tag`:
781
874
 
782
875
  ```ruby
@@ -11,6 +11,16 @@
11
11
  display: none !important;
12
12
  }
13
13
 
14
+ .ui-advanced-select-disabled .ui-advanced-select-trigger {
15
+ background: #f3f4f6;
16
+ color: #6b7280;
17
+ cursor: not-allowed;
18
+ }
19
+
20
+ .ui-advanced-select-disabled .ui-advanced-select-clear {
21
+ display: none;
22
+ }
23
+
14
24
  .ui-advanced-select-trigger {
15
25
  align-items: center;
16
26
  background: #ffffff;
@@ -8,6 +8,7 @@ export default class extends Controller {
8
8
  autoSelectSingle: { type: Boolean, default: true },
9
9
  delay: { type: Number, default: 200 },
10
10
  dependentFields: Object,
11
+ disabled: Boolean,
11
12
  eager: { type: Boolean, default: true },
12
13
  emptyText: String,
13
14
  errorText: String,
@@ -65,7 +66,40 @@ export default class extends Controller {
65
66
  this.expanded ? this.close() : this.open()
66
67
  }
67
68
 
69
+ disable() {
70
+ this.disabledValue = true
71
+ }
72
+
73
+ enable() {
74
+ this.disabledValue = false
75
+ }
76
+
77
+ disabledValueChanged() {
78
+ if (!this.hasTriggerTarget) {
79
+ return
80
+ }
81
+
82
+ const disabled = this.disabledValue
83
+ const classes = this.classList(this.element.dataset.advancedSelectDisabledClass || "ui-advanced-select-disabled")
84
+
85
+ this.triggerTarget.disabled = disabled
86
+ this.hiddenFieldsTarget.querySelectorAll("input").forEach((input) => { input.disabled = disabled })
87
+ this.clearTarget.classList.toggle("hidden", disabled || this.selectedValue.length === 0)
88
+
89
+ if (classes.length) {
90
+ disabled ? this.element.classList.add(...classes) : this.element.classList.remove(...classes)
91
+ }
92
+
93
+ if (disabled && this.expanded) {
94
+ this.close()
95
+ }
96
+ }
97
+
68
98
  open() {
99
+ if (this.disabledValue) {
100
+ return
101
+ }
102
+
69
103
  this.hideTooltip(true)
70
104
  this.dropdownTarget.classList.remove("hidden")
71
105
  this.triggerTarget.setAttribute("aria-expanded", "true")
@@ -120,6 +154,11 @@ export default class extends Controller {
120
154
  clear(event) {
121
155
  event.preventDefault()
122
156
  event.stopPropagation()
157
+
158
+ if (this.disabledValue) {
159
+ return
160
+ }
161
+
123
162
  this.selectedValue = []
124
163
  this.renderSelection()
125
164
  this.close()
@@ -362,7 +401,7 @@ export default class extends Controller {
362
401
  this.renderTooltip()
363
402
  this.renderOptionsState()
364
403
  this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
365
- this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
404
+ this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
366
405
  this.dispatchValueChange()
367
406
  }
368
407
 
@@ -371,6 +410,21 @@ export default class extends Controller {
371
410
  if (input) {
372
411
  input.dispatchEvent(new Event("change", { bubbles: true }))
373
412
  }
413
+
414
+ this.element.dispatchEvent(new CustomEvent("advanced-select:change", {
415
+ bubbles: true,
416
+ detail: {
417
+ name: this.nameValue,
418
+ value: this.currentValue,
419
+ options: [...this.selectedValue]
420
+ }
421
+ }))
422
+ }
423
+
424
+ get currentValue() {
425
+ const values = this.selectedValue.map((option) => option.value || option.id)
426
+
427
+ return this.multipleValue ? values : (values[0] || "")
374
428
  }
375
429
 
376
430
  renderOptionsState() {
@@ -500,6 +554,7 @@ export default class extends Controller {
500
554
  input.type = "hidden"
501
555
  input.name = this.nameValue
502
556
  input.value = option ? option.value || option.id : ""
557
+ input.disabled = this.disabledValue
503
558
 
504
559
  if (!this.multipleValue) {
505
560
  input.id = this.inputIdValue
@@ -1,5 +1,7 @@
1
- <div class="<%= advanced_select_class(class_map, :root) %>"
1
+ <div class="<%= advanced_select_class(class_map, :root, (:disabled if disabled)) %>"
2
2
  data-controller="advanced-select"
3
+ data-advanced-select-disabled-value="<%= disabled %>"
4
+ data-advanced-select-disabled-class="<%= advanced_select_state_class(class_map, :disabled) %>"
3
5
  data-advanced-select-url-value="<%= options_url %>"
4
6
  data-advanced-select-target-id-value="<%= target_id %>"
5
7
  data-advanced-select-name-value="<%= name %>"
@@ -31,13 +33,13 @@
31
33
  <div data-advanced-select-target="hiddenFields">
32
34
  <% if multiple %>
33
35
  <% if include_hidden %>
34
- <%= hidden_field_tag name, "", id: nil %>
36
+ <%= hidden_field_tag name, "", id: nil, disabled: disabled %>
35
37
  <% end %>
36
38
  <% selected_options.each do |option| %>
37
- <%= hidden_field_tag name, option.fetch(:value, option.fetch(:id)) %>
39
+ <%= hidden_field_tag name, option.fetch(:value, option.fetch(:id)), disabled: disabled %>
38
40
  <% end %>
39
41
  <% else %>
40
- <%= hidden_field_tag name, selected_options.first&.fetch(:value, selected_options.first&.fetch(:id)), id: id %>
42
+ <%= hidden_field_tag name, selected_options.first&.fetch(:value, selected_options.first&.fetch(:id)), id: id, disabled: disabled %>
41
43
  <% end %>
42
44
  </div>
43
45
 
@@ -49,13 +51,14 @@
49
51
  aria-haspopup="listbox"
50
52
  aria-expanded="false"
51
53
  aria-controls="<%= "#{id}_dropdown" %>"
54
+ <%= "disabled" if disabled %>
52
55
  data-advanced-select-target="trigger"
53
56
  data-action="advanced-select#toggle keydown->advanced-select#keydown<%= " mouseenter->advanced-select#showTooltip mouseleave->advanced-select#hideTooltip" if tooltip_enabled %>">
54
57
  <span id="<%= "#{id}_summary" %>" class="<%= advanced_select_class(class_map, :summary) %>" data-advanced-select-target="summary">
55
58
  <%= render partial: "advanced_select/summary", locals: { selected_options: selected_options, multiple: multiple, placeholder: placeholder, summary_mode: summary_mode, class_map: class_map } %>
56
59
  </span>
57
60
  <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>
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>
61
+ <span id="<%= "#{id}_clear" %>" class="<%= [advanced_select_class(class_map, :clear), ("hidden" if disabled || selected_options.empty?)].compact.join(" ") %>" data-advanced-select-target="clear" data-action="click->advanced-select#clear">&times;</span>
59
62
  </button>
60
63
 
61
64
  <% if tooltip_enabled %>
@@ -2,6 +2,7 @@ module AdvancedSelect
2
2
  class ClassMap
3
3
  DEFAULTS = {
4
4
  root: "ui-advanced-select",
5
+ disabled: "ui-advanced-select-disabled",
5
6
  trigger: "ui-advanced-select-trigger",
6
7
  summary: "ui-advanced-select-summary",
7
8
  placeholder: "ui-advanced-select-placeholder",
@@ -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, summary_mode: :tokens, tooltip: false, tooltip_partial: nil, 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, disabled: 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
 
@@ -14,6 +14,7 @@ module AdvancedSelect
14
14
  multiple: multiple,
15
15
  searchable: searchable,
16
16
  add_mode: add_mode,
17
+ disabled: disabled,
17
18
  dependent_fields: dependent_fields,
18
19
  include_hidden: include_hidden,
19
20
  auto_select_single: auto_select_single,
@@ -1,3 +1,3 @@
1
1
  module AdvancedSelect
2
- VERSION = "0.1.8"
2
+ VERSION = "0.1.10"
3
3
  end
@@ -11,6 +11,16 @@
11
11
  display: none !important;
12
12
  }
13
13
 
14
+ .ui-advanced-select-disabled .ui-advanced-select-trigger {
15
+ background: #f3f4f6;
16
+ color: #6b7280;
17
+ cursor: not-allowed;
18
+ }
19
+
20
+ .ui-advanced-select-disabled .ui-advanced-select-clear {
21
+ display: none;
22
+ }
23
+
14
24
  .ui-advanced-select-trigger {
15
25
  align-items: center;
16
26
  background: #ffffff;
@@ -8,6 +8,7 @@ export default class extends Controller {
8
8
  autoSelectSingle: { type: Boolean, default: true },
9
9
  delay: { type: Number, default: 200 },
10
10
  dependentFields: Object,
11
+ disabled: Boolean,
11
12
  eager: { type: Boolean, default: true },
12
13
  emptyText: String,
13
14
  errorText: String,
@@ -65,7 +66,40 @@ export default class extends Controller {
65
66
  this.expanded ? this.close() : this.open()
66
67
  }
67
68
 
69
+ disable() {
70
+ this.disabledValue = true
71
+ }
72
+
73
+ enable() {
74
+ this.disabledValue = false
75
+ }
76
+
77
+ disabledValueChanged() {
78
+ if (!this.hasTriggerTarget) {
79
+ return
80
+ }
81
+
82
+ const disabled = this.disabledValue
83
+ const classes = this.classList(this.element.dataset.advancedSelectDisabledClass || "ui-advanced-select-disabled")
84
+
85
+ this.triggerTarget.disabled = disabled
86
+ this.hiddenFieldsTarget.querySelectorAll("input").forEach((input) => { input.disabled = disabled })
87
+ this.clearTarget.classList.toggle("hidden", disabled || this.selectedValue.length === 0)
88
+
89
+ if (classes.length) {
90
+ disabled ? this.element.classList.add(...classes) : this.element.classList.remove(...classes)
91
+ }
92
+
93
+ if (disabled && this.expanded) {
94
+ this.close()
95
+ }
96
+ }
97
+
68
98
  open() {
99
+ if (this.disabledValue) {
100
+ return
101
+ }
102
+
69
103
  this.hideTooltip(true)
70
104
  this.dropdownTarget.classList.remove("hidden")
71
105
  this.triggerTarget.setAttribute("aria-expanded", "true")
@@ -120,6 +154,11 @@ export default class extends Controller {
120
154
  clear(event) {
121
155
  event.preventDefault()
122
156
  event.stopPropagation()
157
+
158
+ if (this.disabledValue) {
159
+ return
160
+ }
161
+
123
162
  this.selectedValue = []
124
163
  this.renderSelection()
125
164
  this.close()
@@ -362,7 +401,7 @@ export default class extends Controller {
362
401
  this.renderTooltip()
363
402
  this.renderOptionsState()
364
403
  this.caretTarget.classList.toggle("hidden", this.selectedValue.length > 0)
365
- this.clearTarget.classList.toggle("hidden", this.selectedValue.length === 0)
404
+ this.clearTarget.classList.toggle("hidden", this.disabledValue || this.selectedValue.length === 0)
366
405
  this.dispatchValueChange()
367
406
  }
368
407
 
@@ -371,6 +410,21 @@ export default class extends Controller {
371
410
  if (input) {
372
411
  input.dispatchEvent(new Event("change", { bubbles: true }))
373
412
  }
413
+
414
+ this.element.dispatchEvent(new CustomEvent("advanced-select:change", {
415
+ bubbles: true,
416
+ detail: {
417
+ name: this.nameValue,
418
+ value: this.currentValue,
419
+ options: [...this.selectedValue]
420
+ }
421
+ }))
422
+ }
423
+
424
+ get currentValue() {
425
+ const values = this.selectedValue.map((option) => option.value || option.id)
426
+
427
+ return this.multipleValue ? values : (values[0] || "")
374
428
  }
375
429
 
376
430
  renderOptionsState() {
@@ -500,6 +554,7 @@ export default class extends Controller {
500
554
  input.type = "hidden"
501
555
  input.name = this.nameValue
502
556
  input.value = option ? option.value || option.id : ""
557
+ input.disabled = this.disabledValue
503
558
 
504
559
  if (!this.multipleValue) {
505
560
  input.id = this.inputIdValue
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.8
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik