katalyst-tables 3.3.0 → 3.3.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/assets/builds/katalyst/tables.esm.js +332 -6
  4. data/app/assets/builds/katalyst/tables.js +332 -6
  5. data/app/assets/builds/katalyst/tables.min.js +1 -1
  6. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  7. data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -1
  8. data/app/assets/stylesheets/katalyst/tables/_query.scss +109 -0
  9. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +1 -1
  10. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +1 -1
  11. data/app/components/katalyst/table_component.rb +13 -2
  12. data/app/components/katalyst/tables/cells/currency_component.rb +21 -2
  13. data/app/components/katalyst/tables/cells/number_component.rb +31 -1
  14. data/app/components/katalyst/tables/query/input_component.html.erb +12 -0
  15. data/app/components/katalyst/tables/query/input_component.rb +46 -0
  16. data/app/components/katalyst/tables/query/modal_component.html.erb +33 -0
  17. data/app/components/katalyst/tables/query/modal_component.rb +89 -0
  18. data/app/components/katalyst/tables/query_component.html.erb +8 -0
  19. data/app/components/katalyst/tables/{filter_component.rb → query_component.rb} +37 -30
  20. data/app/controllers/concerns/katalyst/tables/backend.rb +14 -0
  21. data/app/helpers/katalyst/tables/frontend.rb +14 -8
  22. data/app/javascript/tables/application.js +8 -3
  23. data/app/javascript/tables/query_controller.js +108 -0
  24. data/app/javascript/tables/query_input_controller.js +228 -0
  25. data/app/models/concerns/katalyst/tables/collection/core.rb +5 -3
  26. data/app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb +13 -26
  27. data/app/models/concerns/katalyst/tables/collection/query/parser.rb +16 -13
  28. data/app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb +11 -3
  29. data/app/models/concerns/katalyst/tables/collection/query/value_parser.rb +10 -5
  30. data/app/models/concerns/katalyst/tables/collection/query.rb +44 -6
  31. data/app/models/concerns/katalyst/tables/collection/sorting.rb +11 -1
  32. data/app/models/katalyst/tables/collection/base.rb +10 -0
  33. data/app/models/katalyst/tables/collection/filter.rb +10 -0
  34. data/app/models/katalyst/tables/collection/type/boolean.rb +11 -1
  35. data/app/models/katalyst/tables/collection/type/date.rb +19 -22
  36. data/app/models/katalyst/tables/collection/type/enum.rb +11 -0
  37. data/app/models/katalyst/tables/collection/type/float.rb +3 -39
  38. data/app/models/katalyst/tables/collection/type/helpers/delegate.rb +2 -22
  39. data/app/models/katalyst/tables/collection/type/helpers/extensions.rb +14 -0
  40. data/app/models/katalyst/tables/collection/type/helpers/multiple.rb +30 -0
  41. data/app/models/katalyst/tables/collection/type/helpers/range.rb +59 -0
  42. data/app/models/katalyst/tables/collection/type/integer.rb +3 -39
  43. data/app/models/katalyst/tables/collection/type/value.rb +22 -2
  44. metadata +12 -8
  45. data/app/assets/stylesheets/katalyst/tables/_filter.scss +0 -43
  46. data/app/components/katalyst/tables/filter/modal_component.html.erb +0 -25
  47. data/app/components/katalyst/tables/filter/modal_component.rb +0 -112
  48. data/app/components/katalyst/tables/filter_component.html.erb +0 -18
  49. data/app/javascript/tables/filter/modal_controller.js +0 -13
@@ -1,4 +1,4 @@
1
1
  // where psuedo selector has a specificity of 0 so it can be easily overwritten with simple selectors
2
2
  :where(th.type-date, td.type-date) {
3
- width: var(--width-medium);
3
+ width: var(--width-small);
4
4
  }
@@ -1,4 +1,4 @@
1
1
  // where psuedo selector has a specificity of 0 so it can be easily overwritten with simple selectors
2
- :where(th.type-datetime, td.type-date) {
2
+ :where(th.type-datetime, td.type-datetime) {
3
3
  width: var(--width-medium);
4
4
  }
@@ -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 expected to be in cents.
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
- value.present? ? number_to_currency(value / 100.0, @options) : ""
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? ? number_to_human(value) : ""
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&rsquo;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
@@ -0,0 +1,8 @@
1
+ <% if content? %>
2
+ <%= content %>
3
+ <% else %>
4
+ <%= form do |form| %>
5
+ <%= render query_input(form:) %>
6
+ <%= modal %>
7
+ <% end %>
8
+ <% 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::FilterComponent.new(collection: @people, url: peoples_path) %>
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::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
17
- # <%= filter.form(builder: GOVUKFormBuilder) do |form| %>
18
- # <%= form.govuk_text_field :query %>
19
- # <%= form.govuk_submit "Apply" %>
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::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
28
- # <% filter.with_modal(collection:) do |modal| %>
29
- # <% modal.with_footer do %>
30
- # <%= link_to "Docs", docs_path %>
31
- # <% end %>
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::Filter::ModalComponent
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) do |form|
61
- concat(form.hidden_field(:sort))
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
- private
67
+ def query_input(form:)
68
+ Query::InputComponent.new(form:, **input_attributes)
69
+ end
68
70
 
69
- def default_html_attributes
70
- {
71
- data: {
72
- controller: "tables--filter--modal",
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
- def default_input_attributes
77
+ private
78
+
79
+ def default_html_attributes
83
80
  {
84
81
  data: {
85
- action: "focus->tables--filter--modal#open",
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 FilterModalController from "./filter/modal_controller";
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--filter--modal",
31
- controllerConstructor: FilterModalController,
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
+ }