hotwire_combobox 0.1.43 → 0.2.1

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -1
  3. data/app/assets/javascripts/controllers/hw_combobox_controller.js +29 -3
  4. data/app/assets/javascripts/hotwire_combobox.esm.js +442 -104
  5. data/app/assets/javascripts/hotwire_combobox.umd.js +442 -104
  6. data/app/assets/javascripts/hw_combobox/helpers.js +16 -0
  7. data/app/assets/javascripts/hw_combobox/models/combobox/announcements.js +7 -0
  8. data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +1 -1
  9. data/app/assets/javascripts/hw_combobox/models/combobox/events.js +21 -11
  10. data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +20 -12
  11. data/app/assets/javascripts/hw_combobox/models/combobox/form_field.js +74 -0
  12. data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +160 -0
  13. data/app/assets/javascripts/hw_combobox/models/combobox/navigation.js +15 -6
  14. data/app/assets/javascripts/hw_combobox/models/combobox/options.js +19 -7
  15. data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +50 -49
  16. data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +33 -16
  17. data/app/assets/javascripts/hw_combobox/models/combobox/validity.js +1 -1
  18. data/app/assets/javascripts/hw_combobox/models/combobox.js +3 -0
  19. data/app/assets/stylesheets/hotwire_combobox.css +90 -19
  20. data/app/presenters/hotwire_combobox/component/customizable.rb +9 -1
  21. data/app/presenters/hotwire_combobox/component.rb +106 -32
  22. data/app/presenters/hotwire_combobox/listbox/group.rb +47 -0
  23. data/app/presenters/hotwire_combobox/listbox/item/collection.rb +14 -0
  24. data/app/presenters/hotwire_combobox/listbox/item.rb +111 -0
  25. data/app/presenters/hotwire_combobox/listbox/option.rb +9 -4
  26. data/app/views/hotwire_combobox/_component.html.erb +1 -0
  27. data/app/views/hotwire_combobox/_selection_chip.turbo_stream.erb +8 -0
  28. data/app/views/hotwire_combobox/layouts/_selection_chip.turbo_stream.erb +7 -0
  29. data/lib/hotwire_combobox/helper.rb +112 -91
  30. data/lib/hotwire_combobox/version.rb +1 -1
  31. metadata +11 -3
@@ -0,0 +1,111 @@
1
+ class HotwireCombobox::Listbox::Item
2
+ class << self
3
+ def collection_for(view, options, render_in:, include_blank:, **custom_methods)
4
+ new(view, options, render_in: render_in, include_blank: include_blank, **custom_methods).collection
5
+ end
6
+ end
7
+
8
+ def initialize(view, options, render_in:, include_blank:, **custom_methods)
9
+ @view = view
10
+ @options = options
11
+ @render_in = render_in
12
+ @include_blank = include_blank
13
+ @custom_methods = custom_methods
14
+ end
15
+
16
+ def collection
17
+ items = groups_or_options
18
+ items.unshift(blank_option) if include_blank.present?
19
+ Collection.new items
20
+ end
21
+
22
+ private
23
+ attr_reader :view, :options, :render_in, :include_blank, :custom_methods
24
+
25
+ def groups_or_options
26
+ if grouped?
27
+ create_listbox_group options
28
+ else
29
+ create_listbox_options options
30
+ end
31
+ end
32
+
33
+ def grouped?
34
+ _key, value = options.to_a.first
35
+ value.is_a? Array
36
+ end
37
+
38
+ def create_listbox_group(options)
39
+ options.map do |group_name, group_options|
40
+ HotwireCombobox::Listbox::Group.new group_name,
41
+ options: create_listbox_options(group_options)
42
+ end
43
+ end
44
+
45
+ def create_listbox_options(options)
46
+ if options.first.is_a? HotwireCombobox::Listbox::Option
47
+ options
48
+ else
49
+ options.map do |option|
50
+ HotwireCombobox::Listbox::Option.new **option_attrs(option)
51
+ end
52
+ end
53
+ end
54
+
55
+ def option_attrs(option)
56
+ case option
57
+ when Hash
58
+ option.tap do |attrs|
59
+ attrs[:content] = render_content(object: attrs[:display], attrs: attrs) if render_in.present?
60
+ end
61
+ when String
62
+ {}.tap do |attrs|
63
+ attrs[:display] = option
64
+ attrs[:value] = option
65
+ attrs[:content] = render_content(object: attrs[:display], attrs: attrs) if render_in.present?
66
+ end
67
+ when Array
68
+ {}.tap do |attrs|
69
+ attrs[:display] = option.first
70
+ attrs[:value] = option.last
71
+ attrs[:content] = render_content(object: attrs[:display], attrs: attrs) if render_in.present?
72
+ end
73
+ else
74
+ {}.tap do |attrs|
75
+ attrs[:id] = view.hw_call_method_or_proc(option, custom_methods[:id]) if custom_methods[:id]
76
+ attrs[:display] = view.hw_call_method_or_proc(option, custom_methods[:display]) if custom_methods[:display]
77
+ attrs[:value] = view.hw_call_method_or_proc(option, custom_methods[:value] || :id)
78
+
79
+ if render_in.present?
80
+ attrs[:content] = render_content(object: option, attrs: attrs)
81
+ elsif custom_methods[:content]
82
+ attrs[:content] = view.hw_call_method_or_proc(option, custom_methods[:content])
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def render_content(render_opts: render_in, object:, attrs:)
89
+ view.render **render_opts.reverse_merge(
90
+ object: object,
91
+ locals: { combobox_display: attrs[:display], combobox_value: attrs[:value] })
92
+ end
93
+
94
+ def blank_option
95
+ display, content = extract_blank_display_and_content
96
+ HotwireCombobox::Listbox::Option.new display: display, content: content, value: "", blank: true
97
+ end
98
+
99
+ def extract_blank_display_and_content
100
+ case include_blank
101
+ when Hash
102
+ text = include_blank.delete(:text)
103
+
104
+ [ text, render_content(render_opts: include_blank, object: text, attrs: { display: text, value: "" }) ]
105
+ when String
106
+ [ include_blank, include_blank ]
107
+ else
108
+ [ "", "&nbsp;".html_safe ]
109
+ end
110
+ end
111
+ end
@@ -27,7 +27,8 @@ class HotwireCombobox::Listbox::Option
27
27
  id: id,
