hotwire_combobox 0.1.32 → 0.1.34

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -4
  3. data/app/assets/javascripts/controllers/hw_combobox_controller.js +12 -5
  4. data/app/assets/javascripts/{helpers.js → hw_combobox/helpers.js} +6 -4
  5. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/actors.js +1 -1
  6. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/async_loading.js +1 -1
  7. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/autocomplete.js +21 -6
  8. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/dialog.js +3 -3
  9. data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +57 -0
  10. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/navigation.js +2 -2
  11. data/app/assets/javascripts/hw_combobox/models/combobox/new_options.js +37 -0
  12. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/options.js +2 -10
  13. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/selection.js +18 -6
  14. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/toggle.js +3 -5
  15. data/app/assets/javascripts/{models → hw_combobox/models}/combobox/validity.js +1 -1
  16. data/app/assets/javascripts/hw_combobox/models/combobox.js +15 -0
  17. data/app/assets/stylesheets/hotwire_combobox.css +0 -1
  18. data/app/presenters/hotwire_combobox/component.rb +11 -5
  19. data/app/presenters/hotwire_combobox/listbox/option.rb +6 -6
  20. data/app/views/hotwire_combobox/_pagination.html.erb +1 -1
  21. data/lib/hotwire_combobox/engine.rb +1 -1
  22. data/lib/hotwire_combobox/helper.rb +28 -16
  23. data/lib/hotwire_combobox/version.rb +1 -1
  24. metadata +20 -22
  25. data/app/assets/javascripts/controllers/application.js +0 -9
  26. data/app/assets/javascripts/controllers/index.js +0 -3
  27. data/app/assets/javascripts/hotwire_combobox_application.js +0 -1
  28. data/app/assets/javascripts/models/combobox/filtering.js +0 -57
  29. data/app/assets/javascripts/models/combobox.js +0 -14
  30. /data/app/assets/javascripts/{models → hw_combobox/models}/combobox/base.js +0 -0
  31. /data/app/assets/javascripts/{vendor → hw_combobox/vendor}/bodyScrollLock.js +0 -0
  32. /data/app/assets/javascripts/{vendor → hw_combobox/vendor}/requestjs.js +0 -0
  33. /data/config/{importmap.rb → hw_importmap.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb06e6c688920de5447b5fbea7fd20c242e228a7d335b8e9841cd07c7df03cb0
4
- data.tar.gz: 338aad9ff496b0449e509543698c45b7581cbe9d6c13e869b25a86fa8819b726
3
+ metadata.gz: d3bf6f83f984a36fb6e0491f68a71c9386c3f2a00e6b6082cd490b73e58d652d
4
+ data.tar.gz: db089a58628fa02b4a173f18053a9a81a21402eeafdcfb943d403989597bf314
5
5
  SHA512:
6
- metadata.gz: 84e88c954657e5f527ac5b564ff210ddeb42cd8e9dcb33a5ef40a9882d6edabbbe40fd53211a95b46056336399babf728bb11c7e5993dd845e2a8cbc86fe7b47
7
- data.tar.gz: 9803e31d7244ec3b87d6a6e0dc3532db155f206a540c69d4ee07d586e5e781da0dc5a267e5fa9c8bd4394b006272cfa361d511fb48c29c4d37d7c9a03627107b
6
+ metadata.gz: '0483ae80475b1d52b9b74d21c5749ee0300914954f74e40267de50cfb96dbb77e44a8bdfb5448caf9d743798e2621d678986927c5f201c40052e049c2844a13f'
7
+ data.tar.gz: 594db1df1c85dd096ca2cebf2ff9a7dc6564e061aee5f4403729eb94eaf55ab587f32ea620b0f359bd5f1c41e2e4c70096fb5ef04230a07ee13d6cda7873cd33
data/README.md CHANGED
@@ -18,11 +18,9 @@ Add this line to your application's Gemfile and run `bundle install`:
18
18
  gem "hotwire_combobox"
19
19
  ```
20
20
 
21
- ## Docs
21
+ Only apps that use importmaps are currently supported. Suport for other JS solutions is in progress.
22
22
 
23
- <p align="center">
24
- <img src="docs/assets/images/docs-preview.png" height=500>
25
- </p>
23
+ ## Docs
26
24
 
27
25
  Visit [the docs site](https://hotwirecombobox.com/) for a demo and detailed documentation.
28
26
  If the site is down, you can run the docs locally by cloning [the docs repo](https://github.com/josefarias/hotwire_combobox_docs).
@@ -1,5 +1,5 @@
1
- import Combobox from "models/combobox"
2
- import { Concerns } from "helpers"
1
+ import Combobox from "hw_combobox/models/combobox"
2
+ import { Concerns } from "hw_combobox/helpers"
3
3
  import { Controller } from "@hotwired/stimulus"
4
4
 
5
5
  const concerns = [
@@ -10,6 +10,7 @@ const concerns = [
10
10
  Combobox.Dialog,
11
11
  Combobox.Filtering,
12
12
  Combobox.Navigation,
13
+ Combobox.NewOptions,
13
14
  Combobox.Options,
14
15
  Combobox.Selection,
15
16
  Combobox.Toggle,
@@ -28,10 +29,10 @@ export default class HwComboboxController extends Concerns(...concerns) {
28
29
  "dialogCombobox",
29
30
  "dialogFocusTrap",
30
31
  "dialogListbox",
32
+ "endOfOptionsStream",
31
33
  "handle",
32
34
  "hiddenField",
33
- "listbox",
34
- "paginationFrame"
35
+ "listbox"
35
36
  ]
36
37
 
37
38
  static values = {
@@ -69,7 +70,13 @@ export default class HwComboboxController extends Concerns(...concerns) {
69
70
  }
70
71
  }
71
72
 
72
- paginationFrameTargetConnected() {
73
+ endOfOptionsStreamTargetConnected(element) {
74
+ const inputType = element.dataset.inputType
75
+
73
76
  this._preselectOption()
77
+
78
+ if (inputType) {
79
+ this._commitFilter({ inputType })
80
+ }
74
81
  }
75
82
  }
@@ -1,3 +1,5 @@
1
+ export const nullEvent = new Event("NULL")
2
+
1
3
  export function Concerns(Base, ...mixins) {
2
4
  return mixins.reduce((accumulator, current) => current(accumulator), Base)
3
5
  }
@@ -37,10 +39,6 @@ export function startsWith(string, substring) {
37
39
  return string.toLowerCase().startsWith(substring.toLowerCase())
38
40
  }
39
41
 
40
- export function nextFrame() {
41
- return new Promise(requestAnimationFrame)
42
- }
43
-
44
42
  export function debounce(fn, delay = 150) {
45
43
  let timeoutId = null
46
44
 
@@ -50,3 +48,7 @@ export function debounce(fn, delay = 150) {
50
48
  timeoutId = setTimeout(callback, delay)
51
49
  }
52
50
  }
51
+
52
+ export function isDeleteEvent(event) {
53
+ return event.inputType === "deleteContentBackward" || event.inputType === "deleteWordBackward"
54
+ }
@@ -1,4 +1,4 @@
1
- import Combobox from "models/combobox/base"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
2
 
3
3
  Combobox.Actors = Base => class extends Base {
4
4
  _initializeActors() {
@@ -1,4 +1,4 @@
1
- import Combobox from "models/combobox/base"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
2
 
3
3
  Combobox.AsyncLoading = Base => class extends Base {
4
4
  get _isAsync() {
@@ -1,5 +1,5 @@
1
- import Combobox from "models/combobox/base"
2
- import { startsWith } from "helpers"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { startsWith } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Autocomplete = Base => class extends Base {
5
5
  _connectListAutocomplete() {
@@ -8,17 +8,17 @@ Combobox.Autocomplete = Base => class extends Base {
8
8
  }
9
9
  }
10
10
 
11
- _maybeAutocompleteWith(option, { force }) {
11
+ _autocompleteWith(option, { force }) {
12
12
  if (!this._autocompletesInline && !force) return
13
13
 
14
- const typedValue = this._actingCombobox.value
14
+ const typedValue = this._query
15
15
  const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
16
16
 
17
17
  if (force) {
18
- this._actingCombobox.value = autocompletedValue
18
+ this._query = autocompletedValue
19
19
  this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
20
20
  } else if (startsWith(autocompletedValue, typedValue)) {
21
- this._actingCombobox.value = autocompletedValue
21
+ this._query = autocompletedValue
22
22
  this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
23
23
  }
24
24
  }
@@ -29,6 +29,17 @@ Combobox.Autocomplete = Base => class extends Base {
29
29
  this.listboxTarget.style.display = "none"
30
30
  }
31
31
 
32
+ get _isExactAutocompleteMatch() {
33
+ return this._immediatelyAutocompletableValue === this._query
34
+ }
35
+
36
+ // All `_isExactAutocompleteMatch` matches are `_isPartialAutocompleteMatch` matches
37
+ // but not all `_isPartialAutocompleteMatch` matches are `_isExactAutocompleteMatch` matches.
38
+ get _isPartialAutocompleteMatch() {
39
+ return !!this._immediatelyAutocompletableValue &&
40
+ startsWith(this._immediatelyAutocompletableValue, this._query)
41
+ }
42
+
32
43
  get _autocompletesList() {
33
44
  return this.autocompleteValue === "both" || this.autocompleteValue === "list"
34
45
  }
@@ -36,4 +47,8 @@ Combobox.Autocomplete = Base => class extends Base {
36
47
  get _autocompletesInline() {
37
48
  return this.autocompleteValue === "both" || this.autocompleteValue === "inline"
38
49
  }
50
+
51
+ get _immediatelyAutocompletableValue() {
52
+ return this._visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
53
+ }
39
54
  }
@@ -1,4 +1,4 @@
1
- import Combobox from "models/combobox/base"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
2
 
3
3
  Combobox.Dialog = Base => class extends Base {
4
4
  _connectDialog() {
@@ -14,7 +14,7 @@ Combobox.Dialog = Base => class extends Base {
14
14
  }
15
15
 
16
16
  _moveArtifactsToDialog() {
17
- this.dialogComboboxTarget.value = this._actingCombobox.value
17
+ this.dialogComboboxTarget.value = this._query
18
18
 
19
19
  this._actingCombobox = this.dialogComboboxTarget
20
20
  this._actingListbox = this.dialogListboxTarget
@@ -23,7 +23,7 @@ Combobox.Dialog = Base => class extends Base {
23
23
  }
24
24
 
25
25
  _moveArtifactsInline() {
26
- this.comboboxTarget.value = this._actingCombobox.value
26
+ this.comboboxTarget.value = this._query
27
27
 
28
28
  this._actingCombobox = this.comboboxTarget
29
29
  this._actingListbox = this.listboxTarget
@@ -0,0 +1,57 @@
1
+
2
+ import Combobox from "hw_combobox/models/combobox/base"
3
+ import { applyFilter, debounce, isDeleteEvent } from "hw_combobox/helpers"
4
+ import { get } from "hw_combobox/vendor/requestjs"
5
+
6
+ Combobox.Filtering = Base => class extends Base {
7
+ filter(event) {
8
+ if (this._isAsync) {
9
+ this._debouncedFilterAsync(event)
10
+ } else {
11
+ this._filterSync(event)
12
+ }
13
+ }
14
+
15
+ _initializeFiltering() {
16
+ this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
17
+ }
18
+
19
+ _debouncedFilterAsync(event) {
20
+ this._filterAsync(event)
21
+ }
22
+
23
+ async _filterAsync(event) {
24
+ const query = { q: this._query, input_type: event.inputType }
25
+ await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
26
+ }
27
+
28
+ _filterSync(event) {
29
+ this.open()
30
+ this._allOptionElements.forEach(applyFilter(this._query, { matching: this.filterableAttributeValue }))
31
+ this._commitFilter(event)
32
+ }
33
+
34
+ _commitFilter(event) {
35
+ if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
36
+ this._selectNew()
37
+ } else if (isDeleteEvent(event)) {
38
+ this._deselect()
39
+ } else {
40
+ this._select(this._visibleOptionElements[0])
41
+ }
42
+ }
43
+
44
+ get _isQueried() {
45
+ return this._query.length > 0
46
+ }
47
+
48
+ // Consider +_query+ will contain the full autocompleted value
49
+ // after a certain point in the call chain.
50
+ get _query() {
51
+ return this._actingCombobox.value.trim()
52
+ }
53
+
54
+ set _query(value) {
55
+ this._actingCombobox.value = value
56
+ }
57
+ }
@@ -1,5 +1,5 @@
1
- import Combobox from "models/combobox/base"
2
- import { cancel } from "helpers"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { cancel } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Navigation = Base => class extends Base {
5
5
  navigate(event) {
@@ -0,0 +1,37 @@
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+
3
+ Combobox.NewOptions = Base => class extends Base {
4
+ _shouldTreatAsNewOptionForFiltering(queryIsBeingRefined) {
5
+ if (queryIsBeingRefined) {
6
+ return this._isNewOptionWithNoPotentialMatches
7
+ } else {
8
+ return this._isNewOptionWithPotentialMatches
9
+ }
10
+ }
11
+
12
+ // If the user is going to keep refining the query, we can't be sure whether
13
+ // the option will end up being new or not unless there are no potential matches.
14
+ // +_isNewOptionWithNoPotentialMatches+ allows us to make our best guess
15
+ // while the state of the combobox is still in flux.
16
+ //
17
+ // It's okay for the combobox to say it's not new even if it will be eventually,
18
+ // as only the final state matters for submission purposes. This method exists
19
+ // as a best effort to keep the state accurate as often as we can.
20
+ //
21
+ // Note that the first visible option is automatically selected as you type.
22
+ // So if there's a partial match, it's not a new option at this point.
23
+ //
24
+ // The final state is locked-in upon closing the combobox via `_isNewOptionWithPotentialMatches`.
25
+ get _isNewOptionWithNoPotentialMatches() {
26
+ return this._isNewOptionWithPotentialMatches && !this._isPartialAutocompleteMatch
27
+ }
28
+
29
+ // If the query is finalized, we don't care that there are potential matches
30
+ // because new options can be substrings of existing options.
31
+ //
32
+ // We can't use `_isNewOptionWithNoPotentialMatches` because that would
33
+ // rule out new options that are partial matches.
34
+ get _isNewOptionWithPotentialMatches() {
35
+ return this._isQueried && this._allowNew && !this._isExactAutocompleteMatch
36
+ }
37
+ }
@@ -1,5 +1,5 @@
1
- import Combobox from "models/combobox/base"
2
- import { visible, startsWith } from "helpers"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { visible } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Options = Base => class extends Base {
5
5
  _resetOptions() {
@@ -7,14 +7,6 @@ Combobox.Options = Base => class extends Base {
7
7
  this.hiddenFieldTarget.name = this.originalNameValue
8
8
  }
9
9
 
10
- _isValidNewOption(query, { ignoreAutocomplete = false } = {}) {
11
- const typedValue = this._actingCombobox.value
12
- const autocompletedValue = this._visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
13
- const insufficientAutocomplete = !autocompletedValue || !startsWith(autocompletedValue, typedValue)
14
-
15
- return query.length > 0 && this._allowNew && (ignoreAutocomplete || insufficientAutocomplete)
16
- }
17
-
18
10
  get _allowNew() {
19
11
  return !!this.nameWhenNewValue
20
12
  }
@@ -1,15 +1,16 @@
1
- import Combobox from "models/combobox/base"
2
- import { wrapAroundAccess } from "helpers"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { wrapAroundAccess, nullEvent } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Selection = Base => class extends Base {
5
5
  selectOption(event) {
6
6
  this._select(event.currentTarget)
7
+ this.filter(event)
7
8
  this.close()
8
9
  }
9
10
 
10
11
  _connectSelection() {
11
12
  if (this.hasPrefilledDisplayValue) {
12
- this._actingCombobox.value = this.prefilledDisplayValue
13
+ this._query = this.prefilledDisplayValue
13
14
  }
14
15
  }
15
16
 
@@ -18,7 +19,7 @@ Combobox.Selection = Base => class extends Base {
18
19
 
19
20
  if (option) {
20
21
  this._markValid()
21
- this._maybeAutocompleteWith(option, { force })
22
+ this._autocompleteWith(option, { force })
22
23
  this._commitSelection(option, { selected: true })
23
24
  } else {
24
25
  this._markInvalid()
@@ -49,9 +50,9 @@ Combobox.Selection = Base => class extends Base {
49
50
  if (option) this._commitSelection(option, { selected: false })
50
51
  }
51
52
 
52
- _selectNew(query) {
53
+ _selectNew() {
53
54
  this._resetOptions()
54
- this.hiddenFieldTarget.value = query
55
+ this.hiddenFieldTarget.value = this._query
55
56
  this.hiddenFieldTarget.name = this.nameWhenNewValue
56
57
  }
57
58
 
@@ -70,7 +71,18 @@ Combobox.Selection = Base => class extends Base {
70
71
  }
71
72
  }
72
73
 
74
+ _selectFuzzyMatch() {
75
+ if (this._isFuzzyMatch) {
76
+ this._select(this._visibleOptionElements[0], { force: true })
77
+ this.filter(nullEvent)
78
+ }
79
+ }
80
+
73
81
  get _hasValueButNoSelection() {
74
82
  return this.hiddenFieldTarget.value && !this._selectedOptionElement
75
83
  }
84
+
85
+ get _isFuzzyMatch() {
86
+ return this._isQueried && !!this._visibleOptionElements[0] && !this._isNewOptionWithPotentialMatches
87
+ }
76
88
  }
@@ -1,5 +1,5 @@
1
- import Combobox from "models/combobox/base"
2
- import { disableBodyScroll, enableBodyScroll } from "vendor/bodyScrollLock"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+ import { disableBodyScroll, enableBodyScroll } from "hw_combobox/vendor/bodyScrollLock"
3
3
 
4
4
  Combobox.Toggle = Base => class extends Base {
5
5
  open() {
@@ -51,9 +51,7 @@ Combobox.Toggle = Base => class extends Base {
51
51
  }
52
52
 
53
53
  _ensureSelection() {
54
- if (!this._isValidNewOption(this._actingCombobox.value, { ignoreAutocomplete: true })) {
55
- this._select(this._selectedOptionElement, { force: true })
56
- }
54
+ this._selectFuzzyMatch()
57
55
  }
58
56
 
59
57
  _openByFocusing() {
@@ -1,4 +1,4 @@
1
- import Combobox from "models/combobox/base"
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
2
 
3
3
  Combobox.Validity = Base => class extends Base {
4
4
  _markValid() {
@@ -0,0 +1,15 @@
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+
3
+ import "hw_combobox/models/combobox/actors"
4
+ import "hw_combobox/models/combobox/async_loading"
5
+ import "hw_combobox/models/combobox/autocomplete"
6
+ import "hw_combobox/models/combobox/dialog"
7
+ import "hw_combobox/models/combobox/filtering"
8
+ import "hw_combobox/models/combobox/navigation"
9
+ import "hw_combobox/models/combobox/new_options"
10
+ import "hw_combobox/models/combobox/options"
11
+ import "hw_combobox/models/combobox/selection"
12
+ import "hw_combobox/models/combobox/toggle"
13
+ import "hw_combobox/models/combobox/validity"
14
+
15
+ export default Combobox
@@ -47,7 +47,6 @@
47
47
  margin: 0;
48
48
  padding: 0;
49
49
  position: relative;
50
- isolation: isolate;
51
50
 
52
51
  &, * {
53
52
  box-sizing: border-box;
@@ -19,7 +19,7 @@ class HotwireCombobox::Component
19
19
  **rest
20
20
  @view, @autocomplete, @id, @name, @value, @form, @async_src,
21
21
  @name_when_new, @open, @data, @mobile_at, @options, @dialog_label =
22
- view, autocomplete, id, name, value, form, async_src,
22
+ view, autocomplete, id, name.to_s, value, form, async_src,
23
23
  name_when_new, open, data, mobile_at, options, dialog_label
24
24
 
25
25
  @combobox_attrs = input.reverse_merge(rest).with_indifferent_access
@@ -146,8 +146,8 @@ class HotwireCombobox::Component
146
146
  :association_name
147
147
 
148
148
  def infer_association_name
149
- if name.to_s.include?("_id")
150
- name.to_s.sub(/_id\z/, "")
149
+ if name.include?("_id")
150
+ name.sub(/_id\z/, "")
151
151
  end
152
152
  end
153
153
 
@@ -171,7 +171,7 @@ class HotwireCombobox::Component
171
171
  if async_src && associated_object
172
172
  associated_object.to_combobox_display
173
173
  elsif hidden_field_value
174
- options.find { |option| option.value == hidden_field_value }&.content
174
+ options.find { |option| option.value == hidden_field_value }&.autocompletable_as
175
175
  end
176
176
  end
177
177
 
@@ -204,7 +204,13 @@ class HotwireCombobox::Component
204
204
  end
205
205
 
206
206
  def hidden_field_value
207
- form&.object&.public_send(name) || value
207
+ return value if value
208
+
209
+ if form&.object&.defined_enums&.try :[], name
210
+ form.object.public_send "#{name}_before_type_cast"
211
+ else
212
+ form&.object&.try name
213
+ end
208
214
  end
209
215
 
210
216
 
@@ -11,8 +11,8 @@ class HotwireCombobox::Listbox::Option
11
11
  option.try(:value) || option.id
12
12
  end
13
13
 
14
- def content
15
- option.try(:content) || option.try(:display)
14
+ def autocompletable_as
15
+ option.try(:autocompletable_as) || option.try(:display)
16
16
  end
17
17
 
18
18
  private
@@ -42,11 +42,11 @@ class HotwireCombobox::Listbox::Option
42
42
  }
43
43
  end
44
44
 
45
- def filterable_as
46
- option.try(:filterable_as) || option.try(:display)
45
+ def content
46
+ option.try(:content) || option.try(:display)
47
47
  end
48
48
 
49
- def autocompletable_as
50
- option.try(:autocompletable_as) || option.try(:display)
49
+ def filterable_as
50
+ option.try(:filterable_as) || option.try(:display)
51
51
  end
52
52
  end
@@ -1,4 +1,4 @@
1
1
  <%# locals: (for_id:, src:) -%>
2
2
 
3
3
  <%= turbo_frame_tag hw_pagination_frame_id(for_id), src: src, loading: :lazy,
4
- data: { hw_combobox_target: "paginationFrame" } %>
4
+ data: { hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type] } %>
@@ -21,7 +21,7 @@ module HotwireCombobox
21
21
 
22
22
  initializer "hotwire_combobox.importmap", before: "importmap" do |app|
23
23
  if Rails.application.respond_to?(:importmap)
24
- app.config.importmap.paths << Engine.root.join("config/importmap.rb")
24
+ app.config.importmap.paths << Engine.root.join("config/hw_importmap.rb")
25
25
  end
26
26
  end
27
27
 
@@ -27,13 +27,8 @@ module HotwireCombobox
27
27
  if options.first.is_a? HotwireCombobox::Listbox::Option
28
28
  options
29
29
  else
30
- content = if render_in.present?
31
- ->(object) { render(**render_in.merge(object: object)) }
32
- else
33
- methods[:content]
34
- end
35
-
36
- hw_parse_combobox_options options, **methods.merge(display: display, content: content)
30
+ render_in_proc = ->(object) { render(**render_in.merge(object: object)) } if render_in.present?
31
+ hw_parse_combobox_options options, render_in: render_in_proc, **methods.merge(display: display)
37
32
  end
38
33
  end
39
34
  hw_alias :hw_combobox_options
@@ -86,19 +81,36 @@ module HotwireCombobox
86
81
  url_or_path
87
82
  end
88
83
 
89
- def hw_parse_combobox_options(options, **methods)
84
+ def hw_parse_combobox_options(options, render_in: nil, **methods)
90
85
  options.map do |option|
91
- attrs = option.is_a?(Hash) ? option : hw_option_attrs_for_obj(option, **methods)
92
- HotwireCombobox::Listbox::Option.new **attrs
86
+ HotwireCombobox::Listbox::Option.new **hw_option_attrs_for(option, render_in: render_in, **methods)
93
87
  end
94
88
  end
95
89
 
96
- def hw_option_attrs_for_obj(option, **methods)
97
- {}.tap do |attrs|
98
- attrs[:id] = hw_call_method_or_proc(option, methods[:id]) if methods[:id]
99
- attrs[:value] = hw_call_method_or_proc(option, methods[:value] || :id)
100
- attrs[:display] = hw_call_method_or_proc(option, methods[:display]) if methods[:display]
101
- attrs[:content] = hw_call_method_or_proc(option, methods[:content]) if methods[:content]
90
+ def hw_option_attrs_for(option, render_in: nil, **methods)
91
+ case option
92
+ when Hash
93
+ option
94
+ when String
95
+ {}.tap do |attrs|
96
+ attrs[:display] = option
97
+ attrs[:value] = option
98
+ attrs[:content] = render_in.(option) if render_in
99
+ end
100
+ when Array
101
+ {}.tap do |attrs|
102
+ attrs[:display] = option.first
103
+ attrs[:value] = option.last
104
+ attrs[:content] = render_in.(option.first) if render_in
105
+ end
106
+ else
107
+ {}.tap do |attrs|
108
+ attrs[:value] = hw_call_method_or_proc(option, methods[:value] || :id)
109
+
110
+ attrs[:id] = hw_call_method_or_proc(option, methods[:id]) if methods[:id]
111
+ attrs[:display] = hw_call_method_or_proc(option, methods[:display]) if methods[:display]
112
+ attrs[:content] = hw_call_method_or_proc(option, render_in || methods[:content]) if render_in || methods[:content]
113
+ end
102
114
  end
103
115
  end
104
116
 
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.1.32"
2
+ VERSION = "0.1.34"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotwire_combobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.32
4
+ version: 0.1.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Farias
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-08 00:00:00.000000000 Z
11
+ date: 2024-02-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -63,25 +63,23 @@ files:
63
63
  - README.md
64
64
  - Rakefile
65
65
  - app/assets/config/hw_combobox_manifest.js
66
- - app/assets/javascripts/controllers/application.js
67
66
  - app/assets/javascripts/controllers/hw_combobox_controller.js
68
- - app/assets/javascripts/controllers/index.js
69
- - app/assets/javascripts/helpers.js
70
- - app/assets/javascripts/hotwire_combobox_application.js
71
- - app/assets/javascripts/models/combobox.js
72
- - app/assets/javascripts/models/combobox/actors.js
73
- - app/assets/javascripts/models/combobox/async_loading.js
74
- - app/assets/javascripts/models/combobox/autocomplete.js
75
- - app/assets/javascripts/models/combobox/base.js
76
- - app/assets/javascripts/models/combobox/dialog.js
77
- - app/assets/javascripts/models/combobox/filtering.js
78
- - app/assets/javascripts/models/combobox/navigation.js
79
- - app/assets/javascripts/models/combobox/options.js
80
- - app/assets/javascripts/models/combobox/selection.js
81
- - app/assets/javascripts/models/combobox/toggle.js
82
- - app/assets/javascripts/models/combobox/validity.js
83
- - app/assets/javascripts/vendor/bodyScrollLock.js
84
- - app/assets/javascripts/vendor/requestjs.js
67
+ - app/assets/javascripts/hw_combobox/helpers.js
68
+ - app/assets/javascripts/hw_combobox/models/combobox.js
69
+ - app/assets/javascripts/hw_combobox/models/combobox/actors.js
70
+ - app/assets/javascripts/hw_combobox/models/combobox/async_loading.js
71
+ - app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js
72
+ - app/assets/javascripts/hw_combobox/models/combobox/base.js
73
+ - app/assets/javascripts/hw_combobox/models/combobox/dialog.js
74
+ - app/assets/javascripts/hw_combobox/models/combobox/filtering.js
75
+ - app/assets/javascripts/hw_combobox/models/combobox/navigation.js
76
+ - app/assets/javascripts/hw_combobox/models/combobox/new_options.js
77
+ - app/assets/javascripts/hw_combobox/models/combobox/options.js
78
+ - app/assets/javascripts/hw_combobox/models/combobox/selection.js
79
+ - app/assets/javascripts/hw_combobox/models/combobox/toggle.js
80
+ - app/assets/javascripts/hw_combobox/models/combobox/validity.js
81
+ - app/assets/javascripts/hw_combobox/vendor/bodyScrollLock.js
82
+ - app/assets/javascripts/hw_combobox/vendor/requestjs.js
85
83
  - app/assets/stylesheets/hotwire_combobox.css
86
84
  - app/presenters/hotwire_combobox/component.rb
87
85
  - app/presenters/hotwire_combobox/listbox/option.rb
@@ -93,7 +91,7 @@ files:
93
91
  - app/views/hotwire_combobox/combobox/_hidden_field.html.erb
94
92
  - app/views/hotwire_combobox/combobox/_input.html.erb
95
93
  - app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb
96
- - config/importmap.rb
94
+ - config/hw_importmap.rb
97
95
  - lib/hotwire_combobox.rb
98
96
  - lib/hotwire_combobox/engine.rb
99
97
  - lib/hotwire_combobox/helper.rb
@@ -120,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
118
  - !ruby/object:Gem::Version
121
119
  version: '0'
122
120
  requirements: []
123
- rubygems_version: 3.4.18
121
+ rubygems_version: 3.5.6
124
122
  signing_key:
125
123
  specification_version: 4
126
124
  summary: Autocomplete for Rails apps using Hotwire
@@ -1,9 +0,0 @@
1
- import { Application } from "@hotwired/stimulus"
2
-
3
- const application = Application.start()
4
-
5
- // Configure Stimulus development experience
6
- application.debug = false
7
- window.Stimulus = application
8
-
9
- export { application }
@@ -1,3 +0,0 @@
1
- import { application } from "controllers/application"
2
- import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
3
- eagerLoadControllersFrom("controllers", application)
@@ -1 +0,0 @@
1
- import "controllers"
@@ -1,57 +0,0 @@
1
-
2
- import Combobox from "models/combobox/base"
3
- import { applyFilter, nextFrame, debounce } from "helpers"
4
- import { get } from "vendor/requestjs"
5
-
6
- Combobox.Filtering = Base => class extends Base {
7
- filter(event) {
8
- if (this._isAsync) {
9
- this._debouncedFilterAsync(event)
10
- } else {
11
- this._filterSync(event)
12
- }
13
- }
14
-
15
- _initializeFiltering() {
16
- this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
17
- }
18
-
19
- _debouncedFilterAsync(event) {
20
- this._filterAsync(event)
21
- }
22
-
23
- async _filterAsync(event) {
24
- const q = this._actingCombobox.value.trim()
25
-
26
- await get(this.asyncSrcValue, { responseKind: "turbo-stream", query: { q } })
27
-
28
- this._afterTurboStreamRender(() => this._commitFilter(q, event))
29
- }
30
-
31
- _filterSync(event) {
32
- const query = this._actingCombobox.value.trim()
33
-
34
- this.open()
35
-
36
- this._allOptionElements.forEach(applyFilter(query, { matching: this.filterableAttributeValue }))
37
-
38
- this._commitFilter(query, event)
39
- }
40
-
41
- _commitFilter(query, event) {
42
- const isDeleting = event.inputType === "deleteContentBackward"
43
-
44
- if (this._isValidNewOption(query, { ignoreAutocomplete: isDeleting })) {
45
- this._selectNew(query)
46
- } else if (isDeleting) {
47
- this._deselect()
48
- } else {
49
- this._select(this._visibleOptionElements[0])
50
- }
51
- }
52
-
53
- async _afterTurboStreamRender(callback) {
54
- await nextFrame()
55
- callback()
56
- }
57
- }
@@ -1,14 +0,0 @@
1
- import Combobox from "models/combobox/base"
2
-
3
- import "models/combobox/actors"
4
- import "models/combobox/async_loading"
5
- import "models/combobox/autocomplete"
6
- import "models/combobox/dialog"
7
- import "models/combobox/filtering"
8
- import "models/combobox/navigation"
9
- import "models/combobox/options"
10
- import "models/combobox/selection"
11
- import "models/combobox/toggle"
12
- import "models/combobox/validity"
13
-
14
- export default Combobox
File without changes