advanced_select 0.1.6 → 0.1.7

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: 12aa609bf191df8feb5cdbcbe6c56052cf1e41737bd87d4122b99d65d8788009
4
- data.tar.gz: d49f89a642d10f28aa141c6ef7728335adbc5168fef7f3ba6aba828f164b2477
3
+ metadata.gz: e0e19ef001d7d6c5ca336850d173c8b0f18397d8b03de9d77ce0f52506cf7b62
4
+ data.tar.gz: 374e46c6892ca911d29ddeba525ac3f9695a2c2f166028223f0c577b5f56c3fb
5
5
  SHA512:
6
- metadata.gz: 53132d110065de787848345926574700a2cc2a4b8666d4e0f740c85f4a552950f268378e9aa73d04e1b404612e481f1c364fd3a07cd5a397dfff3c18b6d78122
7
- data.tar.gz: 3e07f94cffddcdbdeb08576c54903ff5d1a5498db40224f5341a5826c3bacbcad59c9fb5254ccbf36449254702102681c16a6547ff9acc4f0e613bb08c72fe94
6
+ metadata.gz: 30d707af2c9f59b38535c99bebe961f29097fa934451d3e539201f1519951f0ed306fde9afca729c64e8fff2c78ac7cda75f45b72288fc68404df3b335d42fd2
7
+ data.tar.gz: e4099f22327d3b4274bb419c80616684eed98db8d76997fbf33d57ffe96df10ace487b2df0f53fdd87a1033c387c1fbff1e1b6cc056c46a61c74e92bf54a634a
data/README.md CHANGED
@@ -77,7 +77,7 @@ AdvancedSelect does not provide query objects, model concerns, authorization log
77
77
  Add the gem to the host Rails app:
78
78
 
79
79
  ```ruby
80
- gem "advanced_select", "~> 0.1.5"
80
+ gem "advanced_select", "~> 0.1.7"
81
81
  ```
82
82
 
83
83
  Run the installer:
@@ -488,19 +488,20 @@ The partial is rendered on the server for the initial selection, and the control
488
488
 
489
489
  - `data-advanced-select-tooltip-list` on the element that holds the rows (the same hook the built-in list uses).
490
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).
491
+ - `data-advanced-select-tooltip-field="…"` on the elements inside the template that should be filled in. Available fields include `id`, `value`, `label`, `display_label`, extra option keys, and nested paths such as `test.name`; values are written with `textContent` (text only, no markup).
492
492
 