28
28
  role: :option,
29
29
  class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank? } ],
30
- data: data
30
+ data: data,
31
+ aria: aria
31
32
  }
32
33
  end
33
34
 
@@ -37,21 +38,25 @@ class HotwireCombobox::Listbox::Option
37
38
 
38
39
  def data
39
40
  {
40
- action: "click->hw-combobox#selectOptionOnClick",
41
+ action: "click->hw-combobox#selectOnClick",
41
42
  filterable_as: filterable_as,
42
43
  autocompletable_as: autocompletable_as,
43
44
  value: value
44
45
  }
45
46
  end
46
47
 
47
- def content
48
- option.try(:content) || option.try(:display)
48
+ def aria
49
+ { selected: false }
49
50
  end
50
51
 
51
52
  def filterable_as
52
53
  option.try(:filterable_as) || option.try(:display)
53
54
  end
54
55
 
56
+ def content
57
+ option.try(:content) || option.try(:display)
58
+ end
59
+
55
60
  def blank?
56
61
  option.try(:blank).present?
57
62
  end
@@ -4,6 +4,7 @@
4
4
  <%= render "hotwire_combobox/combobox/hidden_field", component: component %>
5
5
 
6
6
  <%= tag.div **component.main_wrapper_attrs do %>
7
+ <%= tag.div **component.announcer_attrs %>
7
8
  <%= render "hotwire_combobox/combobox/input", component: component %>
8
9
  <%= render "hotwire_combobox/combobox/paginated_listbox", component: component %>
9
10
  <%= render "hotwire_combobox/combobox/dialog", component: component %>
@@ -0,0 +1,8 @@
1
+ <%# locals: (display:, value:, for_id:, remover_attrs:) -%>
2
+
3
+ <%= within_combobox_selection_chip(for_id: for_id) do %>
4
+ <%= tag.div class: "hw-combobox__chip" do %>
5
+ <%= tag.span display %>
6
+ <%= tag.span **remover_attrs %>
7
+ <% end %>
8
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: (for_id:) -%>
2
+
3
+ <%= turbo_stream.before for_id do %>
4
+ <%= tag.div data: { hw_combobox_chip: "" } do %>
5
+ <%= yield %>
6
+ <% end %>
7
+ <% end %>
@@ -13,38 +13,125 @@ module HotwireCombobox
13
13
  def hw_combobox_style_tag(*args, **kwargs)
14
14
  stylesheet_link_tag HotwireCombobox.stylesheet_path, *args, **kwargs
15
15
  end
16
- hw_alias :hw_combobox_style_tag
17
16
 
18
17
  def hw_combobox_tag(name, options_or_src = [], render_in: {}, include_blank: nil, **kwargs, &block)
