katalyst-tables 3.3.1 → 3.3.3
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 +4 -4
- data/README.md +1 -1
- data/app/assets/builds/katalyst/tables.esm.js +332 -6
- data/app/assets/builds/katalyst/tables.js +332 -6
- data/app/assets/builds/katalyst/tables.min.js +1 -1
- data/app/assets/builds/katalyst/tables.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -1
- data/app/assets/stylesheets/katalyst/tables/_query.scss +109 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +1 -1
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +1 -1
- data/app/components/katalyst/table_component.rb +13 -2
- data/app/components/katalyst/tables/cells/currency_component.rb +21 -2
- data/app/components/katalyst/tables/cells/number_component.rb +31 -1
- data/app/components/katalyst/tables/query/input_component.html.erb +13 -0
- data/app/components/katalyst/tables/query/input_component.rb +50 -0
- data/app/components/katalyst/tables/query/modal_component.html.erb +33 -0
- data/app/components/katalyst/tables/query/modal_component.rb +89 -0
- data/app/components/katalyst/tables/query_component.html.erb +8 -0
- data/app/components/katalyst/tables/{filter_component.rb → query_component.rb} +37 -30
- data/app/controllers/concerns/katalyst/tables/backend.rb +14 -0
- data/app/helpers/katalyst/tables/frontend.rb +14 -8
- data/app/javascript/tables/application.js +8 -3
- data/app/javascript/tables/query_controller.js +108 -0
- data/app/javascript/tables/query_input_controller.js +228 -0
- data/app/models/concerns/katalyst/tables/collection/core.rb +2 -2
- data/app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb +13 -26
- data/app/models/concerns/katalyst/tables/collection/query/parser.rb +16 -13
- data/app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb +11 -3
- data/app/models/concerns/katalyst/tables/collection/query/value_parser.rb +10 -5
- data/app/models/concerns/katalyst/tables/collection/query.rb +42 -5
- data/app/models/concerns/katalyst/tables/collection/sorting.rb +11 -1
- data/app/models/katalyst/tables/collection/base.rb +10 -0
- data/app/models/katalyst/tables/collection/filter.rb +10 -0
- data/config/locales/tables.en.yml +2 -0
- data/{app/models → lib}/katalyst/tables/collection/type/boolean.rb +11 -1
- data/lib/katalyst/tables/collection/type/date.rb +57 -0
- data/lib/katalyst/tables/collection/type/enum.rb +32 -0
- data/lib/katalyst/tables/collection/type/float.rb +21 -0
- data/lib/katalyst/tables/collection/type/helpers/delegate.rb +32 -0
- data/{app/models → lib}/katalyst/tables/collection/type/helpers/extensions.rb +14 -0
- data/lib/katalyst/tables/collection/type/helpers/multiple.rb +60 -0
- data/lib/katalyst/tables/collection/type/helpers/range.rb +59 -0
- data/lib/katalyst/tables/collection/type/integer.rb +21 -0
- data/{app/models → lib}/katalyst/tables/collection/type/value.rb +22 -2
- data/{app/models → lib}/katalyst/tables/collection/type.rb +16 -0
- data/lib/katalyst/tables/collection.rb +11 -0
- data/lib/katalyst/tables.rb +6 -0
- metadata +26 -21
- data/app/assets/stylesheets/katalyst/tables/_filter.scss +0 -43
- data/app/components/katalyst/tables/filter/modal_component.html.erb +0 -25
- data/app/components/katalyst/tables/filter/modal_component.rb +0 -112
- data/app/components/katalyst/tables/filter_component.html.erb +0 -18
- data/app/javascript/tables/filter/modal_controller.js +0 -13
- data/app/models/katalyst/tables/collection/type/date.rb +0 -60
- data/app/models/katalyst/tables/collection/type/enum.rb +0 -21
- data/app/models/katalyst/tables/collection/type/float.rb +0 -57
- data/app/models/katalyst/tables/collection/type/helpers/delegate.rb +0 -50
- data/app/models/katalyst/tables/collection/type/helpers/multiple.rb +0 -30
- data/app/models/katalyst/tables/collection/type/integer.rb +0 -57
- /data/{app/models → lib}/katalyst/tables/collection/type/query.rb +0 -0
- /data/{app/models → lib}/katalyst/tables/collection/type/search.rb +0 -0
- /data/{app/models → lib}/katalyst/tables/collection/type/string.rb +0 -0
@@ -239,9 +239,20 @@ module Katalyst
|
|
239
239
|
|
240
240
|
# Generates a column from numeric values formatted appropriately.
|
241
241
|
#
|
242
|
+
# Supports Rails' built in number formatters, i.e.
|
243
|
+
# * +phone+: ActiveSupport::NumberHelper#number_to_phone
|
244
|
+
# * +currency+: ActiveSupport::NumberHelper#number_to_currency
|
245
|
+
# * +percentage+: ActiveSupport::NumberHelper#number_to_percentage
|
246
|
+
# * +delimited+: ActiveSupport::NumberHelper#number_to_delimited
|
247
|
+
# * +rounded+: ActiveSupport::NumberHelper#number_to_rounded
|
248
|
+
# * +human_size+: ActiveSupport::NumberHelper#number_to_human_size
|
249
|
+
# * +human+: ActiveSupport::NumberHelper#number_to_human
|
250
|
+
#
|
242
251
|
# @param column [Symbol] the column's name, called as a method on the record
|
243
252
|
# @param label [String|nil] the label to use for the column header
|
244
253
|
# @param heading [boolean] if true, data cells will use `th` tags
|
254
|
+
# @param format [String|Symbol] Rails number_to_X format option, defaults to +delimited+
|
255
|
+
# @param options [Hash] options to be passed to `number_to_<format>`
|
245
256
|
# @param ** [Hash] HTML attributes to be added to column cells
|
246
257
|
# @param & [Proc] optional block to alter the cell content
|
247
258
|
#
|
@@ -252,9 +263,9 @@ module Katalyst
|
|
252
263
|
#
|
253
264
|
# @example Render the number of comments on a post
|
254
265
|
# <% row.number :comment_count %> # => <td>0</td>
|
255
|
-
def number(column, label: nil, heading: false, **, &)
|
266
|
+
def number(column, label: nil, heading: false, format: :delimited, options: {}, **, &)
|
256
267
|
with_cell(Tables::Cells::NumberComponent.new(
|
257
|
-
collection:, row:, column:, record:, label:, heading:, **,
|
268
|
+
collection:, row:, column:, record:, label:, heading:, format:, options:, **,
|
258
269
|
), &)
|
259
270
|
end
|
260
271
|
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bigdecimal/util"
|
4
|
+
|
3
5
|
module Katalyst
|
4
6
|
module Tables
|
5
7
|
module Cells
|
6
8
|
# Formats the value as a money value
|
7
9
|
#
|
8
|
-
# The value is
|
10
|
+
# The value is assumed to be cents if integer, or dollars if float or
|
11
|
+
# decimal. Also supports RubyMoney type if defined.
|
12
|
+
#
|
9
13
|
# Adds a class to the cell to allow for custom styling
|
10
14
|
class CurrencyComponent < CellComponent
|
11
15
|
def initialize(options:, **)
|
@@ -15,7 +19,22 @@ module Katalyst
|
|
15
19
|
end
|
16
20
|
|
17
21
|
def rendered_value
|
18
|
-
|
22
|
+
format(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def format(value)
|
26
|
+
value.present? ? number_to_currency(value, @options) : ""
|
27
|
+
end
|
28
|
+
|
29
|
+
def value
|
30
|
+
case (v = super)
|
31
|
+
when nil
|
32
|
+
nil
|
33
|
+
when Integer
|
34
|
+
(super.to_d / BigDecimal("100"))
|
35
|
+
else
|
36
|
+
(v.to_d rescue nil) # rubocop:disable Style/RescueModifier
|
37
|
+
end
|
19
38
|
end
|
20
39
|
|
21
40
|
private
|
@@ -7,8 +7,38 @@ module Katalyst
|
|
7
7
|
#
|
8
8
|
# Adds a class to the cell to allow for custom styling
|
9
9
|
class NumberComponent < CellComponent
|
10
|
+
include ActiveSupport::NumberHelper
|
11
|
+
|
12
|
+
def initialize(format:, options:, **)
|
13
|
+
super(**)
|
14
|
+
|
15
|
+
@format = format
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def format(value)
|
20
|
+
case @format
|
21
|
+
when :phone
|
22
|
+
number_to_phone(value, @options)
|
23
|
+
when :currency
|
24
|
+
number_to_currency(value, @options)
|
25
|
+
when :percentage
|
26
|
+
number_to_percentage(value, @options)
|
27
|
+
when :delimited
|
28
|
+
number_to_delimited(value, @options)
|
29
|
+
when :rounded
|
30
|
+
number_to_rounded(value, @options)
|
31
|
+
when :human_size
|
32
|
+
number_to_human_size(value, @options)
|
33
|
+
when :human
|
34
|
+
number_to_human(value, @options)
|
35
|
+
else
|
36
|
+
raise ArgumentError, "Unsupported format #{@format}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
10
40
|
def rendered_value
|
11
|
-
value.present? ?
|
41
|
+
value.present? ? format(value) : ""
|
12
42
|
end
|
13
43
|
|
14
44
|
private
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<div class="query-input" data-controller="tables--query-input" data-turbo-permanent>
|
2
|
+
<%= form.label name, "Search", hidden: "" %>
|
3
|
+
<%= form.text_field(
|
4
|
+
name,
|
5
|
+
type: :search,
|
6
|
+
placeholder:,
|
7
|
+
autocomplete: "off",
|
8
|
+
**html_attributes,
|
9
|
+
) %>
|
10
|
+
<div aria-hidden="true" class="highlight" data-tables--query-input-target="highlight"></div>
|
11
|
+
<%= form.hidden_field(:p) %>
|
12
|
+
<%= form.button("Apply", type: :submit, name: nil, tabindex: -1) %>
|
13
|
+
</div>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Query
|
6
|
+
class InputComponent < ViewComponent::Base
|
7
|
+
include Katalyst::HtmlAttributes
|
8
|
+
|
9
|
+
attr_reader :form
|
10
|
+
|
11
|
+
def initialize(form:, **)
|
12
|
+
super(**)
|
13
|
+
|
14
|
+
@form = form
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
query_attribute || raise(ArgumentError, "No query attribute. " \
|
19
|
+
"Does your collection include Katalyst::Tables::Collection::Query?")
|
20
|
+
end
|
21
|
+
|
22
|
+
def collection
|
23
|
+
form.object
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_html_attributes
|
27
|
+
{
|
28
|
+
data: {
|
29
|
+
action: %w[
|
30
|
+
input->tables--query-input#update
|
31
|
+
keyup.enter->tables--query#closeModal
|
32
|
+
],
|
33
|
+
tables__query_input_target: "input",
|
34
|
+
},
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def placeholder
|
41
|
+
t("katalyst.tables.query.placeholder", name: collection.model_name.human.pluralize.downcase)
|
42
|
+
end
|
43
|
+
|
44
|
+
def query_attribute
|
45
|
+
collection.class.attribute_types.detect { |_, a| a.type == :query }&.first
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<%= tag.section(**html_attributes) do %>
|
2
|
+
<% if collection.errors.any? %>
|
3
|
+
<header>
|
4
|
+
<% collection.errors.each do |error| %>
|
5
|
+
<% next unless error.type == :unknown %>
|
6
|
+
<div class="error">Sorry, we don’t support the <code><%= error.attribute %></code> filter.</div>
|
7
|
+
<% end %>
|
8
|
+
</header>
|
9
|
+
<% end %>
|
10
|
+
<div class="content">
|
11
|
+
<% if show_values? %>
|
12
|
+
<h4>Possible values for <code><%= current_key %>:</code></h4>
|
13
|
+
<ul>
|
14
|
+
<% values_for(current_key).each do |value| %>
|
15
|
+
<li><code><%= format_value(value) %></code></li>
|
16
|
+
<% end %>
|
17
|
+
</ul>
|
18
|
+
<% else %>
|
19
|
+
<h4>Available filters:</h4>
|
20
|
+
<dl>
|
21
|
+
<% available_filters.each do |key, description| %>
|
22
|
+
<dt><code><%= key %>:</code></dt>
|
23
|
+
<dd>Filter on values for <%= description.downcase %></dd>
|
24
|
+
<% end %>
|
25
|
+
</dl>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
28
|
+
<% if footer? %>
|
29
|
+
<footer>
|
30
|
+
<%= footer %>
|
31
|
+
</footer>
|
32
|
+
<% end %>
|
33
|
+
<% end %>
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Query
|
6
|
+
class ModalComponent < ViewComponent::Base
|
7
|
+
include Katalyst::HtmlAttributes
|
8
|
+
include Katalyst::Tables::Frontend
|
9
|
+
|
10
|
+
renders_one :footer
|
11
|
+
|
12
|
+
attr_reader :collection, :url
|
13
|
+
|
14
|
+
def initialize(collection:, **)
|
15
|
+
super(**)
|
16
|
+
|
17
|
+
@collection = collection
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def default_html_attributes
|
23
|
+
{
|
24
|
+
class: "query-modal",
|
25
|
+
data: {
|
26
|
+
tables__query_target: "modal",
|
27
|
+
action: ["turbo:before-morph-attribute->tables--query#beforeMorphAttribute"],
|
28
|
+
},
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
using Collection::Type::Helpers::Extensions
|
33
|
+
|
34
|
+
def show_values?
|
35
|
+
current_key && attributes[current_key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_key
|
39
|
+
unless instance_variable_defined?(:@current_key)
|
40
|
+
attributes.each_key do |key|
|
41
|
+
@current_key = key if collection.query_active?(key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@current_key ||= nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def attributes
|
49
|
+
collection.class.attribute_types
|
50
|
+
.select { |_, a| a.filterable? && a.type != :search }
|
51
|
+
.to_h
|
52
|
+
end
|
53
|
+
|
54
|
+
def available_filters
|
55
|
+
keys = attributes.keys
|
56
|
+
|
57
|
+
if current_token.present?
|
58
|
+
keys = keys.select { |k| k.include?(current_token) }
|
59
|
+
end
|
60
|
+
|
61
|
+
keys.map do |key|
|
62
|
+
[key, collection.model.human_attribute_name(key)]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def values_for(key)
|
67
|
+
collection.examples_for(key).map(&:to_s).compact_blank
|
68
|
+
end
|
69
|
+
|
70
|
+
def format_value(value)
|
71
|
+
if /\A[\w.-]*\z/.match?(value)
|
72
|
+
value
|
73
|
+
else
|
74
|
+
%("#{value}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def current_token
|
79
|
+
return nil unless collection.position&.in?(0..collection.query.length)
|
80
|
+
|
81
|
+
prefix = collection.query[...collection.position].match(/\w*\z/)
|
82
|
+
suffix = collection.query[collection.position..].match(/\A\w*/)
|
83
|
+
|
84
|
+
"#{prefix}#{suffix}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Katalyst
|
4
4
|
module Tables
|
5
5
|
# A component for rendering a data driven filter for a collection.
|
6
|
-
# <%= Katalyst::Tables::
|
6
|
+
# <%= Katalyst::Tables::QueryComponent.new(collection: @people, url: peoples_path) %>
|
7
7
|
#
|
8
8
|
# By default, the component will render a form containing a single text field. Interacting with the
|
9
9
|
# text field will display a dropdown outlining all available keys and values to be filtered on.
|
@@ -13,30 +13,29 @@ module Katalyst
|
|
13
13
|
# to ensure the correct attributes and default form fields are collected.
|
14
14
|
# You can pass additional options to the `form` method to modify it.
|
15
15
|
#
|
16
|
-
# <%= Katalyst::Tables::
|
17
|
-
# <%=
|
18
|
-
#
|
19
|
-
#
|
16
|
+
# <%= Katalyst::Tables::QueryComponent.new(collection: @people, url: peoples_path) do |query| %>
|
17
|
+
# <%= query.form(builder: GOVUKFormBuilder) do |form| %>
|
18
|
+
# <%= form.govuk_text_field :q %>
|
19
|
+
# <%= form.govuk_submit "Apply" %>
|
20
|
+
# <%= modal %>
|
20
21
|
# <% end %>
|
21
22
|
# <% end %>
|
22
23
|
#
|
23
|
-
#
|
24
24
|
# Additionally the component allows for access to the dropdown that displays when interacting with the input.
|
25
25
|
# The dropdown supports additional "footer" content to be added.
|
26
26
|
#
|
27
|
-
# <%= Katalyst::Tables::
|
28
|
-
# <%
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
27
|
+
# <%= Katalyst::Tables::QueryComponent.new(collection: @people, url: peoples_path) do |query| %>
|
28
|
+
# <% query.with_modal(collection:) do |modal| %>
|
29
|
+
# <% modal.with_footer do %>
|
30
|
+
# <%= link_to "Docs", docs_path %>
|
31
|
+
# <% end %>
|
32
32
|
# <% end %>
|
33
33
|
# <% end %>
|
34
|
-
|
35
|
-
class FilterComponent < ViewComponent::Base
|
34
|
+
class QueryComponent < ViewComponent::Base
|
36
35
|
include Katalyst::HtmlAttributes
|
37
36
|
include Katalyst::Tables::Frontend
|
38
37
|
|
39
|
-
renders_one :modal, Katalyst::Tables::
|
38
|
+
renders_one :modal, Katalyst::Tables::Query::ModalComponent
|
40
39
|
|
41
40
|
define_html_attribute_methods :input_attributes
|
42
41
|
|
@@ -57,32 +56,40 @@ module Katalyst
|
|
57
56
|
form_with(model: collection,
|
58
57
|
url:,
|
59
58
|
method: :get,
|
60
|
-
**options
|
61
|
-
|
59
|
+
**options,
|
60
|
+
**html_attributes) do |form|
|
61
|
+
concat(sort_input(form:))
|
62
62
|
|
63
63
|
yield form if block_given?
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
-
|
67
|
+
def query_input(form:)
|
68
|
+
Query::InputComponent.new(form:, **input_attributes)
|
69
|
+
end
|
68
70
|
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
action: <<~ACTIONS.gsub(/\s+/, " "),
|
74
|
-
click@window->tables--filter--modal#close
|
75
|
-
click->tables--filter--modal#open:stop
|
76
|
-
keydown.esc->tables--filter--modal#close
|
77
|
-
ACTIONS
|
78
|
-
},
|
79
|
-
}
|
71
|
+
def sort_input(form:)
|
72
|
+
return if collection.default_sort?
|
73
|
+
|
74
|
+
form.hidden_field(:sort)
|
80
75
|
end
|
81
76
|
|
82
|
-
|
77
|
+
private
|
78
|
+
|
79
|
+
def default_html_attributes
|
83
80
|
{
|
84
81
|
data: {
|
85
|
-
|
82
|
+
controller: "tables--query",
|
83
|
+
turbo_action: :replace,
|
84
|
+
action: %w[
|
85
|
+
click@window->tables--query#closeModal
|
86
|
+
click->tables--query#openModal:stop
|
87
|
+
focusin@window->tables--query#closeModal
|
88
|
+
focusin->tables--query#openModal:stop
|
89
|
+
keydown.esc->tables--query#clear:stop
|
90
|
+
submit->tables--query#submit
|
91
|
+
input->tables--query#update
|
92
|
+
],
|
86
93
|
},
|
87
94
|
}
|
88
95
|
end
|
@@ -8,6 +8,7 @@ module Katalyst
|
|
8
8
|
|
9
9
|
included do
|
10
10
|
class_attribute :_default_table_component, instance_accessor: false
|
11
|
+
class_attribute :_default_table_query_component, instance_accessor: false
|
11
12
|
class_attribute :_default_summary_table_component, instance_accessor: false
|
12
13
|
end
|
13
14
|
|
@@ -20,6 +21,14 @@ module Katalyst
|
|
20
21
|
self._default_table_component = component
|
21
22
|
end
|
22
23
|
|
24
|
+
# Set the table query component to be used as the default for all tables
|
25
|
+
# in the views rendered by this controller and its subclasses.
|
26
|
+
#
|
27
|
+
# @param component [Class] the table query component class to use
|
28
|
+
def default_table_query_component(component)
|
29
|
+
self._default_table_query_component = component
|
30
|
+
end
|
31
|
+
|
23
32
|
# Set the summary table component to be used as the default for all
|
24
33
|
# summary tables in the views rendered by this controller and its
|
25
34
|
# subclasses.
|
@@ -35,6 +44,11 @@ module Katalyst
|
|
35
44
|
self.class._default_table_component
|
36
45
|
end
|
37
46
|
|
47
|
+
# Default table query component for this controller
|
48
|
+
def default_table_query_component
|
49
|
+
self.class._default_table_query_component
|
50
|
+
end
|
51
|
+
|
38
52
|
# Default summary table component for this controller
|
39
53
|
def default_summary_table_component
|
40
54
|
self.class._default_summary_table_component
|
@@ -55,6 +55,15 @@ module Katalyst
|
|
55
55
|
render(Selectable::FormComponent.new(collection:, id:, primary_key:), &)
|
56
56
|
end
|
57
57
|
|
58
|
+
# Construct a new query interface for filtering the current page.
|
59
|
+
#
|
60
|
+
# @param collection [Katalyst::Tables::Collection::Core] the collection to render
|
61
|
+
# @param url [String] the url to submit the form to (e.g. <resources>_path)
|
62
|
+
def table_query_with(collection:, url: url_for(action: :index), component: nil, &)
|
63
|
+
component ||= default_table_query_component_class
|
64
|
+
render(component.new(collection:, url:), &)
|
65
|
+
end
|
66
|
+
|
58
67
|
# Construct a new summary table component.
|
59
68
|
#
|
60
69
|
# @param model [ActiveRecord::Base] subject for the table
|
@@ -69,14 +78,6 @@ module Katalyst
|
|
69
78
|
render(component, &)
|
70
79
|
end
|
71
80
|
|
72
|
-
# Construct a new filter.
|
73
|
-
#
|
74
|
-
# @param collection [Katalyst::Tables::Collection::Core] the collection to render
|
75
|
-
# @param url [String] the url to submit the form to (e.g. <resources>_path)
|
76
|
-
def filter_with(collection:, url: url_for(action: :index), &)
|
77
|
-
render(FilterComponent.new(collection:, url:), &)
|
78
|
-
end
|
79
|
-
|
80
81
|
private
|
81
82
|
|
82
83
|
def default_table_component_class
|
@@ -84,6 +85,11 @@ module Katalyst
|
|
84
85
|
component.respond_to?(:constantize) ? component.constantize : component
|
85
86
|
end
|
86
87
|
|
88
|
+
def default_table_query_component_class
|
89
|
+
component = controller.try(:default_table_query_component) || QueryComponent
|
90
|
+
component.respond_to?(:constantize) ? component.constantize : component
|
91
|
+
end
|
92
|
+
|
87
93
|
def default_summary_table_component_class
|
88
94
|
component = controller.try(:default_summary_table_component) || SummaryTableComponent
|
89
95
|
component.respond_to?(:constantize) ? component.constantize : component
|
@@ -3,7 +3,8 @@ import OrderableListController from "./orderable/list_controller";
|
|
3
3
|
import OrderableFormController from "./orderable/form_controller";
|
4
4
|
import SelectionFormController from "./selection/form_controller";
|
5
5
|
import SelectionItemController from "./selection/item_controller";
|
6
|
-
import
|
6
|
+
import QueryController from "./query_controller";
|
7
|
+
import QueryInputController from "./query_input_controller";
|
7
8
|
|
8
9
|
const Definitions = [
|
9
10
|
{
|
@@ -27,8 +28,12 @@ const Definitions = [
|
|
27
28
|
controllerConstructor: SelectionItemController,
|
28
29
|
},
|
29
30
|
{
|
30
|
-
identifier: "tables--
|
31
|
-
controllerConstructor:
|
31
|
+
identifier: "tables--query",
|
32
|
+
controllerConstructor: QueryController,
|
33
|
+
},
|
34
|
+
{
|
35
|
+
identifier: "tables--query-input",
|
36
|
+
controllerConstructor: QueryInputController,
|
32
37
|
},
|
33
38
|
];
|
34
39
|
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class QueryController extends Controller {
|
4
|
+
static targets = ["modal"];
|
5
|
+
|
6
|
+
disconnect() {
|
7
|
+
delete this.pending;
|
8
|
+
|
9
|
+
document.removeEventListener("selectionchange", this.selection);
|
10
|
+
}
|
11
|
+
|
12
|
+
focus() {
|
13
|
+
if (document.activeElement === this.query) return;
|
14
|
+
|
15
|
+
this.query.addEventListener(
|
16
|
+
"focusin",
|
17
|
+
(e) => {
|
18
|
+
e.target.setSelectionRange(-1, -1);
|
19
|
+
},
|
20
|
+
{ once: true },
|
21
|
+
);
|
22
|
+
|
23
|
+
this.query.focus();
|
24
|
+
}
|
25
|
+
|
26
|
+
closeModal() {
|
27
|
+
delete this.modalTarget.dataset.open;
|
28
|
+
|
29
|
+
if (document.activeElement === this.query) document.activeElement.blur();
|
30
|
+
|
31
|
+
document.removeEventListener("selectionchange", this.selection);
|
32
|
+
}
|
33
|
+
|
34
|
+
openModal() {
|
35
|
+
this.modalTarget.dataset.open = true;
|
36
|
+
|
37
|
+
document.addEventListener("selectionchange", this.selection);
|
38
|
+
}
|
39
|
+
|
40
|
+
clear() {
|
41
|
+
if (this.query.value === "") {
|
42
|
+
// if the user presses escape once, browser clears the input
|
43
|
+
// if the user presses escape again, get them out of here
|
44
|
+
this.closeModal();
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
submit() {
|
49
|
+
const hasFocus = this.isFocused;
|
50
|
+
const position = hasFocus && this.query.selectionStart;
|
51
|
+
|
52
|
+
if (this.pending) {
|
53
|
+
clearTimeout(this.pending);
|
54
|
+
delete this.pending;
|
55
|
+
}
|
56
|
+
|
57
|
+
// prevent an unnecessary `?q=` parameter from appearing in the URL
|
58
|
+
if (this.query.value === "") {
|
59
|
+
this.query.disabled = true;
|
60
|
+
|
61
|
+
// restore input and focus after form submission
|
62
|
+
setTimeout(() => {
|
63
|
+
this.query.disabled = false;
|
64
|
+
if (hasFocus) this.query.focus();
|
65
|
+
}, 0);
|
66
|
+
}
|
67
|
+
|
68
|
+
// add/remove current cursor position
|
69
|
+
if (hasFocus && position) {
|
70
|
+
this.position.value = position;
|
71
|
+
this.position.disabled = false;
|
72
|
+
} else {
|
73
|
+
this.position.value = "";
|
74
|
+
this.position.disabled = true;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
update = () => {
|
79
|
+
if (this.pending) clearTimeout(this.pending);
|
80
|
+
this.pending = setTimeout(() => {
|
81
|
+
this.element.requestSubmit();
|
82
|
+
}, 300);
|
83
|
+
};
|
84
|
+
|
85
|
+
selection = () => {
|
86
|
+
if (this.isFocused) this.update();
|
87
|
+
};
|
88
|
+
|
89
|
+
beforeMorphAttribute(e) {
|
90
|
+
switch (e.detail.attributeName) {
|
91
|
+
case "data-open":
|
92
|
+
e.preventDefault();
|
93
|
+
break;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
get query() {
|
98
|
+
return this.element.querySelector("input[type=search]");
|
99
|
+
}
|
100
|
+
|
101
|
+
get position() {
|
102
|
+
return this.element.querySelector("input[name=p]");
|
103
|
+
}
|
104
|
+
|
105
|
+
get isFocused() {
|
106
|
+
return this.query === document.activeElement;
|
107
|
+
}
|
108
|
+
}
|