493
493
  ```erb
494
494
  <%# app/views/alternatives/_tooltip.html.erb %>
495
495
  <table>
496
496
  <thead>
497
- <tr><th>Code &amp; Name</th><th>Type</th></tr>
497
+ <tr><th>Code &amp; Name</th><th>Type</th><th>Test</th></tr>
498
498
  </thead>
499
499
  <tbody data-advanced-select-tooltip-list>
500
500
  <% selected_options.each do |option| %>
501
501
  <tr>
502
502
  <td><%= option.fetch(:display_label) %></td>
503
503
  <td><%= option.fetch(:value) %></td>
504
+ <td><%= option.dig(:test, :name) %></td>
504
505
  </tr>
505
506
  <% end %>
506
507
  </tbody>
@@ -508,6 +509,7 @@ The partial is rendered on the server for the initial selection, and the control
508
509
  <tr>
509
510
  <td data-advanced-select-tooltip-field="display_label"></td>
510
511
  <td data-advanced-select-tooltip-field="value"></td>
512
+ <td data-advanced-select-tooltip-field="test.name"></td>
511
513
  </tr>
512
514
  </template>
513
515
  </table>
@@ -39,11 +39,7 @@ export default class extends Controller {
39
39
  this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
40
40
  this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
41
41
  this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
42
- this.selectedValue = this.selectedValue.map((option) => ({
43
- ...option,
44
- id: option.id.toString(),
45
- displayLabel: option.displayLabel || option.label
46
- }))
42
+ this.selectedValue = this.selectedValue.map((option) => this.normalizeSelectedOption(option))
47
43
  this.close = this.close.bind(this)
48
44
  this.renderOptionsState()
49
45
  this.setupEagerDependentFields()
@@ -109,7 +105,7 @@ export default class extends Controller {
109
105
 
110
106
  choose(event) {
111
107
  event.preventDefault()
112
- this.selectOption(event.params.value, event.params.label, event.params.submitValue, { displayLabel: event.params.displayLabel })
108
+ this.selectOptionFromElement(event.currentTarget)
113
109
  }
114
110
 
115
111
  add(event) {
@@ -281,17 +277,28 @@ export default class extends Controller {
281
277
  }
282
278
 
283
279
  selectOption(value, label, submitValue = value, { displayLabel = label, refreshOptions = this.multipleValue } = {}) {
284
- value = value.toString()
285
- submitValue = submitValue.toString()
280
+ this.selectOptionData(
281
+ this.normalizeSelectedOption({ id: value, value: submitValue, label, displayLabel }),
282
+ { refreshOptions }
283
+ )
284
+ }
285
+
286
+ selectOptionFromElement(element, { refreshOptions = this.multipleValue } = {}) {
287
+ this.selectOptionData(this.optionData(element), { refreshOptions })
288
+ }
289
+
290
+ selectOptionData(option, { refreshOptions = this.multipleValue } = {}) {
291
+ const selectedOption = this.normalizeSelectedOption(option)
292
+ const value = selectedOption.id
286
293
 
287
294
  if (this.multipleValue) {
288
295
  if (this.selectedValue.some((option) => option.id === value)) {
289
296
  this.selectedValue = this.selectedValue.filter((option) => option.id !== value)
290
297
  } else {
291
- this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }, ...this.selectedValue]
298
+ this.selectedValue = [selectedOption, ...this.selectedValue]
292
299
  }
293
300
  } else {
294
- this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }]
301
+ this.selectedValue = [selectedOption]
295
302
  }
296
303
 
297
304
  this.renderSelection()
@@ -304,6 +311,51 @@ export default class extends Controller {
304
311
  }
305
312
  }
306
313
 
314
+ optionData(element) {
315
+ const data = this.parseOptionData(element.dataset.advancedSelectOptionParam)
316
+ const value = element.dataset.advancedSelectValueParam
317
+ const submitValue = element.dataset.advancedSelectSubmitValueParam || value
318
+ const label = element.dataset.advancedSelectLabelParam
319
+ const displayLabel = element.dataset.advancedSelectDisplayLabelParam || label
320
+
321
+ return {
322
+ ...data,
323
+ id: value,
324
+ value: submitValue,
325
+ label,
326
+ displayLabel
327
+ }
328
+ }
329
+
330
+ parseOptionData(json) {
331
+ if (!json) {
332
+ return {}
333
+ }
334
+
335
+ try {
336
+ const data = JSON.parse(json)
337
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {}
338
+ } catch (_error) {
339
+ return {}
340
+ }
341
+ }
342
+
343
+ normalizeSelectedOption(option) {
344
+ const id = option.id.toString()
345
+ const value = (option.value || id).toString()
346
+ const label = (option.label || option.displayLabel || option.display_label || value).toString()
347
+ const displayLabel = (option.displayLabel || option.display_label || label).toString()
348
+
349
+ return {
350
+ ...option,
351
+ id,
352
+ value,
353
+ label,
354
+ displayLabel,
355
+ display_label: displayLabel
356
+ }
357
+ }
358
+
307
359
  renderSelection() {
308
360
  this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
309
361
  this.summaryTarget.replaceChildren(...this.selectionElements)
@@ -383,13 +435,7 @@ export default class extends Controller {
383
435
  return
384
436
  }
385
437
 
386
- const option = options[0]
387
- this.selectOption(
388
- option.dataset.advancedSelectValueParam,
389
- option.dataset.advancedSelectLabelParam,
390
- option.dataset.advancedSelectSubmitValueParam || option.dataset.advancedSelectValueParam,
391
- { displayLabel: option.dataset.advancedSelectDisplayLabelParam, refreshOptions: false }
392
- )
438
+ this.selectOptionFromElement(options[0], { refreshOptions: false })
393
439
  }
394
440
 
395
441
  chooseActiveOption() {
@@ -401,12 +447,7 @@ export default class extends Controller {
401
447
  this.activeOption.dataset.advancedSelectDisplayLabelParam
402
448
  )
403
449
  } else {
404
- this.selectOption(
405
- this.activeOption.dataset.advancedSelectValueParam,
406
- this.activeOption.dataset.advancedSelectLabelParam,
407
- this.activeOption.dataset.advancedSelectSubmitValueParam || this.activeOption.dataset.advancedSelectValueParam,
408
- { displayLabel: this.activeOption.dataset.advancedSelectDisplayLabelParam }
409
- )
450
+ this.selectOptionFromElement(this.activeOption)
410
451
  }
411
452
  }
412
453
 
@@ -602,11 +643,29 @@ export default class extends Controller {
602
643
  return this.displayLabel(option)
603
644
  }
604
645
 
605
- const camelizedField = field.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
606
- const value = option[field] ?? option[camelizedField]
646
+ const value = this.optionFieldValue(option, field)
607
647
  return value === undefined || value === null ? "" : value.toString()
608
648
  }
609
649
 
650
+ optionFieldValue(option, field) {
651
+ const directValue = option[field] ?? option[this.camelize(field)]
652
+ if (directValue !== undefined) {
653
+ return directValue
654
+ }
655
+
656
+ return field.split(".").reduce((value, segment) => {
657
+ if (value === undefined || value === null) {
658
+ return undefined
659
+ }
660
+
661
+ return value[segment] ?? value[this.camelize(segment)]
662
+ }, option)
663
+ }
664
+
665
+ camelize(value) {
666
+ return value.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
667
+ }
668
+
610
669
  textElement(tagName, className, text) {
611
670
  const element = document.createElement(tagName)
612
671
  element.className = className
@@ -642,4 +701,4 @@ export default class extends Controller {
642
701
  get expanded() {
643
702
  return this.triggerTarget.getAttribute("aria-expanded") === "true"
644
703
  }
645
- }
704
+ }
@@ -50,12 +50,12 @@ module AdvancedSelect
50
50
 
51
51
  def advanced_select_selected_options(selected)
52
52
  advanced_select_array(selected).map do |option|
53
- {
53
+ option.to_h.merge(
54
54
  id: option.fetch(:id).to_s,
55
55
  value: advanced_select_option_value(option),
56
56
  label: advanced_select_option_label(option),
57
57
  display_label: advanced_select_option_display_label(option)
58
- }
58
+ )
59
59
  end
60
60
  end
61
61
 
@@ -79,7 +79,8 @@ module AdvancedSelect
79
79
  advanced_select_value_param: option.fetch(:id),
80
80
  advanced_select_submit_value_param: advanced_select_option_value(option),
81
81
  advanced_select_label_param: advanced_select_option_label(option),
82
- advanced_select_display_label_param: advanced_select_option_display_label(option)
82
+ advanced_select_display_label_param: advanced_select_option_display_label(option),
83
+ advanced_select_option_param: advanced_select_option_payload(option)
83
84
  }
84
85
  ) do
85
86
  safe_join([
@@ -170,6 +171,15 @@ module AdvancedSelect
170
171
  option[:description].to_s
171
172
  end
172
173
 
174
+ def advanced_select_option_payload(option)
175
+ option.to_h.merge(
176
+ id: option.fetch(:id).to_s,
177
+ value: advanced_select_option_value(option),
178
+ label: advanced_select_option_label(option),
179
+ display_label: advanced_select_option_display_label(option)
180
+ ).to_json
181
+ end
182
+
173
183
  def advanced_select_display_label(label)
174
184
  label.to_s.split(" > ").last
175
185
  end
@@ -1,3 +1,3 @@
1
1
  module AdvancedSelect
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
@@ -39,11 +39,7 @@ export default class extends Controller {
39
39
  this.optionActiveClasses = this.classList(this.element.dataset.advancedSelectOptionActiveClass || "ui-advanced-select-option-active")
40
40
  this.addOptionActiveClasses = this.classList(this.element.dataset.advancedSelectAddOptionActiveClass || "")
41
41
  this.optionSelectedClasses = this.classList(this.element.dataset.advancedSelectOptionSelectedClass || "")
42
- this.selectedValue = this.selectedValue.map((option) => ({
43
- ...option,
44
- id: option.id.toString(),
45
- displayLabel: option.displayLabel || option.label
46
- }))
42
+ this.selectedValue = this.selectedValue.map((option) => this.normalizeSelectedOption(option))
47
43
  this.close = this.close.bind(this)
48
44
  this.renderOptionsState()
49
45
  this.setupEagerDependentFields()
@@ -109,7 +105,7 @@ export default class extends Controller {
109
105
 
110
106
  choose(event) {
111
107
  event.preventDefault()
112
- this.selectOption(event.params.value, event.params.label, event.params.submitValue, { displayLabel: event.params.displayLabel })
108
+ this.selectOptionFromElement(event.currentTarget)
113
109
  }
114
110
 
115
111
  add(event) {
@@ -281,17 +277,28 @@ export default class extends Controller {
281
277
  }
282
278
 
283
279
  selectOption(value, label, submitValue = value, { displayLabel = label, refreshOptions = this.multipleValue } = {}) {
284
- value = value.toString()
285
- submitValue = submitValue.toString()
280
+ this.selectOptionData(
281
+ this.normalizeSelectedOption({ id: value, value: submitValue, label, displayLabel }),
282
+ { refreshOptions }
283
+ )
284
+ }
285
+
286
+ selectOptionFromElement(element, { refreshOptions = this.multipleValue } = {}) {
287
+ this.selectOptionData(this.optionData(element), { refreshOptions })
288
+ }
289
+
290
+ selectOptionData(option, { refreshOptions = this.multipleValue } = {}) {
291
+ const selectedOption = this.normalizeSelectedOption(option)
292
+ const value = selectedOption.id
286
293
 
287
294
  if (this.multipleValue) {
288
295
  if (this.selectedValue.some((option) => option.id === value)) {
289
296
  this.selectedValue = this.selectedValue.filter((option) => option.id !== value)
290
297
  } else {
291
- this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }, ...this.selectedValue]
298
+ this.selectedValue = [selectedOption, ...this.selectedValue]
292
299
  }
293
300
  } else {
294
- this.selectedValue = [{ id: value, value: submitValue, label, displayLabel }]
301
+ this.selectedValue = [selectedOption]
295
302
  }
296
303
 
297
304
  this.renderSelection()
@@ -304,6 +311,51 @@ export default class extends Controller {
304
311
  }
305
312
  }
306
313
 
314
+ optionData(element) {
315
+ const data = this.parseOptionData(element.dataset.advancedSelectOptionParam)
316
+ const value = element.dataset.advancedSelectValueParam
317
+ const submitValue = element.dataset.advancedSelectSubmitValueParam || value
318
+ const label = element.dataset.advancedSelectLabelParam
319
+ const displayLabel = element.dataset.advancedSelectDisplayLabelParam || label
320
+
321
+ return {
322
+ ...data,
323
+ id: value,
324
+ value: submitValue,
325
+ label,
326
+ displayLabel
327
+ }
328
+ }
329
+
330
+ parseOptionData(json) {
331
+ if (!json) {
332
+ return {}
333
+ }
334
+
335
+ try {
336
+ const data = JSON.parse(json)
337
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {}
338
+ } catch (_error) {
339
+ return {}
340
+ }
341
+ }
342
+
343
+ normalizeSelectedOption(option) {
344
+ const id = option.id.toString()
345
+ const value = (option.value || id).toString()
346
+ const label = (option.label || option.displayLabel || option.display_label || value).toString()
347
+ const displayLabel = (option.displayLabel || option.display_label || label).toString()
348
+
349
+ return {
350
+ ...option,
351
+ id,
352
+ value,
353
+ label,
354
+ displayLabel,
355
+ display_label: displayLabel
356
+ }
357
+ }
358
+
307
359
  renderSelection() {
308
360
  this.hiddenFieldsTarget.replaceChildren(...this.hiddenFieldElements)
309
361
  this.summaryTarget.replaceChildren(...this.selectionElements)
@@ -354,8 +406,6 @@ export default class extends Controller {
354
406
  return
355
407
  }
356
408
 
357
- // Delegated on document so it keeps working after a parent advanced select
358
- // rewrites its hidden input on each selection.
359
409
  this.dependentChangeListener = (event) => {
360
410
  if (event.target instanceof Element && this.dependentSelectors.some((selector) => event.target.matches(selector))) {
361
411
  this.reloadDependentOptions()
@@ -385,13 +435,7 @@ export default class extends Controller {
385
435
  return
386
436
  }
387
437
 
388
- const option = options[0]
389
- this.selectOption(
390
- option.dataset.advancedSelectValueParam,
391
- option.dataset.advancedSelectLabelParam,
392
- option.dataset.advancedSelectSubmitValueParam || option.dataset.advancedSelectValueParam,
393
- { displayLabel: option.dataset.advancedSelectDisplayLabelParam, refreshOptions: false }
394
- )
438
+ this.selectOptionFromElement(options[0], { refreshOptions: false })
395
439
  }
396
440
 
397
441
  chooseActiveOption() {
@@ -403,12 +447,7 @@ export default class extends Controller {
403
447
  this.activeOption.dataset.advancedSelectDisplayLabelParam
404
448
  )
405
449
  } else {
406
- this.selectOption(
407
- this.activeOption.dataset.advancedSelectValueParam,
408
- this.activeOption.dataset.advancedSelectLabelParam,
409
- this.activeOption.dataset.advancedSelectSubmitValueParam || this.activeOption.dataset.advancedSelectValueParam,
410
- { displayLabel: this.activeOption.dataset.advancedSelectDisplayLabelParam }
411
- )
450
+ this.selectOptionFromElement(this.activeOption)
412
451
  }
413
452
  }
414
453
 
@@ -528,10 +567,19 @@ export default class extends Controller {
528
567
  }
529
568
 
530
569
  renderTooltip() {
531
- if (!this.hasTooltipTarget || this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
570
+ if (!this.hasTooltipTarget) {
532
571
  return
533
572
  }
534
573
 
574
+ if (this.tooltipTarget.hasAttribute("data-advanced-select-tooltip-custom")) {
575
+ this.renderCustomTooltip()
576
+ return
577
+ }
578
+
579
+ this.renderBuiltInTooltip()
580
+ }
581
+
582
+ renderBuiltInTooltip() {
535
583
  const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
536
584
  if (!list) {
537
585
  return
@@ -546,6 +594,78 @@ export default class extends Controller {
546
594
  }
547
595
  }
548
596
 
597
+ renderCustomTooltip() {
598
+ const list = this.tooltipTarget.querySelector("[data-advanced-select-tooltip-list]")
599
+ const template = this.customTooltipTemplate()
600
+
601
+ if (!list || !template) {
602
+ if (this.selectedValue.length === 0) {
603
+ this.hideTooltip(true)
604
+ }
605
+ return
606
+ }
607
+
608
+ list.replaceChildren(
609
+ ...this.selectedValue.map((option) => this.customTooltipElement(template, option))
610
+ )
611
+
612
+ if (this.selectedValue.length === 0) {
613
+ this.hideTooltip(true)
614
+ }
615
+ }
616
+
617
+ customTooltipTemplate() {
618
+ if (this.customTooltipTemplateContent) {
619
+ return this.customTooltipTemplateContent
620
+ }
621
+
622
+ const template = this.tooltipTarget.querySelector("template[data-advanced-select-tooltip-template]")
623
+ if (!template) {
624
+ return null
625
+ }
626
+
627
+ this.customTooltipTemplateContent = template.content.cloneNode(true)
628
+ template.remove()
629
+ return this.customTooltipTemplateContent
630
+ }
631
+
632
+ customTooltipElement(template, option) {
633
+ const fragment = template.cloneNode(true)
634
+ fragment.querySelectorAll("[data-advanced-select-tooltip-field]").forEach((element) => {
635
+ element.textContent = this.tooltipFieldValue(option, element.dataset.advancedSelectTooltipField)
636
+ })
637
+
638
+ return fragment
639
+ }
640
+
641
+ tooltipFieldValue(option, field) {
642
+ if (field === "display_label" || field === "displayLabel") {
643
+ return this.displayLabel(option)
644
+ }
645
+
646
+ const value = this.optionFieldValue(option, field)
647
+ return value === undefined || value === null ? "" : value.toString()
648
+ }
649
+
650
+ optionFieldValue(option, field) {
651
+ const directValue = option[field] ?? option[this.camelize(field)]
652
+ if (directValue !== undefined) {
653
+ return directValue
654
+ }
655
+
656
+ return field.split(".").reduce((value, segment) => {
657
+ if (value === undefined || value === null) {
658
+ return undefined
659
+ }
660
+
661
+ return value[segment] ?? value[this.camelize(segment)]
662
+ }, option)
663
+ }
664
+
665
+ camelize(value) {
666
+ return value.replace(/_([a-z])/g, (_match, character) => character.toUpperCase())
667
+ }
668
+
549
669
  textElement(tagName, className, text) {
550
670
  const element = document.createElement(tagName)
551
671
  element.className = className
@@ -581,4 +701,4 @@ export default class extends Controller {
581
701
  get expanded() {
582
702
  return this.triggerTarget.getAttribute("aria-expanded") === "true"
583
703
  }
584
- }
704
+ }
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.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik