hotwire_combobox 0.1.43 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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