19
- options, src = hw_extract_options_and_src(options_or_src, render_in, include_blank)
18
+ options, src = hw_extract_options_and_src options_or_src, render_in, include_blank
20
19
  component = HotwireCombobox::Component.new self, name, options: options, async_src: src, **kwargs
21
20
  render component, &block
22
21
  end
23
- hw_alias :hw_combobox_tag
24
22
 
25
- def hw_combobox_options(options, render_in: {}, include_blank: nil, display: :to_combobox_display, **methods)
26
- if options.first.is_a? HotwireCombobox::Listbox::Option
27
- options
28
- else
29
- opts = hw_parse_combobox_options options, render_in_proc: hw_render_in_proc(render_in), **methods.merge(display: display)
30
- opts.unshift(hw_blank_option(include_blank)) if include_blank.present?
31
- opts
32
- end
23
+ def hw_combobox_options(
24
+ options,
25
+ render_in: {},
26
+ include_blank: nil,
27
+ display: :to_combobox_display,
28
+ **custom_methods)
29
+ HotwireCombobox::Listbox::Item.collection_for \
30
+ self,
31
+ options,
32
+ render_in: render_in,
33
+ include_blank: include_blank,
34
+ **custom_methods.merge(display: display)
33
35
  end
34
- hw_alias :hw_combobox_options
35
36
 
36
- def hw_paginated_combobox_options(options, for_id: params[:for_id], src: request.path, next_page: nil, render_in: {}, include_blank: {}, **methods)
37
- include_blank = params[:page] ? nil : include_blank
38
- options = hw_combobox_options options, render_in: render_in, include_blank: include_blank, **methods
37
+ def hw_paginated_combobox_options(
38
+ options,
39
+ for_id: params[:for_id],
40
+ src: request.path,
41
+ next_page: nil,
42
+ render_in: {},
43
+ include_blank: {},
44
+ **custom_methods)
45
+ include_blank = params[:page].to_i > 0 ? nil : include_blank
46
+ options = hw_combobox_options options, render_in: render_in, include_blank: include_blank, **custom_methods
39
47
  this_page = render "hotwire_combobox/paginated_options", for_id: for_id, options: options
40
48
  next_page = render "hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page
41
49
 
42
50
  safe_join [ this_page, next_page ]
43
51
  end
44
- hw_alias :hw_paginated_combobox_options
45
-
46
52
  alias_method :hw_async_combobox_options, :hw_paginated_combobox_options
53
+
54
+ def hw_within_combobox_selection_chip(for_id: params[:for_id], &block)
55
+ render layout: "hotwire_combobox/layouts/selection_chip", locals: { for_id: for_id }, &block
56
+ end
57
+
58
+ def hw_combobox_selection_chip(
59
+ display:,
60
+ value:,
61
+ for_id: params[:for_id],
62
+ remover_attrs: hw_combobox_chip_remover_attrs(display: display, value: value))
63
+ render "hotwire_combobox/selection_chip",
64
+ display: display,
65
+ value: value,
66
+ for_id: for_id,
67
+ remover_attrs: remover_attrs
68
+ end
69
+
70
+ def hw_combobox_selection_chips_for(
71
+ objects,
72
+ display: :to_combobox_display,
73
+ value: :id,
74
+ for_id: params[:for_id])
75
+ objects.map do |object|
76
+ hw_combobox_selection_chip \
77
+ display: hw_call_method(object, display),
78
+ value: hw_call_method(object, value),
79
+ for_id: for_id
80
+ end.then { |chips| safe_join chips }
81
+ end
82
+
83
+ def hw_dismissing_combobox_selection_chip(display:, value:, for_id: params[:for_id])
84
+ hw_combobox_selection_chip \
85
+ display: display,
86
+ value: value,
87
+ for_id: for_id,
88
+ remover_attrs: hw_combobox_dismissing_chip_remover_attrs(display, value)
89
+ end
90
+
91
+ def hw_dismissing_combobox_selection_chips_for(
92
+ objects,
93
+ display: :to_combobox_display,
94
+ value: :id,
95
+ for_id: params[:for_id])
96
+ objects.map do |object|
97
+ hw_dismissing_combobox_selection_chip \
98
+ display: hw_call_method(object, display),
99
+ value: hw_call_method(object, value),
100
+ for_id: for_id
101
+ end.then { |chips| safe_join chips }
102
+ end
103
+
104
+ def hw_combobox_chip_remover_attrs(display:, value:, **kwargs)
105
+ {
106
+ tabindex: "0",
107
+ class: token_list("hw-combobox__chip__remover", kwargs[:class]),
108
+ aria: { label: "Remove #{display}" },
109
+ data: {
110
+ action: "click->hw-combobox#removeChip:stop keydown->hw-combobox#navigateChip",
111
+ hw_combobox_target: "chipDismisser",
112
+ hw_combobox_value_param: value
113
+ }
114
+ }
115
+ end
116
+
117
+ def hw_combobox_dismissing_chip_remover_attrs(display, value)
118
+ hw_combobox_chip_remover_attrs(display: display, value: value).tap do |attrs|
119
+ attrs[:data][:hw_combobox_target] = token_list(attrs[:data][:hw_combobox_target], "closer")
120
+ end
121
+ end
122
+
123
+ hw_alias :hw_combobox_style_tag
124
+ hw_alias :hw_combobox_tag
125
+ hw_alias :hw_combobox_options
126
+ hw_alias :hw_paginated_combobox_options
47
127
  hw_alias :hw_async_combobox_options
