advanced_select 0.1.2 → 0.1.3

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: 8acafe10e284da26e61bec53737160926b3b6f43e51aa4d2a690e9a9b5a47367
4
- data.tar.gz: c2465e1a792dc86ece9747b18fe2dcb3e077d706aed3e178515fe1376308067b
3
+ metadata.gz: 254f883547b3f323b98150becc0db7f8ff76f609a9a7186a5d1eb8903a0afb7f
4
+ data.tar.gz: 56b4d1866feaad22d4c4ed00bb2ab573240dbedda3d2f7cc6a1759c7da53b571
5
5
  SHA512:
6
- metadata.gz: d9a0cefc246959a5f8f5910e91e648f5654c55eee8bbb63ae78232c5abd9b7ab0b307eb04744f70016d5e23ab54cc3b7c9a6973c18b5d2ea3b885255a54e8c5a
7
- data.tar.gz: 4e4143bb1da3fb3e50f5aa6fda9cce40f3631c6f7d0ba5b2185a5f06ca4cb9f0b16938932aca365206cf0391ac5b8151335ae1e32e1e5c9abd4a26447e383617
6
+ metadata.gz: f6a74b01aed5465044972b7f89d75148a9192120d8bba05e7a212899a11715198a3a4743092e329e1dc8ae4ccb0c86c2ab21ccafb48b88fa52059212b6d4acc3
7
+ data.tar.gz: 9df6c7b80f331cdc57fabaddce592f1f899728fe643d73ac3c98149acdb6b9f4c7a559e2fd87113655338ece19695fcfed7fbb6b570808bf02395f44c568301b
data/README.md CHANGED
@@ -316,11 +316,12 @@ Use local options when the complete option list is already available while rende
316
316
  id: "record_item_id",
317
317
  selected: selected_option,
318
318
  options: options,
319
- placeholder: t(".item_placeholder"),
320
- searchable: false
319
+ placeholder: t(".item_placeholder")
321
320
  ) %>
322
321
  ```
323
322
 
323
+ Local selects are searchable by default. Set `searchable: false` when the option list should open without a search field.
324
+
324
325
  Options are hashes:
325
326
 
326
327
  ```ruby
