hotwire_combobox 0.1.35 → 0.1.37
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 +4 -4
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +7 -3
- data/app/assets/javascripts/hw_combobox/helpers.js +13 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/actors.js +8 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +6 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +2 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +10 -8
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +16 -10
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +1 -2
- data/app/presenters/hotwire_combobox/component.rb +7 -14
- data/app/presenters/hotwire_combobox/listbox/option.rb +3 -1
- data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +2 -1
- data/app/views/hotwire_combobox/_paginated_options.turbo_stream.erb +1 -1
- data/app/views/hotwire_combobox/_pagination.html.erb +5 -2
- data/app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb +2 -4
- data/lib/hotwire_combobox/helper.rb +6 -2
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85fbe2e5c1b8d503be62eceae58650266582ac3428be6558c0af8bb76ec5f03c
|
4
|
+
data.tar.gz: f13eb27444b1e530391a8058dc6f00a6d46d9df4af75eb2de2cd28a568851f0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 207509d422415ad6684e08b09690b2244f6fde41e3a9f92a6e90ddf7808f1d0a43d1966f48847c52e133e48e8d81d8139dd2883053dd7b44ed5739d7ab2d62fc
|
7
|
+
data.tar.gz: 184048c9ec147b4d28abf8a3c0d189fa6b73e72a9ef47c6db4c9cf3a595b880287d4e0ddc985d785a38fbce1b9a58279825c46099f800492368ae2cafe349a30
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox"
|
2
|
-
import { Concerns } from "hw_combobox/helpers"
|
2
|
+
import { Concerns, sleep } from "hw_combobox/helpers"
|
3
3
|
import { Controller } from "@hotwired/stimulus"
|
4
4
|
|
5
|
+
window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0 // ms, for testing purposes
|
6
|
+
|
5
7
|
const concerns = [
|
6
8
|
Controller,
|
7
9
|
Combobox.Actors,
|
@@ -70,10 +72,12 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
70
72
|
}
|
71
73
|
}
|
72
74
|
|
73
|
-
endOfOptionsStreamTargetConnected(element) {
|
75
|
+
async endOfOptionsStreamTargetConnected(element) {
|
74
76
|
const inputType = element.dataset.inputType
|
77
|
+
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
|
75
78
|
|
76
|
-
if (inputType && inputType !== "hw:
|
79
|
+
if (inputType && inputType !== "hw:lockInSelection") {
|
80
|
+
if (delay) await sleep(delay)
|
77
81
|
this._commitFilter({ inputType })
|
78
82
|
} else {
|
79
83
|
this._preselectOption()
|
@@ -37,7 +37,7 @@ export function startsWith(string, substring) {
|
|
37
37
|
return string.toLowerCase().startsWith(substring.toLowerCase())
|
38
38
|
}
|
39
39
|
|
40
|
-
export function debounce(fn, delay =
|
40
|
+
export function debounce(fn, delay = 150) {
|
41
41
|
let timeoutId = null
|
42
42
|
|
43
43
|
return (...args) => {
|
@@ -50,3 +50,15 @@ export function debounce(fn, delay = 300) {
|
|
50
50
|
export function isDeleteEvent(event) {
|
51
51
|
return event.inputType === "deleteContentBackward" || event.inputType === "deleteWordBackward"
|
52
52
|
}
|
53
|
+
|
54
|
+
export function sleep(ms) {
|
55
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
56
|
+
}
|
57
|
+
|
58
|
+
export function unselectedPortion(element) {
|
59
|
+
if (element.selectionStart === element.selectionEnd) {
|
60
|
+
return element.value
|
61
|
+
} else {
|
62
|
+
return element.value.substring(0, element.selectionStart)
|
63
|
+
}
|
64
|
+
}
|
@@ -6,6 +6,10 @@ Combobox.Actors = Base => class extends Base {
|
|
6
6
|
this._actingCombobox = this.comboboxTarget
|
7
7
|
}
|
8
8
|
|
9
|
+
_forAllComboboxes(callback) {
|
10
|
+
this._allComboboxes.forEach(callback)
|
11
|
+
}
|
12
|
+
|
9
13
|
get _actingListbox() {
|
10
14
|
return this.actingListbox
|
11
15
|
}
|
@@ -21,4 +25,8 @@ Combobox.Actors = Base => class extends Base {
|
|
21
25
|
set _actingCombobox(combobox) {
|
22
26
|
this.actingCombobox = combobox
|
23
27
|
}
|
28
|
+
|
29
|
+
get _allComboboxes() {
|
30
|
+
return [ this.comboboxTarget, this.dialogComboboxTarget ]
|
31
|
+
}
|
24
32
|
}
|
@@ -11,14 +11,14 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
11
11
|
_autocompleteWith(option, { force }) {
|
12
12
|
if (!this._autocompletesInline && !force) return
|
13
13
|
|
14
|
-
const typedValue = this.
|
14
|
+
const typedValue = this._typedQuery
|
15
15
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
|
16
16
|
|
17
17
|
if (force) {
|
18
|
-
this.
|
18
|
+
this._fullQuery = autocompletedValue
|
19
19
|
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
|
20
20
|
} else if (startsWith(autocompletedValue, typedValue)) {
|
21
|
-
this.
|
21
|
+
this._fullQuery = autocompletedValue
|
22
22
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
|
23
23
|
}
|
24
24
|
}
|
@@ -30,14 +30,14 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
30
30
|
}
|
31
31
|
|
32
32
|
get _isExactAutocompleteMatch() {
|
33
|
-
return this._immediatelyAutocompletableValue === this.
|
33
|
+
return this._immediatelyAutocompletableValue === this._fullQuery
|
34
34
|
}
|
35
35
|
|
36
36
|
// All `_isExactAutocompleteMatch` matches are `_isPartialAutocompleteMatch` matches
|
37
37
|
// but not all `_isPartialAutocompleteMatch` matches are `_isExactAutocompleteMatch` matches.
|
38
38
|
get _isPartialAutocompleteMatch() {
|
39
39
|
return !!this._immediatelyAutocompletableValue &&
|
40
|
-
startsWith(this._immediatelyAutocompletableValue, this.
|
40
|
+
startsWith(this._immediatelyAutocompletableValue, this._fullQuery)
|
41
41
|
}
|
42
42
|
|
43
43
|
get _autocompletesList() {
|
@@ -49,6 +49,6 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
49
49
|
}
|
50
50
|
|
51
51
|
get _immediatelyAutocompletableValue() {
|
52
|
-
return this.
|
52
|
+
return this._ensurableOption?.getAttribute(this.autocompletableAttributeValue)
|
53
53
|
}
|
54
54
|
}
|
@@ -14,7 +14,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
_moveArtifactsToDialog() {
|
17
|
-
this.dialogComboboxTarget.value = this.
|
17
|
+
this.dialogComboboxTarget.value = this._fullQuery
|
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.
|
26
|
+
this.comboboxTarget.value = this._fullQuery
|
27
27
|
|
28
28
|
this._actingCombobox = this.comboboxTarget
|
29
29
|
this._actingListbox = this.listboxTarget
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
import Combobox from "hw_combobox/models/combobox/base"
|
3
|
-
import { applyFilter, debounce, isDeleteEvent } from "hw_combobox/helpers"
|
3
|
+
import { applyFilter, debounce, isDeleteEvent, unselectedPortion } from "hw_combobox/helpers"
|
4
4
|
import { get } from "hw_combobox/vendor/requestjs"
|
5
5
|
|
6
6
|
Combobox.Filtering = Base => class extends Base {
|
@@ -21,13 +21,13 @@ Combobox.Filtering = Base => class extends Base {
|
|
21
21
|
}
|
22
22
|
|
23
23
|
async _filterAsync(event) {
|
24
|
-
const query = { q: this.
|
24
|
+
const query = { q: this._fullQuery, input_type: event.inputType }
|
25
25
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
|
26
26
|
}
|
27
27
|
|
28
28
|
_filterSync(event) {
|
29
29
|
this.open()
|
30
|
-
this._allOptionElements.forEach(applyFilter(this.
|
30
|
+
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }))
|
31
31
|
this._commitFilter(event)
|
32
32
|
}
|
33
33
|
|
@@ -42,16 +42,18 @@ Combobox.Filtering = Base => class extends Base {
|
|
42
42
|
}
|
43
43
|
|
44
44
|
get _isQueried() {
|
45
|
-
return this.
|
45
|
+
return this._fullQuery.length > 0
|
46
46
|
}
|
47
47
|
|
48
|
-
|
49
|
-
// after a certain point in the call chain.
|
50
|
-
get _query() {
|
48
|
+
get _fullQuery() {
|
51
49
|
return this._actingCombobox.value
|
52
50
|
}
|
53
51
|
|
54
|
-
set
|
52
|
+
set _fullQuery(value) {
|
55
53
|
this._actingCombobox.value = value
|
56
54
|
}
|
55
|
+
|
56
|
+
get _typedQuery() {
|
57
|
+
return unselectedPortion(this._actingCombobox)
|
58
|
+
}
|
57
59
|
}
|
@@ -10,16 +10,16 @@ Combobox.Selection = Base => class extends Base {
|
|
10
10
|
|
11
11
|
_connectSelection() {
|
12
12
|
if (this.hasPrefilledDisplayValue) {
|
13
|
-
this.
|
13
|
+
this._fullQuery = this.prefilledDisplayValue
|
14
14
|
}
|
15
15
|
}
|
16
16
|
|
17
|
-
_select(option, {
|
17
|
+
_select(option, { forceAutocomplete = false } = {}) {
|
18
18
|
this._resetOptions()
|
19
19
|
|
20
20
|
if (option) {
|
21
21
|
this._markValid()
|
22
|
-
this._autocompleteWith(option, { force })
|
22
|
+
this._autocompleteWith(option, { force: forceAutocomplete })
|
23
23
|
this._commitSelection(option, { selected: true })
|
24
24
|
} else {
|
25
25
|
this._markInvalid()
|
@@ -41,23 +41,25 @@ Combobox.Selection = Base => class extends Base {
|
|
41
41
|
}
|
42
42
|
|
43
43
|
option.setAttribute("aria-selected", selected)
|
44
|
+
this._setActiveDescendant(selected ? option.id : "")
|
44
45
|
}
|
45
46
|
|
46
47
|
_deselect() {
|
47
48
|
const option = this._selectedOptionElement
|
48
49
|
if (option) this._commitSelection(option, { selected: false })
|
49
50
|
this.hiddenFieldTarget.value = null
|
51
|
+
this._setActiveDescendant("")
|
50
52
|
}
|
51
53
|
|
52
54
|
_selectNew() {
|
53
55
|
this._resetOptions()
|
54
|
-
this.hiddenFieldTarget.value = this.
|
56
|
+
this.hiddenFieldTarget.value = this._fullQuery
|
55
57
|
this.hiddenFieldTarget.name = this.nameWhenNewValue
|
56
58
|
}
|
57
59
|
|
58
60
|
_selectIndex(index) {
|
59
61
|
const option = wrapAroundAccess(this._visibleOptionElements, index)
|
60
|
-
this._select(option, {
|
62
|
+
this._select(option, { forceAutocomplete: true })
|
61
63
|
}
|
62
64
|
|
63
65
|
_preselectOption() {
|
@@ -70,18 +72,22 @@ Combobox.Selection = Base => class extends Base {
|
|
70
72
|
}
|
71
73
|
}
|
72
74
|
|
73
|
-
|
74
|
-
if (this.
|
75
|
-
this._select(this._ensurableOption, {
|
76
|
-
this.filter({ inputType: "hw:
|
75
|
+
_lockInSelection() {
|
76
|
+
if (this._shouldLockInSelection) {
|
77
|
+
this._select(this._ensurableOption, { forceAutocomplete: true })
|
78
|
+
this.filter({ inputType: "hw:lockInSelection" })
|
77
79
|
}
|
78
80
|
}
|
79
81
|
|
82
|
+
_setActiveDescendant(id) {
|
83
|
+
this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id))
|
84
|
+
}
|
85
|
+
|
80
86
|
get _hasValueButNoSelection() {
|
81
87
|
return this.hiddenFieldTarget.value && !this._selectedOptionElement
|
82
88
|
}
|
83
89
|
|
84
|
-
get
|
90
|
+
get _shouldLockInSelection() {
|
85
91
|
return this._isQueried && !!this._ensurableOption && !this._isNewOptionWithPotentialMatches
|
86
92
|
}
|
87
93
|
|
@@ -8,7 +8,7 @@ Combobox.Toggle = Base => class extends Base {
|
|
8
8
|
|
9
9
|
close() {
|
10
10
|
if (this._isOpen) {
|
11
|
-
this.
|
11
|
+
this._lockInSelection()
|
12
12
|
this.expandedValue = false
|
13
13
|
}
|
14
14
|
}
|
@@ -34,7 +34,6 @@ Combobox.Toggle = Base => class extends Base {
|
|
34
34
|
closeOnFocusOutside({ target }) {
|
35
35
|
if (!this._isOpen) return
|
36
36
|
if (this.element.contains(target)) return
|
37
|
-
if (target.matches("main")) return
|
38
37
|
|
39
38
|
this.close()
|
40
39
|
}
|
@@ -53,8 +53,9 @@ class HotwireCombobox::Component
|
|
53
53
|
class: "hw-combobox__input",
|
54
54
|
type: input_type,
|
55
55
|
data: input_data,
|
56
|
-
aria: input_aria
|
57
|
-
|
56
|
+
aria: input_aria,
|
57
|
+
autocomplete: :off
|
58
|
+
}.with_indifferent_access.merge combobox_attrs.except(*nested_attrs)
|
58
59
|
end
|
59
60
|
|
60
61
|
|
@@ -77,11 +78,6 @@ class HotwireCombobox::Component
|
|
77
78
|
end
|
78
79
|
|
79
80
|
|
80
|
-
def listbox_options_attrs
|
81
|
-
{ id: listbox_options_id }
|
82
|
-
end
|
83
|
-
|
84
|
-
|
85
81
|
def dialog_wrapper_attrs
|
86
82
|
{
|
87
83
|
class: "hw-combobox__dialog__wrapper"
|
@@ -239,7 +235,8 @@ class HotwireCombobox::Component
|
|
239
235
|
controls: listbox_id,
|
240
236
|
owns: listbox_id,
|
241
237
|
haspopup: "listbox",
|
242
|
-
autocomplete: autocomplete
|
238
|
+
autocomplete: autocomplete,
|
239
|
+
activedescendant: ""
|
243
240
|
end
|
244
241
|
|
245
242
|
|
@@ -260,11 +257,6 @@ class HotwireCombobox::Component
|
|
260
257
|
end
|
261
258
|
|
262
259
|
|
263
|
-
def listbox_options_id
|
264
|
-
"#{listbox_id}__options"
|
265
|
-
end
|
266
|
-
|
267
|
-
|
268
260
|
def dialog_data
|
269
261
|
{
|
270
262
|
action: "keydown->hw-combobox#navigate",
|
@@ -290,7 +282,8 @@ class HotwireCombobox::Component
|
|
290
282
|
{
|
291
283
|
controls: dialog_listbox_id,
|
292
284
|
owns: dialog_listbox_id,
|
293
|
-
autocomplete: autocomplete
|
285
|
+
autocomplete: autocomplete,
|
286
|
+
activedescendant: ""
|
294
287
|
}
|
295
288
|
end
|
296
289
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
1
3
|
class HotwireCombobox::Listbox::Option
|
2
4
|
def initialize(option)
|
3
5
|
@option = option.is_a?(Hash) ? Data.new(**option) : option
|
@@ -30,7 +32,7 @@ class HotwireCombobox::Listbox::Option
|
|
30
32
|
end
|
31
33
|
|
32
34
|
def id
|
33
|
-
option.try(:id)
|
35
|
+
option.try(:id) || SecureRandom.uuid
|
34
36
|
end
|
35
37
|
|
36
38
|
def data
|
@@ -1,5 +1,6 @@
|
|
1
1
|
<%# locals: (for_id:, next_page:, src:) -%>
|
2
2
|
|
3
|
-
<%= turbo_stream.
|
3
|
+
<%= turbo_stream.remove hw_pagination_frame_wrapper_id(for_id) %>
|
4
|
+
<%= turbo_stream.append hw_listbox_id(for_id) do %>
|
4
5
|
<%= render "hotwire_combobox/pagination", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page) %>
|
5
6
|
<% end %>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<%# locals: (for_id:, options:) -%>
|
2
2
|
|
3
|
-
<%= turbo_stream.public_send(hw_combobox_page_stream_action,
|
3
|
+
<%= turbo_stream.public_send(hw_combobox_page_stream_action, hw_listbox_id(for_id)) do %>
|
4
4
|
<% options.each do |option| %>
|
5
5
|
<%= render option %>
|
6
6
|
<% end %>
|
@@ -1,4 +1,7 @@
|
|
1
1
|
<%# locals: (for_id:, src:) -%>
|
2
2
|
|
3
|
-
<%=
|
4
|
-
|
3
|
+
<%= tag.li id: hw_pagination_frame_wrapper_id(for_id), data: {
|
4
|
+
hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type] },
|
5
|
+
aria: { hidden: true } do %>
|
6
|
+
<%= turbo_frame_tag hw_pagination_frame_id(for_id), src: src, loading: :lazy %>
|
7
|
+
<% end %>
|
@@ -1,8 +1,6 @@
|
|
1
1
|
<%= tag.ul **component.listbox_attrs do |ul| %>
|
2
|
-
|
3
|
-
|
4
|
-
<%= render option %>
|
5
|
-
<% end %>
|
2
|
+
<% component.options.each do |option| %>
|
3
|
+
<%= render option %>
|
6
4
|
<% end %>
|
7
5
|
|
8
6
|
<% if component.paginated? %>
|
@@ -45,8 +45,12 @@ module HotwireCombobox
|
|
45
45
|
hw_alias :hw_async_combobox_options
|
46
46
|
|
47
47
|
protected # library use only
|
48
|
-
def
|
49
|
-
"#{id}-hw-
|
48
|
+
def hw_listbox_id(id)
|
49
|
+
"#{id}-hw-listbox"
|
50
|
+
end
|
51
|
+
|
52
|
+
def hw_pagination_frame_wrapper_id(id)
|
53
|
+
"#{id}__hw_combobox_pagination__wrapper"
|
50
54
|
end
|
51
55
|
|
52
56
|
def hw_pagination_frame_id(id)
|
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.
|
4
|
+
version: 0.1.37
|
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-
|
11
|
+
date: 2024-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -52,7 +52,8 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.2'
|
55
|
-
description:
|
55
|
+
description: An autocomplete combobox implementation for Ruby on Rails apps using
|
56
|
+
Hotwire.
|
56
57
|
email:
|
57
58
|
- jose@farias.mx
|
58
59
|
executables: []
|