hotwire_combobox 0.2.0 → 0.2.2

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