128
+ hw_alias :hw_within_combobox_selection_chip
129
+ hw_alias :hw_combobox_selection_chip
130
+ hw_alias :hw_combobox_selection_chips_for
131
+ hw_alias :hw_dismissing_combobox_selection_chip
132
+ hw_alias :hw_dismissing_combobox_selection_chips_for
133
+ hw_alias :hw_combobox_chip_remover_attrs
134
+ hw_alias :hw_combobox_dismissing_chip_remover_attrs
48
135
 
49
136
  # private library use only
50
137
  def hw_listbox_id(id)
@@ -70,23 +157,7 @@ module HotwireCombobox
70
157
  end
71
158
 
72
159
  def hw_combobox_page_stream_action
73
- params[:page] ? :append : :update
74
- end
75
-
76
- def hw_blank_option(include_blank)
77
- display, content = hw_extract_blank_display_and_content include_blank
78
-
79
- HotwireCombobox::Listbox::Option.new display: display, content: content, value: "", blank: true
80
- end
81
-
82
- def hw_extract_blank_display_and_content(include_blank)
83
- if include_blank.is_a? Hash
84
- text = include_blank.delete(:text)
85
-
86
- [ text, hw_call_render_in_proc(hw_render_in_proc(include_blank), text, display: text, value: "") ]
87
- else
88
- [ include_blank, include_blank ]
89
- end
160
+ params[:page].to_i > 0 ? :append : :update
90
161
  end
91
162
 
92
163
  def hw_uri_with_params(url_or_path, **params)
@@ -98,73 +169,23 @@ module HotwireCombobox
98
169
  url_or_path
99
170
  end
100
171
 
101
- private
102
- def hw_render_in_proc(render_in)
103
- if render_in.present?
104
- ->(object, locals) { render(**render_in.reverse_merge(object: object, locals: locals)) }
172
+ def hw_call_method_or_proc(object, method_or_proc)
173
+ if method_or_proc.is_a? Proc
174
+ method_or_proc.call object
175
+ else
176
+ hw_call_method object, method_or_proc
105
177
  end
106
178
  end
107
179
 
180
+ private
108
181
  def hw_extract_options_and_src(options_or_src, render_in, include_blank)
109
182
  if options_or_src.is_a? String
110
- [ [], options_or_src ]
183
+ [ hw_combobox_options([]), options_or_src ]
111
184
  else
112
185
  [ hw_combobox_options(options_or_src, render_in: render_in, include_blank: include_blank), nil ]
113
186
  end
114
187
  end
115
188
 