@@ -7,6 +7,7 @@ export default class extends Controller {
7
7
  addMode: Boolean,
8
8
  delay: { type: Number, default: 200 },
9
9
  dependentFields: Object,
10
+ emptyText: String,
10
11
  errorText: String,
11
12
  includeHidden: { type: Boolean, default: true },
12
13
  inputId: String,
@@ -29,6 +30,7 @@ export default class extends Controller {
29
30
  this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
30
31
  this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
31
32
  this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
33
+ this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
32
34
  this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
33
35
  this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
34
36
  this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
@@ -81,6 +83,12 @@ export default class extends Controller {
81
83
  }
82
84
 
83
85
  search() {
86
+ if (!this.urlValue) {
87
+ this.filterOptions()
88
+ this.activate(-1)
89
+ return
90
+ }
91
+
84
92
  window.clearTimeout(this.timer)
85
93
  this.renderLoading()
86
94
  this.activate(-1)
@@ -127,9 +135,57 @@ export default class extends Controller {
127
135
  clearSearch() {
128
136
  if (this.searchableValue) {
129
137
  this.searchTarget.value = ""
138
+
139
+ if (!this.urlValue) {
140
+ this.filterOptions()
141
+ }
142
+ }
143
+ }
144
+
145
+ filterOptions() {
146
+ const query = this.normalize(this.searchTarget.value)
147
+ const container = this.currentOptionsTarget
148
+ let visibleCount = 0
149
+ let currentGroup = null
150
+ let groupHasMatch = false
151
+
152
+ Array.from(container.children).forEach((child) => {
153
+ if (child.hasAttribute("data-advanced-select-group-label")) {
154
+ if (currentGroup) currentGroup.classList.toggle("hidden", !groupHasMatch)
155
+ currentGroup = child
156
+ groupHasMatch = false
157
+ } else if (child.hasAttribute("data-advanced-select-option")) {
158
+ const match = this.normalize(child.dataset.advancedSelectLabelParam).includes(query)
159
+ child.classList.toggle("hidden", !match)
160
+ if (match) {
161
+ visibleCount += 1
162
+ groupHasMatch = true
163
+ }
164
+ }
165
+ })
166
+
167
+ if (currentGroup) currentGroup.classList.toggle("hidden", !groupHasMatch)
168
+
169
+ this.toggleEmptyState(visibleCount === 0)
170
+ }
171
+
172
+ toggleEmptyState(empty) {
173
+ const container = this.currentOptionsTarget
174
+ let emptyState = container.querySelector("[data-advanced-select-empty-state]")
175
+
176
+ if (empty && !emptyState) {
177
+ emptyState = this.textElement("div", this.emptyClass, this.emptyTextValue)
178
+ emptyState.setAttribute("data-advanced-select-empty-state", "")
179
+ container.appendChild(emptyState)
180
+ } else if (!empty && emptyState) {
181
+ emptyState.remove()
130
182
  }
131
183
  }
132
184
 
185
+ normalize(text) {
186
+ return (text || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
187
+ }
188
+
133
189
  fetchOptions({ selected = false } = {}) {
134
190
  if (!this.urlValue) {
135
191
  return
@@ -8,7 +8,7 @@
8
8
 
9
9
  <% advanced_select_option_groups(options).each do |group| %>
10
10
  <% if group[:label].present? %>
11
- <div class="<%= advanced_select_class(class_map, :group_label) %>"><%= group.fetch(:label) %></div>
11
+ <div class="<%= advanced_select_class(class_map, :group_label) %>" data-advanced-select-group-label><%= group.fetch(:label) %></div>
12
12
  <% end %>
13
13
 
14
14
  <% group.fetch(:options).each do |option| %>
@@ -28,6 +28,6 @@
28
28
  <%= t("shared.advanced_select.add_option", query: query) %>
29
29
  </button>
30
30
  <% elsif advanced_select_options_empty?(options) %>
31
- <div class="<%= advanced_select_class(class_map, :empty) %>"><%= t("shared.advanced_select.empty") %></div>
31
+ <div class="<%= advanced_select_class(class_map, :empty) %>" data-advanced-select-empty-state><%= t("shared.advanced_select.empty") %></div>
32
32
  <% end %>
33
33
  </div>
@@ -5,6 +5,7 @@
5
5
  data-advanced-select-name-value="<%= name %>"
6
6
  data-advanced-select-input-id-value="<%= id %>"
7
7
  data-advanced-select-placeholder-value="<%= placeholder %>"
8
+ data-advanced-select-empty-text-value="<%= t("shared.advanced_select.empty") %>"
8
9
  data-advanced-select-loading-text-value="<%= t("shared.advanced_select.loading") %>"
9
10
  data-advanced-select-error-text-value="<%= t("shared.advanced_select.error") %>"
10
11
  data-advanced-select-dependent-fields-value="<%= dependent_fields.to_json %>"
@@ -15,6 +16,7 @@
15
16
  data-advanced-select-placeholder-class="<%= advanced_select_class(class_map, :placeholder) %>"
16
17
  data-advanced-select-value-class="<%= advanced_select_class(class_map, :value) %>"
17
18
  data-advanced-select-token-class="<%= advanced_select_class(class_map, :token) %>"
19
+ data-advanced-select-empty-class="<%= advanced_select_class(class_map, :empty) %>"
18
20
  data-advanced-select-loading-class="<%= advanced_select_class(class_map, :loading) %>"
19
21
  data-advanced-select-error-class="<%= advanced_select_class(class_map, :error) %>"
20
22
  data-advanced-select-option-active-class="<%= advanced_select_state_class(class_map, :option_active) %>"
@@ -56,7 +58,7 @@
56
58
  <%= tag.input type: "search",
57
59
  id: "#{id}_search",
58
60
  autocomplete: "off",
59
- placeholder: placeholder,
61
+ placeholder: t("shared.advanced_select.search_placeholder"),
60
62
  class: advanced_select_class(class_map, :search),
61
63
  data: {
62
64
  advanced_select_target: "search",
@@ -5,3 +5,4 @@ en:
5
5
  empty: "No options found"
6
6
  error: "Options could not be loaded"
7
7
  loading: "Loading..."
8
+ search_placeholder: "Search..."
@@ -5,3 +5,4 @@ tr:
5
5
  empty: "Seçenek bulunamadı"
6
6
  error: "Seçenekler yüklenemedi"
7
7
  loading: "Yükleniyor..."
8
+ search_placeholder: "Ara..."
@@ -12,7 +12,7 @@ module AdvancedSelect
12
12
  options: options,
13
13
  placeholder: placeholder,
14
14
  multiple: multiple,
15
- searchable: searchable && options_url.present?,
15
+ searchable: searchable,
16
16
  add_mode: add_mode,
17
17
  dependent_fields: dependent_fields,
18
18
  include_hidden: include_hidden,
@@ -1,3 +1,3 @@
1
1
  module AdvancedSelect
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -7,7 +7,9 @@ export default class extends Controller {
7
7
  addMode: Boolean,
8
8
  delay: { type: Number, default: 200 },
9
9
  dependentFields: Object,
10
+ emptyText: String,
10
11
  errorText: String,
12
+ includeHidden: { type: Boolean, default: true },
11
13
  inputId: String,
12
14
  loadingText: String,
13
15
  multiple: Boolean,
@@ -28,6 +30,7 @@ export default class extends Controller {
28
30
  this.tokenClass = this.element.dataset.advancedSelectTokenClass || "ui-advanced-select-token"
29
31
  this.loadingClass = this.element.dataset.advancedSelectLoadingClass || "ui-advanced-select-loading"
30
32
  this.errorClass = this.element.dataset.advancedSelectErrorClass || "ui-advanced-select-error"
33
+ this.emptyClass = this.element.dataset.advancedSelectEmptyClass || "ui-advanced-select-empty"
31
34
  this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
32
35
  this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
33
36
  this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
@@ -37,6 +40,7 @@ export default class extends Controller {
37
40
  displayLabel: option.displayLabel || option.label
38
41
  }))
39
42
  this.close = this.close.bind(this)
43
+ this.renderOptionsState()
40
44
  }
41
45
 
42
46
  disconnect() {
@@ -79,6 +83,12 @@ export default class extends Controller {
79
83
  }
80
84
 
81
85
  search() {
86
+ if (!this.urlValue) {
87
+ this.filterOptions()
88
+ this.activate(-1)
89
+ return
90
+ }
91
+
82
92
  window.clearTimeout(this.timer)
83
93
  this.renderLoading()
84
94
  this.activate(-1)
@@ -125,9 +135,57 @@ export default class extends Controller {
125
135
  clearSearch() {
126
136
  if (this.searchableValue) {
127
137
  this.searchTarget.value = ""
138
+
139
+ if (!this.urlValue) {
140
+ this.filterOptions()
141
+ }
128
142
  }
129
143
  }
130
144
 
145
+ filterOptions() {
146
+ const query = this.normalize(this.searchTarget.value)
147
+ const container = this.currentOptionsTarget
148
+ let visibleCount = 0
149
+ let currentGroup = null
150
+ let groupHasMatch = false
151
+
152
+ Array.from(container.children).forEach((child) => {
153
+ if (child.hasAttribute("data-advanced-select-group-label")) {
154
+ if (currentGroup) currentGroup.classList.toggle("hidden", !groupHasMatch)
155
+ currentGroup = child
156
+ groupHasMatch = false
157
+ } else if (child.hasAttribute("data-advanced-select-option")) {
158
+ const match = this.normalize(child.dataset.advancedSelectLabelParam).includes(query)
159
+ child.classList.toggle("hidden", !match)
160
+ if (match) {
161
+ visibleCount += 1
162
+ groupHasMatch = true
163
+ }
164
+ }
165
+ })
166
+
167
+ if (currentGroup) currentGroup.classList.toggle("hidden", !groupHasMatch)
168
+
169
+ this.toggleEmptyState(visibleCount === 0)
170
+ }
171
+
172
+ toggleEmptyState(empty) {
173
+ const container = this.currentOptionsTarget
174
+ let emptyState = container.querySelector("[data-advanced-select-empty-state]")
175
+
176
+ if (empty && !emptyState) {
177
+ emptyState = this.textElement("div", this.emptyClass, this.emptyTextValue)
178
+ emptyState.setAttribute("data-advanced-select-empty-state", "")
179
+ container.appendChild(emptyState)
180
+ } else if (!empty && emptyState) {
181
+ emptyState.remove()
182
+ }
183
+ }
184
+
185
+ normalize(text) {
186
+ return (text || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
187
+ }
188
+
131
189
  fetchOptions({ selected = false } = {}) {
132
190
  if (!this.urlValue) {
133
191
  return
@@ -214,7 +272,7 @@ export default class extends Controller {
214
272
  if (this.selectedValue.some((option) => option.id === value)) {
215
273
  this.selectedValue = this.selectedValue.filter((option) => option.id !== value)
216
274
  } else {
217
- this.selectedValue = this.selectedValue.concat({ id: value, value: submitValue, label, displayLabel })
275
+ this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }, ...this.selectedValue]
218
276
  }
219
277
  } else {
220
278
  this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }]
@@ -240,6 +298,7 @@ export default class extends Controller {
240
298
 
241
299
  renderOptionsState() {
242
300
  const selectedIds = new Set(this.selectedValue.map((option) => option.id))
301
+ const container = this.currentOptionsTarget
243
302
 
244
303
  this.optionElements.forEach((option) => {
245
304
  const selected = selectedIds.has(option.dataset.advancedSelectValueParam)
@@ -251,6 +310,13 @@ export default class extends Controller {
251
310
  check.textContent = selected ? "\u2713" : ""
252
311
  }
253
312
  })
313
+
314
+ for (let i = this.selectedValue.length - 1; i >= 0; i--) {
315
+ const option = container.querySelector(
316
+ `[data-advanced-select-option][data-advanced-select-value-param="${this.selectedValue[i].id}"]`
317
+ )
318
+ if (option) container.prepend(option)
319
+ }
254
320
  }
255
321
 
256
322
  chooseActiveOption() {
@@ -303,7 +369,13 @@ export default class extends Controller {
303
369
  }
304
370
 
305
371
  get hiddenFieldElements() {
306
- const options = this.multipleValue ? this.selectedValue : [this.selectedValue[0]]
372
+ let options
373
+
374
+ if (this.multipleValue) {
375
+ options = this.includeHiddenValue ? [null, ...this.selectedValue] : this.selectedValue
376
+ } else {
377
+ options = [this.selectedValue[0]]
378
+ }
307
379
 
308
380
  return options.map((option) => {
309
381
  const input = document.createElement("input")
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.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik