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 +4 -4
- data/README.md +1 -1
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +5 -1
- data/app/assets/javascripts/hotwire_combobox.esm.js +7 -3
- data/app/assets/javascripts/hotwire_combobox.umd.js +7 -3
- data/app/assets/javascripts/hw_combobox/models/combobox/multiselect.js +1 -1
- data/app/assets/stylesheets/hotwire_combobox.css +7 -2
- data/app/presenters/hotwire_combobox/component.rb +17 -18
- data/app/presenters/hotwire_combobox/listbox/group.rb +3 -1
- data/app/presenters/hotwire_combobox/listbox/item/collection.rb +14 -0
- data/app/presenters/hotwire_combobox/listbox/item.rb +15 -8
- data/app/presenters/hotwire_combobox/listbox/option.rb +4 -4
- data/app/views/hotwire_combobox/_component.html.erb +13 -0
- data/app/views/hotwire_combobox/_next_page.turbo_stream.erb +2 -2
- data/lib/hotwire_combobox/helper.rb +20 -24
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbd0907dd22a30683ba48f331a588c125f47ad6ad05d3bd613b5fc44047ed153
|
4
|
+
data.tar.gz: 330118f1c3cf332a93fe29f3ccf25b4a287362047d1301608c838d601d2f920c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
}
|
@@ -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:
|
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
|
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
|
-
|
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
|
246
|
-
|
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
|
-
|
246
|
+
form_object&.public_send association_name
|
253
247
|
end
|
254
248
|
end
|
255
249
|
|
256
250
|
def association_exists?
|
257
|
-
|
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
|
306
|
-
|
303
|
+
if form_object&.defined_enums&.try :[], name
|
304
|
+
form_object.public_send "#{name}_before_type_cast"
|
307
305
|
else
|
308
|
-
|
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
|
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
|
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).
|
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
|
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
|
-
|
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.
|
47
|
-
|
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
|
-
|
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
|
-
|
105
|
+
when String
|
101
106
|
[ include_blank, include_blank ]
|
107
|
+
else
|
108
|
+
[ "", " ".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:,
|
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:
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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.
|
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
|
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
|
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.
|
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-
|
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.
|
147
|
+
rubygems_version: 3.5.7
|
133
148
|
signing_key:
|
134
149
|
specification_version: 4
|
135
150
|
summary: Accessible Autocomplete for Rails apps
|