katalyst-tables 3.3.0 → 3.3.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/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 +12 -0
- data/app/components/katalyst/tables/query/input_component.rb +46 -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 +5 -3
- 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 +44 -6
- 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/app/models/katalyst/tables/collection/type/boolean.rb +11 -1
- data/app/models/katalyst/tables/collection/type/date.rb +19 -22
- data/app/models/katalyst/tables/collection/type/enum.rb +11 -0
- data/app/models/katalyst/tables/collection/type/float.rb +3 -39
- data/app/models/katalyst/tables/collection/type/helpers/delegate.rb +2 -22
- data/app/models/katalyst/tables/collection/type/helpers/extensions.rb +14 -0
- data/app/models/katalyst/tables/collection/type/helpers/multiple.rb +30 -0
- data/app/models/katalyst/tables/collection/type/helpers/range.rb +59 -0
- data/app/models/katalyst/tables/collection/type/integer.rb +3 -39
- data/app/models/katalyst/tables/collection/type/value.rb +22 -2
- metadata +12 -8
- 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
@@ -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,12 @@
|
|
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
|
+
autocomplete: "off",
|
7
|
+
**html_attributes,
|
8
|
+
) %>
|
9
|
+
<div aria-hidden="true" class="highlight" data-tables--query-input-target="highlight"></div>
|
10
|
+
<%= form.hidden_field(:p) %>
|
11
|
+
<%= form.button("Apply", type: :submit, name: nil, tabindex: -1) %>
|
12
|
+
</div>
|
@@ -0,0 +1,46 @@
|
|
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 query_attribute
|
41
|
+
collection.class.attribute_types.detect { |_, a| a.type == :query }&.first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
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
|
+
}
|