hotwire_combobox 0.2.0 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddf9dd6a046c3871c1b3935a8a2e9e899a88787ba6d39e673ec8eee792c920dc
4
- data.tar.gz: f6d207f71a8ce4779df0cc12cd4264106b1fb086319bc8ad4c303a22604f2f1c
3
+ metadata.gz: fbd0907dd22a30683ba48f331a588c125f47ad6ad05d3bd613b5fc44047ed153
4
+ data.tar.gz: 330118f1c3cf332a93fe29f3ccf25b4a287362047d1301608c838d601d2f920c
5
5
  SHA512:
6
- metadata.gz: 5cb541bd15115ee71ed82dea3d2c922abcd9c8279f835b7d6074fd825cbd08e6ac40d1b181b398ee6d00a1e4c7d7fc3f18892134cc362aa98640519ee27b9030
7
- data.tar.gz: a6adeb3593a0f25d9a726be101c52c00488d367592d134c089d520cc6a087eff67f00b5f965fcc8b1c09e6c1f6f96e955815d234cdc19162a1dc657a83271183
6
+ metadata.gz: 5cb2373c38c8f9bbe0d7d0276183e516100effd4973c87a9587d2d45dc854cb54dbb0fa55eef0db8eac2226399878c3d02f0868ae1ae6ca45185db0f5b15739d
7
+ data.tar.gz: eca06ee9703ec8929bede6a1b706a86e76905c7cd7be23233c2fe997c47db67d412ea5ea437c67416fe43aeb07679074b3f5d493ce491308f99edf78c1df664b
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Easy and Accessible Autocomplete for Ruby on Rails
6
6
 
