hotwire_combobox 0.1.36 → 0.1.38
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/README.md +86 -2
- data/app/assets/javascripts/hotwire_combobox.esm.js +1254 -0
- data/app/assets/javascripts/hotwire_combobox.umd.js +1260 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/actors.js +8 -0
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +6 -1
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +6 -0
- data/app/assets/stylesheets/hotwire_combobox.css +4 -0
- data/app/presenters/hotwire_combobox/component.rb +15 -16
- data/app/presenters/hotwire_combobox/listbox/option.rb +9 -3
- data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +3 -2
- 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/engine.rb +1 -1
- data/lib/hotwire_combobox/helper.rb +89 -20
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +4 -2
@@ -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
|
}
|
@@ -21,7 +21,12 @@ Combobox.Filtering = Base => class extends Base {
|
|
21
21
|
}
|
22
22
|
|
23
23
|
async _filterAsync(event) {
|
24
|
-
const query = {
|
24
|
+
const query = {
|
25
|
+
q: this._fullQuery,
|
26
|
+
input_type: event.inputType,
|
27
|
+
for_id: this.element.dataset.asyncId
|
28
|
+
}
|
29
|
+
|
25
30
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
|
26
31
|
}
|
27
32
|
|
@@ -41,12 +41,14 @@ 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() {
|
@@ -77,6 +79,10 @@ Combobox.Selection = Base => class extends Base {
|
|
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
|
}
|
@@ -121,6 +121,10 @@
|
|
121
121
|
text-overflow: ellipsis;
|
122
122
|
}
|
123
123
|
|
124
|
+
.hw-combobox__option--blank {
|
125
|
+
border-bottom: var(--hw-border-width--slim) solid var(--hw-border-color);
|
126
|
+
}
|
127
|
+
|
124
128
|
.hw-combobox__option:hover,
|
125
129
|
.hw-combobox__option--selected {
|
126
130
|
background-color: var(--hw-active-bg-color);
|
@@ -1,5 +1,7 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
1
3
|
class HotwireCombobox::Component
|
2
|
-
attr_reader :
|
4
|
+
attr_reader :options, :dialog_label
|
3
5
|
|
4
6
|
def initialize \
|
5
7
|
view, name,
|
@@ -53,8 +55,9 @@ class HotwireCombobox::Component
|
|
53
55
|
class: "hw-combobox__input",
|
54
56
|
type: input_type,
|
55
57
|
data: input_data,
|
56
|
-
aria: input_aria
|
57
|
-
|
58
|
+
aria: input_aria,
|
59
|
+
autocomplete: :off
|
60
|
+
}.with_indifferent_access.merge combobox_attrs.except(*nested_attrs)
|
58
61
|
end
|
59
62
|
|
60
63
|
|
@@ -77,11 +80,6 @@ class HotwireCombobox::Component
|
|
77
80
|
end
|
78
81
|
|
79
82
|
|
80
|
-
def listbox_options_attrs
|
81
|
-
{ id: listbox_options_id }
|
82
|
-
end
|
83
|
-
|
84
|
-
|
85
83
|
def dialog_wrapper_attrs
|
86
84
|
{
|
87
85
|
class: "hw-combobox__dialog__wrapper"
|
@@ -185,9 +183,13 @@ class HotwireCombobox::Component
|
|
185
183
|
form&.object&.class&.reflect_on_association(association_name).present?
|
186
184
|
end
|
187
185
|
|
186
|
+
def async_src
|
187
|
+
view.hw_uri_with_params @async_src, for_id: canonical_id, format: :turbo_stream
|
188
|
+
end
|
189
|
+
|
188
190
|
|
189
191
|
def canonical_id
|
190
|
-
id || form&.field_id(name)
|
192
|
+
id || form&.field_id(name) || SecureRandom.uuid
|
191
193
|
end
|
192
194
|
|
193
195
|
|
@@ -239,7 +241,8 @@ class HotwireCombobox::Component
|
|
239
241
|
controls: listbox_id,
|
240
242
|
owns: listbox_id,
|
241
243
|
haspopup: "listbox",
|
242
|
-
autocomplete: autocomplete
|
244
|
+
autocomplete: autocomplete,
|
245
|
+
activedescendant: ""
|
243
246
|
end
|
244
247
|
|
245
248
|
|
@@ -260,11 +263,6 @@ class HotwireCombobox::Component
|
|
260
263
|
end
|
261
264
|
|
262
265
|
|
263
|
-
def listbox_options_id
|
264
|
-
"#{listbox_id}__options"
|
265
|
-
end
|
266
|
-
|
267
|
-
|
268
266
|
def dialog_data
|
269
267
|
{
|
270
268
|
action: "keydown->hw-combobox#navigate",
|
@@ -290,7 +288,8 @@ class HotwireCombobox::Component
|
|
290
288
|
{
|
291
289
|
controls: dialog_listbox_id,
|
292
290
|
owns: dialog_listbox_id,
|
293
|
-
autocomplete: autocomplete
|
291
|
+
autocomplete: autocomplete,
|
292
|
+
activedescendant: ""
|
294
293
|
}
|
295
294
|
end
|
296
295
|
|
@@ -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
|
@@ -16,7 +18,7 @@ class HotwireCombobox::Listbox::Option
|
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
19
|
-
Data = Struct.new :id, :value, :display, :content, :filterable_as, :autocompletable_as, keyword_init: true
|
21
|
+
Data = Struct.new :id, :value, :display, :content, :blank, :filterable_as, :autocompletable_as, keyword_init: true
|
20
22
|
|
21
23
|
attr_reader :option
|
22
24
|
|
@@ -24,13 +26,13 @@ class HotwireCombobox::Listbox::Option
|
|
24
26
|
{
|
25
27
|
id: id,
|
26
28
|
role: :option,
|
27
|
-
class: "hw-combobox__option",
|
29
|
+
class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank? } ],
|
28
30
|
data: data
|
29
31
|
}
|
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
|
@@ -49,4 +51,8 @@ class HotwireCombobox::Listbox::Option
|
|
49
51
|
def filterable_as
|
50
52
|
option.try(:filterable_as) || option.try(:display)
|
51
53
|
end
|
54
|
+
|
55
|
+
def blank?
|
56
|
+
option.try(:blank).present?
|
57
|
+
end
|
52
58
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
<%# locals: (for_id:, next_page:, src:) -%>
|
2
2
|
|
3
|
-
<%= turbo_stream.
|
4
|
-
|
3
|
+
<%= turbo_stream.remove hw_pagination_frame_wrapper_id(for_id) %>
|
4
|
+
<%= turbo_stream.append hw_listbox_id(for_id) do %>
|
5
|
+
<%= render "hotwire_combobox/pagination", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page, for_id) %>
|
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? %>
|
@@ -10,7 +10,7 @@ module HotwireCombobox
|
|
10
10
|
unless HotwireCombobox.bypass_convenience_methods?
|
11
11
|
module FormBuilderExtensions
|
12
12
|
def combobox(*args, **kwargs)
|
13
|
-
@template.hw_combobox_tag
|
13
|
+
@template.hw_combobox_tag(*args, **kwargs.merge(form: self))
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
@@ -15,27 +15,32 @@ module HotwireCombobox
|
|
15
15
|
end
|
16
16
|
hw_alias :hw_combobox_style_tag
|
17
17
|
|
18
|
-
def hw_combobox_tag(name, options_or_src = [], render_in: {}, **kwargs)
|
19
|
-
options, src = hw_extract_options_and_src(options_or_src, render_in)
|
18
|
+
def hw_combobox_tag(name, options_or_src = [], render_in: {}, include_blank: nil, **kwargs)
|
19
|
+
options, src = hw_extract_options_and_src(options_or_src, render_in, include_blank)
|
20
20
|
component = HotwireCombobox::Component.new self, name, options: options, async_src: src, **kwargs
|
21
21
|
|
22
22
|
render "hotwire_combobox/combobox", component: component
|
23
23
|
end
|
24
24
|
hw_alias :hw_combobox_tag
|
25
25
|
|
26
|
-
def hw_combobox_options(options, render_in: {}, display: :to_combobox_display, **methods)
|
26
|
+
def hw_combobox_options(options, render_in: {}, include_blank: nil, display: :to_combobox_display, **methods)
|
27
27
|
if options.first.is_a? HotwireCombobox::Listbox::Option
|
28
28
|
options
|
29
29
|
else
|
30
|
-
render_in_proc =
|
31
|
-
|
30
|
+
render_in_proc = hw_render_in_proc(render_in) if render_in.present?
|
31
|
+
|
32
|
+
hw_parse_combobox_options(options, render_in: render_in_proc, **methods.merge(display: display)).tap do |options|
|
33
|
+
options.unshift(hw_blank_option(include_blank)) if include_blank.present?
|
34
|
+
end
|
32
35
|
end
|
33
36
|
end
|
34
37
|
hw_alias :hw_combobox_options
|
35
38
|
|
36
|
-
def hw_paginated_combobox_options(options, for_id
|
37
|
-
|
38
|
-
|
39
|
+
def hw_paginated_combobox_options(options, for_id: params[:for_id], src: request.path, next_page: nil, render_in: {}, include_blank: {}, **methods)
|
40
|
+
include_blank = params[:page] ? nil : include_blank
|
41
|
+
options = hw_combobox_options options, render_in: render_in, include_blank: include_blank, **methods
|
42
|
+
this_page = render "hotwire_combobox/paginated_options", for_id: for_id, options: options
|
43
|
+
next_page = render "hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page
|
39
44
|
|
40
45
|
safe_join [ this_page, next_page ]
|
41
46
|
end
|
@@ -44,18 +49,26 @@ module HotwireCombobox
|
|
44
49
|
alias_method :hw_async_combobox_options, :hw_paginated_combobox_options
|
45
50
|
hw_alias :hw_async_combobox_options
|
46
51
|
|
47
|
-
|
48
|
-
def
|
49
|
-
"#{id}-hw-
|
52
|
+
# private library use only
|
53
|
+
def hw_listbox_id(id)
|
54
|
+
"#{id}-hw-listbox"
|
55
|
+
end
|
56
|
+
|
57
|
+
def hw_pagination_frame_wrapper_id(id)
|
58
|
+
"#{id}__hw_combobox_pagination__wrapper"
|
50
59
|
end
|
51
60
|
|
52
61
|
def hw_pagination_frame_id(id)
|
53
62
|
"#{id}__hw_combobox_pagination"
|
54
63
|
end
|
55
64
|
|
56
|
-
def hw_combobox_next_page_uri(uri, next_page)
|
65
|
+
def hw_combobox_next_page_uri(uri, next_page, for_id)
|
57
66
|
if next_page
|
58
|
-
hw_uri_with_params uri,
|
67
|
+
hw_uri_with_params uri,
|
68
|
+
page: next_page,
|
69
|
+
q: params[:q],
|
70
|
+
for_id: for_id,
|
71
|
+
format: :turbo_stream
|
59
72
|
end
|
60
73
|
end
|
61
74
|
|
@@ -63,12 +76,19 @@ module HotwireCombobox
|
|
63
76
|
params[:page] ? :append : :update
|
64
77
|
end
|
65
78
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
79
|
+
def hw_blank_option(include_blank)
|
80
|
+
display, content = hw_extract_blank_display_and_content include_blank
|
81
|
+
|
82
|
+
HotwireCombobox::Listbox::Option.new display: display, content: content, value: "", blank: true
|
83
|
+
end
|
84
|
+
|
85
|
+
def hw_extract_blank_display_and_content(include_blank)
|
86
|
+
if include_blank.is_a? Hash
|
87
|
+
text = include_blank.delete(:text)
|
88
|
+
|
89
|
+
[ text, hw_render_in_proc(include_blank).(text) ]
|
70
90
|
else
|
71
|
-
[
|
91
|
+
[ include_blank, include_blank ]
|
72
92
|
end
|
73
93
|
end
|
74
94
|
|
@@ -81,9 +101,23 @@ module HotwireCombobox
|
|
81
101
|
url_or_path
|
82
102
|
end
|
83
103
|
|
104
|
+
private
|
105
|
+
def hw_render_in_proc(render_in)
|
106
|
+
->(object) { render(**render_in.reverse_merge(object: object)) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def hw_extract_options_and_src(options_or_src, render_in, include_blank)
|
110
|
+
if options_or_src.is_a? String
|
111
|
+
[ [], options_or_src ]
|
112
|
+
else
|
113
|
+
[ hw_combobox_options(options_or_src, render_in: render_in, include_blank: include_blank), nil ]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
84
117
|
def hw_parse_combobox_options(options, render_in: nil, **methods)
|
85
118
|
options.map do |option|
|
86
|
-
HotwireCombobox::Listbox::Option.new
|
119
|
+
HotwireCombobox::Listbox::Option.new \
|
120
|
+
**hw_option_attrs_for(option, render_in: render_in, **methods)
|
87
121
|
end
|
88
122
|
end
|
89
123
|
|
@@ -118,8 +152,43 @@ module HotwireCombobox
|
|
118
152
|
if method_or_proc.is_a? Proc
|
119
153
|
method_or_proc.call object
|
120
154
|
else
|
121
|
-
object
|
155
|
+
hw_call_method object, method_or_proc
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def hw_call_method(object, method)
|
160
|
+
if object.respond_to? method
|
161
|
+
object.public_send method
|
162
|
+
else
|
163
|
+
hw_raise_no_public_method_error object, method
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def hw_raise_no_public_method_error(object, method)
|
168
|
+
if object.respond_to? method, true
|
169
|
+
header = "`#{object.class}` responds to `##{method}` but the method is not public."
|
170
|
+
else
|
171
|
+
header = "`#{object.class}` does not respond to `##{method}`."
|
172
|
+
end
|
173
|
+
|
174
|
+
if method.to_s == "to_combobox_display"
|
175
|
+
header << "\n\nThis method is used to determine how this option should appear in the combobox options list."
|
122
176
|
end
|
177
|
+
|
178
|
+
raise NoMethodError, <<~MSG
|
179
|
+
[ACTION NEEDED] – Message from HotwireCombobox:
|
180
|
+
|
181
|
+
#{header}
|
182
|
+
|
183
|
+
Please add this as a public method and return a string.
|
184
|
+
|
185
|
+
Example:
|
186
|
+
class #{object.class} < ApplicationRecord
|
187
|
+
def #{method}
|
188
|
+
name # or `title`, `to_s`, etc.
|
189
|
+
end
|
190
|
+
end
|
191
|
+
MSG
|
123
192
|
end
|
124
193
|
end
|
125
194
|
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.
|
4
|
+
version: 0.1.38
|
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-03-
|
11
|
+
date: 2024-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -65,6 +65,8 @@ files:
|
|
65
65
|
- Rakefile
|
66
66
|
- app/assets/config/hw_combobox_manifest.js
|
67
67
|
- app/assets/javascripts/controllers/hw_combobox_controller.js
|
68
|
+
- app/assets/javascripts/hotwire_combobox.esm.js
|
69
|
+
- app/assets/javascripts/hotwire_combobox.umd.js
|
68
70
|
- app/assets/javascripts/hw_combobox/helpers.js
|
69
71
|
- app/assets/javascripts/hw_combobox/models/combobox.js
|
70
72
|
- app/assets/javascripts/hw_combobox/models/combobox/actors.js
|