116
- def hw_parse_combobox_options(options, render_in_proc: nil, **methods)
117
- options.map do |option|
118
- HotwireCombobox::Listbox::Option.new \
119
- **hw_option_attrs_for(option, render_in_proc: render_in_proc, **methods)
120
- end
121
- end
122
-
123
- def hw_option_attrs_for(option, render_in_proc: nil, **methods)
124
- case option
125
- when Hash
126
- option.tap do |attrs|
127
- attrs[:content] = hw_call_render_in_proc(render_in_proc, attrs[:display], attrs) if render_in_proc
128
- end
129
- when String
130
- {}.tap do |attrs|
131
- attrs[:display] = option
132
- attrs[:value] = option
133
- attrs[:content] = hw_call_render_in_proc(render_in_proc, attrs[:display], attrs) if render_in_proc
134
- end
135
- when Array
136
- {}.tap do |attrs|
137
- attrs[:display] = option.first
138
- attrs[:value] = option.last
139
- attrs[:content] = hw_call_render_in_proc(render_in_proc, attrs[:display], attrs) if render_in_proc
140
- end
141
- else
142
- {}.tap do |attrs|
143
- attrs[:id] = hw_call_method_or_proc(option, methods[:id]) if methods[:id]
144
- attrs[:display] = hw_call_method_or_proc(option, methods[:display]) if methods[:display]
145
- attrs[:value] = hw_call_method_or_proc(option, methods[:value] || :id)
146
-
147
- if render_in_proc
148
- attrs[:content] = hw_call_render_in_proc(render_in_proc, option, attrs)
149
- elsif methods[:content]
150
- attrs[:content] = hw_call_method_or_proc(option, methods[:content])
151
- end
152
- end
153
- end
154
- end
155
-
156
- def hw_call_render_in_proc(render_in_proc, object, attrs)
157
- render_in_proc.(object, combobox_display: attrs[:display], combobox_value: attrs[:value])
158
- end
159
-
160
- def hw_call_method_or_proc(object, method_or_proc)
161
- if method_or_proc.is_a? Proc
162
- method_or_proc.call object
163
- else
164
- hw_call_method object, method_or_proc
165
- end
166
- end
167
-
168
189
  def hw_call_method(object, method)
169
190
  if object.respond_to? method
170
191
  object.public_send method
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.1.43"
2
+ VERSION = "0.2.1"
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.43
4
+ version: 0.2.1
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-15 00:00:00.000000000 Z
11
+ date: 2024-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -69,12 +69,15 @@ files:
69
69
  - app/assets/javascripts/hw_combobox/helpers.js
70
70
  - app/assets/javascripts/hw_combobox/models/combobox.js
71
71
  - app/assets/javascripts/hw_combobox/models/combobox/actors.js
72
+ - app/assets/javascripts/hw_combobox/models/combobox/announcements.js
72
73
  - app/assets/javascripts/hw_combobox/models/combobox/async_loading.js
73
74
  - app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js
74
75
  - app/assets/javascripts/hw_combobox/models/combobox/base.js
75
76
  - app/assets/javascripts/hw_combobox/models/combobox/dialog.js
76
77
  - app/assets/javascripts/hw_combobox/models/combobox/events.js
77
78
  - app/assets/javascripts/hw_combobox/models/combobox/filtering.js
79
+ - app/assets/javascripts/hw_combobox/models/combobox/form_field.js
80
+ - app/assets/javascripts/hw_combobox/models/combobox/multiselect.js
78
81
  - app/assets/javascripts/hw_combobox/models/combobox/navigation.js
79
82
  - app/assets/javascripts/hw_combobox/models/combobox/new_options.js
80
83
  - app/assets/javascripts/hw_combobox/models/combobox/options.js
@@ -86,15 +89,20 @@ files:
86
89
  - app/assets/stylesheets/hotwire_combobox.css
87
90
  - app/presenters/hotwire_combobox/component.rb
88
91
  - app/presenters/hotwire_combobox/component/customizable.rb
92
+ - app/presenters/hotwire_combobox/listbox/group.rb
93
+ - app/presenters/hotwire_combobox/listbox/item.rb
94
+ - app/presenters/hotwire_combobox/listbox/item/collection.rb
89
95
  - app/presenters/hotwire_combobox/listbox/option.rb
90
96
  - app/views/hotwire_combobox/_component.html.erb
91
97
  - app/views/hotwire_combobox/_next_page.turbo_stream.erb
92
98
  - app/views/hotwire_combobox/_paginated_options.turbo_stream.erb
93
99
  - app/views/hotwire_combobox/_pagination.html.erb
100
+ - app/views/hotwire_combobox/_selection_chip.turbo_stream.erb
94
101
  - app/views/hotwire_combobox/combobox/_dialog.html.erb
95
102
  - app/views/hotwire_combobox/combobox/_hidden_field.html.erb
96
103
  - app/views/hotwire_combobox/combobox/_input.html.erb
97
104
  - app/views/hotwire_combobox/combobox/_paginated_listbox.html.erb
105
+ - app/views/hotwire_combobox/layouts/_selection_chip.turbo_stream.erb
98
106
  - config/hw_importmap.rb
99
107
  - lib/hotwire_combobox.rb
100
108
  - lib/hotwire_combobox/engine.rb
@@ -122,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
130
  - !ruby/object:Gem::Version
123
131
  version: '0'
124
132
  requirements: []
125
- rubygems_version: 3.5.6
133
+ rubygems_version: 3.4.18
126
134
  signing_key:
127
135
  specification_version: 4
128
136
  summary: Accessible Autocomplete for Rails apps