7
- [![CI Tests](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci_tests.yml/badge.svg)](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci_tests.yml) [![Gem Version](https://badge.fury.io/rb/hotwire_combobox.svg)](https://badge.fury.io/rb/hotwire_combobox)
7
+ [![CI](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci.yml/badge.svg)](https://github.com/josefarias/hotwire_combobox/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/hotwire_combobox.svg)](https://badge.fury.io/rb/hotwire_combobox)
8
8
 
9
9
 
10
10
  > [!IMPORTANT]
@@ -61,11 +61,15 @@ export default class HwComboboxController extends Concerns(...concerns) {
61
61
  initialize() {
62
62
  this._initializeActors()
63
63
  this._initializeFiltering()
64
- this._initializeMultiselect()
65
64
  }
66
65
 
67
66
  connect() {
67
+ this.idempotentConnect()
68
+ }
69
+
70
+ idempotentConnect() {
68
71
  this._connectSelection()
72
+ this._connectMultiselect()
69
73
  this._connectListAutocomplete()
70
74
  this._connectDialog()
71
75
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- HotwireCombobox 0.2.0
2
+ HotwireCombobox 0.2.2
3
3
  */
4
4
  import { Controller } from '@hotwired/stimulus';
5
5
 
@@ -776,7 +776,7 @@ Combobox.Multiselect = Base => class extends Base {
776
776
  }
777
777
  }
778
778
 
779
- _initializeMultiselect() {
779
+ _connectMultiselect() {
780
780
  if (!this._isMultiPreselected) {
781
781
  this._preselectMultiple();
782
782
  this._markMultiPreselected();
@@ -1702,11 +1702,15 @@ class HwComboboxController extends Concerns(...concerns) {
1702
1702
  initialize() {
1703
1703
  this._initializeActors();
1704
1704
  this._initializeFiltering();
1705
- this._initializeMultiselect();
1706
1705
  }
1707
1706
 
1708
1707
  connect() {
1708
+ this.idempotentConnect();
1709
+ }
1710
+
1711
+ idempotentConnect() {
1709
1712
  this._connectSelection();
1713
+ this._connectMultiselect();
1710
1714
  this._connectListAutocomplete();
1711
1715
  this._connectDialog();
1712
1716
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- HotwireCombobox 0.2.0
2
+ HotwireCombobox 0.2.2
3
3
  */
4
4
  (function (global, factory) {
5
5
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@hotwired/stimulus')) :
@@ -780,7 +780,7 @@ HotwireCombobox 0.2.0
780
780
  }
781
781
  }
782
782
 
783
- _initializeMultiselect() {
783
+ _connectMultiselect() {
784
784
  if (!this._isMultiPreselected) {
785
785
  this._preselectMultiple();
786
786
  this._markMultiPreselected();
@@ -1706,11 +1706,15 @@ HotwireCombobox 0.2.0
1706
1706
  initialize() {
1707
1707
  this._initializeActors();
1708
1708
  this._initializeFiltering();
1709
- this._initializeMultiselect();
1710
1709
  }
1711
1710
 
1712
1711
  connect() {
1712
+ this.idempotentConnect();
1713
+ }
1714
+
1715
+ idempotentConnect() {
1713
1716
  this._connectSelection();
1717
+ this._connectMultiselect();
1714
1718
  this._connectListAutocomplete();
1715
1719
  this._connectDialog();
1716
1720
  }
@@ -55,7 +55,7 @@ Combobox.Multiselect = Base => class extends Base {
55
55
  }
56
56
  }
57
57
 
58
- _initializeMultiselect() {
58
+ _connectMultiselect() {
59
59
  if (!this._isMultiPreselected) {
60
60
  this._preselectMultiple()
61
61
  this._markMultiPreselected()
@@ -2,6 +2,7 @@
2
2
  --hw-active-bg-color: #F3F4F6;
3
3
  --hw-border-color: #D1D5DB;
4
4
  --hw-group-color: #57595C;
5
+ --hw-group-bg-color: #FFFFFF;
5
6
  --hw-invalid-color: #EF4444;
6
7
  --hw-dialog-label-color: #1D1D1D;
7
8
  --hw-focus-color: #2563EB;
@@ -78,7 +79,7 @@
78
79
  }
79
80
 
80
81
  .hw-combobox__input {
81
- border: 0;
82
+ border: none;
82
83
  font-size: inherit;
83
84
  line-height: var(--hw-line-height);
84
85
  min-width: 0;
@@ -87,7 +88,10 @@
87
88
  width: 100%;
88
89
  }
89
90
 
90
- .hw-combobox__input:focus-visible {
91
+ .hw-combobox__input:focus,
92
+ .hw-combobox__input:focus-visible,
93
+ .hw-combobox__input:focus-within {
94
+ box-shadow: none;
91
95
  outline: none;
92
96
  }
93
97
 
@@ -145,6 +149,7 @@
145
149
  }
146
150
 
147
151
  .hw-combobox__group__label {
152
+ background-color: var(--hw-group-bg-color);
148
153
  color: var(--hw-group-color);
149
154
  padding: var(--hw-padding--slim);
150
155
  }
@@ -80,15 +80,7 @@ class HotwireCombobox::Component
80
80
 
81
81
  def announcer_attrs
82
82
  {
83
- style: "
84
- position: absolute;
85
- width: 1px;
86
- height: 1px;
87
- margin: -1px;
88
- padding: 0;
89
- overflow: hidden;
90
- clip: rect(0, 0, 0, 0);
91
- border: 0;".squish,
83
+ class: "hw-combobox__announcer",
92
84
  aria: announcer_aria,
93
85
  data: announcer_data
94
86
  }
@@ -238,23 +230,29 @@ class HotwireCombobox::Component
238
230
  end
239
231
 
240
232
  def prefilled_display
241
- return if multiselect?
233
+ return if multiselect? || !hidden_field_value
242
234
 
243
235
  if async_src && associated_object
244
236
  associated_object.to_combobox_display
245
- elsif hidden_field_value
246
- options.find { |option| option.value == hidden_field_value }&.autocompletable_as
237
+ elsif async_src && form_object&.respond_to?(name)
238
+ form_object.public_send name
239
+ else
240
+ options.find_by_value(hidden_field_value)&.autocompletable_as
247
241
  end
248
242
  end
249
243
 
250
244
  def associated_object
251
245
  @associated_object ||= if association_exists?
252
- form.object.public_send association_name
246
+ form_object&.public_send association_name
253
247
  end
254
248
  end
255
249
 
256
250
  def association_exists?
257
- form&.object&.class&.reflect_on_association(association_name).present?
251
+ form_object&.class&.reflect_on_association(association_name).present?
252
+ end
253
+
254
+ def form_object
255
+ form&.object
258
256
  end
259
257
 
260
258
  def async_src
@@ -302,10 +300,10 @@ class HotwireCombobox::Component
302
300
  def hidden_field_value
303
301
  return value if value
304
302
 
305
- if form&.object&.defined_enums&.try :[], name
306
- form.object.public_send "#{name}_before_type_cast"
303
+ if form_object&.defined_enums&.try :[], name
304
+ form_object.public_send "#{name}_before_type_cast"
307
305
  else
308
- form&.object&.try(name).then do |value|
306
+ form_object&.try(name).then do |value|
309
307
  value.respond_to?(:map) ? value.join(",") : value
310
308
  end
311
309
  end
@@ -329,7 +327,8 @@ class HotwireCombobox::Component
329
327
  click@window->hw-combobox#closeOnClickOutside
330
328
  focusin@window->hw-combobox#closeOnFocusOutside
331
329
  turbo:before-stream-render@document->hw-combobox#rerouteListboxStreamToDialog
332
- turbo:before-cache@document->hw-combobox#hideChipsForCache".squish,
330
+ turbo:before-cache@document->hw-combobox#hideChipsForCache
331
+ turbo:morph-element->hw-combobox#idempotentConnect".squish,
333
332
  hw_combobox_target: "combobox",
334
333
  async_id: canonical_id
335
334
  end
@@ -1,6 +1,8 @@
1
1
  require "securerandom"
2
2
 
3
3
  class HotwireCombobox::Listbox::Group
4
+ attr_reader :options
5
+
4
6
  def initialize(name, options:)
5
7
  @name = name
6
8
  @options = options
@@ -17,7 +19,7 @@ class HotwireCombobox::Listbox::Group
17
19
  end
18
20
 
19
21
  private
20
- attr_reader :name, :options
22
+ attr_reader :name
21
23
 
22
24
  def id
23
25
  @id ||= SecureRandom.uuid
@@ -0,0 +1,14 @@
1
+ class HotwireCombobox::Listbox::Item::Collection < Array
2
+ def find_by_value(value)
3
+ if grouped?
4
+ flat_map { |item| item.options }.find { |option| option.value == value }
5
+ else
6
+ find { |option| option.value == value }
7
+ end
8
+ end
9
+
10
+ private
11
+ def grouped?
12
+ first.is_a? HotwireCombobox::Listbox::Group
13
+ end
14
+ end
@@ -1,7 +1,7 @@
1
1
  class HotwireCombobox::Listbox::Item
2
2
  class << self
3
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).items
4
+ new(view, options, render_in: render_in, include_blank: include_blank, **custom_methods).collection
5
5
  end
6
6
  end
7
7
 
@@ -13,10 +13,10 @@ class HotwireCombobox::Listbox::Item
13
13
  @custom_methods = custom_methods
14
14
  end
15
15
 
16
- def items
16
+ def collection
17
17
  items = groups_or_options
18
18
  items.unshift(blank_option) if include_blank.present?
19
- items
19
+ Collection.new items
20
20
  end
21
21
 
22
22
  private
@@ -31,7 +31,7 @@ class HotwireCombobox::Listbox::Item
31
31
  end
32
32
 
33
33
  def grouped?
34
- key, value = options.to_a.first
34
+ _key, value = options.to_a.first
35
35
  value.is_a? Array
36
36
  end
37
37
 
@@ -43,8 +43,12 @@ class HotwireCombobox::Listbox::Item
43
43
  end
44
44
 
45
45
  def create_listbox_options(options)
46
- options.map do |option|
47
- HotwireCombobox::Listbox::Option.new **option_attrs(option)
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
48
52
  end
49
53
  end
50
54
 
@@ -93,12 +97,15 @@ class HotwireCombobox::Listbox::Item
93
97
  end
94
98
 
95
99
  def extract_blank_display_and_content
96
- if include_blank.is_a? Hash
100
+ case include_blank
101
+ when Hash
97
102
  text = include_blank.delete(:text)
98
103
 
99
104
  [ text, render_content(render_opts: include_blank, object: text, attrs: { display: text, value: "" }) ]
100
- else
105
+ when String
101
106
  [ include_blank, include_blank ]
107
+ else
108
+ [ "", "&nbsp;".html_safe ]
102
109
  end
103
110
  end
104
111
  end
@@ -17,10 +17,6 @@ class HotwireCombobox::Listbox::Option
17
17
  option.try(:autocompletable_as) || option.try(:display)
18
18
  end
19
19
 
20
- def content
21
- option.try(:content) || option.try(:display)
22
- end
23
-
24
20
  private
25
21
  Data = Struct.new :id, :value, :display, :content, :blank, :filterable_as, :autocompletable_as, keyword_init: true
26
22
 
@@ -57,6 +53,10 @@ class HotwireCombobox::Listbox::Option
57
53
  option.try(:filterable_as) || option.try(:display)
58
54
  end
59
55
 
56
+ def content
57
+ option.try(:content) || option.try(:display)
58
+ end
59
+
60
60
  def blank?
61
61
  option.try(:blank).present?
62
62
  end
@@ -4,6 +4,19 @@
4
4
  <%= render "hotwire_combobox/combobox/hidden_field", component: component %>
5
5
 
6
6
  <%= tag.div **component.main_wrapper_attrs do %>
7
+ <%# Announcer styles defined here because they're not optional %>
8
+ <%= tag.style nonce: content_security_policy_nonce do %>
9
+ .hw-combobox__announcer {
10
+ position: absolute;
11
+ width: 1px;
12
+ height: 1px;
13
+ margin: -1px;
14
+ padding: 0;
15
+ overflow: hidden;
16
+ clip: rect(0, 0, 0, 0);
17
+ border: 0;
18
+ }
19
+ <% end %>
7
20
  <%= tag.div **component.announcer_attrs %>
8
21
  <%= render "hotwire_combobox/combobox/input", component: component %>
9
22
  <%= render "hotwire_combobox/combobox/paginated_listbox", component: component %>
@@ -1,6 +1,6 @@
1
- <%# locals: (for_id:, next_page:, src:) -%>
1
+ <%# locals: (for_id:, src:) -%>
2
2
 
3
3
  <%= turbo_stream.remove hw_pagination_frame_wrapper_id(for_id) %>
4
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
+ <%= render "hotwire_combobox/pagination", for_id: for_id, src: src %>
6
6
  <% end %>
@@ -26,22 +26,18 @@ module HotwireCombobox
26
26
  include_blank: nil,
27
27
  display: :to_combobox_display,
28
28
  **custom_methods)
29
- if options.first.is_a? HotwireCombobox::Listbox::Option
30
- options
31
- else
32
- HotwireCombobox::Listbox::Item.collection_for \
33
- self,
34
- options,
35
- render_in: render_in,
36
- include_blank: include_blank,
37
- **custom_methods.merge(display: display)
38
- end
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)
39
35
  end
40
36
 
41
37
  def hw_paginated_combobox_options(
42
38
  options,
43
39
  for_id: params[:for_id],
44
- src: request.path,
40
+ src: request.fullpath,
45
41
  next_page: nil,
46
42
  render_in: {},
47
43
  include_blank: {},
@@ -49,7 +45,7 @@ module HotwireCombobox
49
45
  include_blank = params[:page].to_i > 0 ? nil : include_blank
50
46
  options = hw_combobox_options options, render_in: render_in, include_blank: include_blank, **custom_methods
51
47
  this_page = render "hotwire_combobox/paginated_options", for_id: for_id, options: options
52
- next_page = render "hotwire_combobox/next_page", for_id: for_id, src: src, next_page: next_page
48
+ next_page = render "hotwire_combobox/next_page", for_id: for_id, src: hw_combobox_next_page_uri(src, next_page, for_id)
53
49
 
54
50
  safe_join [ this_page, next_page ]
55
51
  end
@@ -150,23 +146,13 @@ module HotwireCombobox
150
146
  "#{id}__hw_combobox_pagination"
151
147
  end
152
148
 
153
- def hw_combobox_next_page_uri(uri, next_page, for_id)
154
- if next_page
155
- hw_uri_with_params uri,
156
- page: next_page,
157
- q: params[:q],
158
- for_id: for_id,
159
- format: :turbo_stream
160
- end
161
- end
162
-
163
149
  def hw_combobox_page_stream_action
164
150
  params[:page].to_i > 0 ? :append : :update
165
151
  end
166
152
 
167
153
  def hw_uri_with_params(url_or_path, **params)
168
154
  URI.parse(url_or_path).tap do |url_or_path|
169
- query = URI.decode_www_form(url_or_path.query || "").to_h.merge(params)
155
+ query = URI.decode_www_form(url_or_path.query || "").to_h.merge(params.compact_blank.stringify_keys)
170
156
  url_or_path.query = URI.encode_www_form query
171
157
  end.to_s
172
158
  rescue URI::InvalidURIError
@@ -182,9 +168,19 @@ module HotwireCombobox
182
168
  end
183
169
 
184
170
  private
171
+ def hw_combobox_next_page_uri(uri, next_page, for_id)
172
+ return unless next_page
173
+
174
+ hw_uri_with_params uri,
175
+ page: next_page,
176
+ q: params[:q],
177
+ for_id: for_id,
178
+ format: :turbo_stream
179
+ end
180
+
185
181
  def hw_extract_options_and_src(options_or_src, render_in, include_blank)
186
182
  if options_or_src.is_a? String
187
- [ [], options_or_src ]
183
+ [ hw_combobox_options([]), options_or_src ]
188
184
  else
189
185
  [ hw_combobox_options(options_or_src, render_in: render_in, include_blank: include_blank), nil ]
190
186
  end
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.2"
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.2.0
4
+ version: 0.2.2
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-22 00:00:00.000000000 Z
11
+ date: 2024-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.1'
55
69
  description: An accessible autocomplete for Ruby on Rails apps using Hotwire.
56
70
  email:
57
71
  - jose@farias.mx
@@ -91,6 +105,7 @@ files:
91
105
  - app/presenters/hotwire_combobox/component/customizable.rb
92
106
  - app/presenters/hotwire_combobox/listbox/group.rb
93
107
  - app/presenters/hotwire_combobox/listbox/item.rb
108
+ - app/presenters/hotwire_combobox/listbox/item/collection.rb
94
109
  - app/presenters/hotwire_combobox/listbox/option.rb
95
110
  - app/views/hotwire_combobox/_component.html.erb
96
111
  - app/views/hotwire_combobox/_next_page.turbo_stream.erb
@@ -129,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
144
  - !ruby/object:Gem::Version
130
145
  version: '0'
131
146
  requirements: []
132
- rubygems_version: 3.5.6
147
+ rubygems_version: 3.5.7
133
148
  signing_key:
134
149
  specification_version: 4
135
150
  summary: Accessible Autocomplete for Rails apps