katalyst-tables 3.0.0.beta1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -2
  3. data/README.md +56 -190
  4. data/app/assets/builds/katalyst/tables.esm.js +17 -47
  5. data/app/assets/builds/katalyst/tables.js +17 -47
  6. data/app/assets/builds/katalyst/tables.min.js +1 -1
  7. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  8. data/app/components/concerns/katalyst/tables/has_table_content.rb +17 -8
  9. data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
  10. data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
  11. data/app/components/concerns/katalyst/tables/selectable.rb +18 -75
  12. data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
  13. data/app/components/katalyst/table_component.html.erb +4 -4
  14. data/app/components/katalyst/table_component.rb +271 -53
  15. data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
  16. data/app/components/katalyst/tables/body_row_component.rb +4 -31
  17. data/app/components/katalyst/tables/cell_component.rb +85 -0
  18. data/app/components/katalyst/tables/{body → cells}/boolean_component.rb +8 -2
  19. data/app/components/katalyst/tables/{body → cells}/currency_component.rb +7 -7
  20. data/app/components/katalyst/tables/{body → cells}/date_component.rb +12 -9
  21. data/app/components/katalyst/tables/{body → cells}/date_time_component.rb +13 -10
  22. data/app/components/katalyst/tables/{body → cells}/number_component.rb +5 -5
  23. data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
  24. data/app/components/katalyst/tables/{body → cells}/rich_text_component.rb +8 -5
  25. data/app/components/katalyst/tables/cells/select_component.rb +39 -0
  26. data/app/components/katalyst/tables/data.rb +30 -0
  27. data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
  28. data/app/components/katalyst/tables/header_row_component.rb +4 -25
  29. data/app/components/katalyst/tables/label.rb +37 -0
  30. data/app/components/katalyst/tables/orderable/form_component.rb +38 -0
  31. data/app/components/katalyst/tables/selectable/form_component.html.erb +3 -3
  32. data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
  33. data/app/controllers/concerns/katalyst/tables/backend.rb +2 -28
  34. data/app/helpers/katalyst/tables/frontend.rb +48 -2
  35. data/app/javascript/tables/application.js +0 -5
  36. data/app/javascript/tables/orderable/form_controller.js +8 -6
  37. data/app/javascript/tables/orderable/item_controller.js +9 -0
  38. data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
  39. data/app/models/concerns/katalyst/tables/collection/sorting.rb +85 -17
  40. data/app/models/katalyst/tables/collection/array.rb +38 -0
  41. data/app/models/katalyst/tables/collection/base.rb +4 -0
  42. data/config/locales/tables.en.yml +0 -6
  43. data/lib/katalyst/tables/config.rb +23 -0
  44. data/lib/katalyst/tables.rb +9 -0
  45. metadata +22 -29
  46. data/app/components/concerns/katalyst/tables/body/typed_columns.rb +0 -132
  47. data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
  48. data/app/components/concerns/katalyst/tables/header/typed_columns.rb +0 -179
  49. data/app/components/katalyst/tables/body/attachment_component.rb +0 -58
  50. data/app/components/katalyst/tables/body/link_component.rb +0 -40
  51. data/app/components/katalyst/tables/body_cell_component.rb +0 -55
  52. data/app/components/katalyst/tables/header/attachment_component.rb +0 -15
  53. data/app/components/katalyst/tables/header/boolean_component.rb +0 -15
  54. data/app/components/katalyst/tables/header/currency_component.rb +0 -15
  55. data/app/components/katalyst/tables/header/date_component.rb +0 -15
  56. data/app/components/katalyst/tables/header/date_time_component.rb +0 -15
  57. data/app/components/katalyst/tables/header/link_component.rb +0 -15
  58. data/app/components/katalyst/tables/header/number_component.rb +0 -15
  59. data/app/components/katalyst/tables/header/rich_text_component.rb +0 -15
  60. data/app/components/katalyst/tables/header_cell_component.rb +0 -97
  61. data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
  62. data/app/javascript/tables/turbo/collection_controller.js +0 -38
  63. data/app/models/katalyst/tables/collection/sort_form.rb +0 -120
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac8dcee0526a034b23487868fb642c389e69c323aaf463a7aa6896bf777d5bc1
4
- data.tar.gz: 8cea19cc8467137e533b9146daa65f51dacdf78763e4fde7904542261cbc66b3
3
+ metadata.gz: cda2701479ecd72f9964ab539c5203bd0c300aeb962cf926552904575474ddea
4
+ data.tar.gz: 6f43fc5e31c8536811e28cba0db4be3c0967db24d06ad22f2a72c89512030d87
5
5
  SHA512:
6
- metadata.gz: ea6efd6150d3bb2d9f9d48c89920831f97e6dede515cfcd99ec049f81328c32c373fbf49cfd798adde0f1d8055819e1eb4f40931f8ac392809006c384c219c98
7
- data.tar.gz: 66480c79be3aa1762145c3d92723f284ed61749d46029600d20541820e2d831e266fa79ffa0de9ad77efc750ba80f274f7fee86267b7a946cad78ba8c49773da
6
+ metadata.gz: 14acdb5b09216c84c06b2082ed2a11888f0930b1807f7ade14cbc7fa02751f3c888a69b5397df7eb028395b7902eaad47abaa710e33fa24ecc6d6ed98757616f
7
+ data.tar.gz: f3721287c65a5aa0d4e81c36d5f704b90e2995cf372376ba19c9b03cb5b0129a5f4c8a56b369a09b2c62e72eb585163d656abb303a8dd62aa8b7912193f710b5
data/CHANGELOG.md CHANGED
@@ -1,8 +1,18 @@
1
- ## [3.0.0.beta1]
2
- - Breaking change: remove Turbo Streams from table and pagination components, focus preservation is handled via Turbo Morph
1
+ ## [3.0.0]
2
+
3
+ - Breaking change: remove Turbo Streams from table and pagination components,
4
+ focus preservation is handled via Turbo Morph
3
5
  - Improve spec coverage
6
+ - Re-write internals to make it easier to extend and customize
4
7
  - Update examples in [README](README.md) and [docs](/docs) to reflect changes
5
8
 
9
+ If you're upgrading from 2.x, you'll need to change your controllers to use
10
+ the recommendations from the README. The changes should be straightforward,
11
+ but you will need to enable morphing to allow focus preservation.
12
+
13
+ In general, we don't recommend using row partials anymore, as it's easier to
14
+ read the code when the row is defined in the index view.
15
+
6
16
  ## [2.6.0]
7
17
 
8
18
  - Added table row selection
data/README.md CHANGED
@@ -24,33 +24,48 @@ import tables from "@katalyst/tables";
24
24
  application.load(tables);
25
25
  ```
26
26
 
27
+ ```scss
28
+ // app/assets/stylesheets/application.scss
29
+ @use "@katalyst/tables";
30
+ ```
31
+
27
32
  ## Usage
28
33
 
29
34
  This gem provides entry points for backend and frontend concerns:
30
- * `Katalyst::TableComponent` can be used render encapsulated tables, it calls a
31
- partial for each row.
35
+ * `Katalyst::TableComponent` can be used render encapsulated tables.
32
36
  * `Katalyst::Tables::Frontend` provides `table_with` for inline table generation
33
37
  * `Katalyst::Tables::Collection::Base` provides a default entry point for
34
38
  building collections in your controller actions.
35
39
 
36
- ## Frontend
40
+ ### Frontend
41
+
42
+ Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
43
+
44
+ You can use the `table_with` helper to generate a table inline in your view without explicitly interacting with the
45
+ table component. This is the preferred approach when creating tables. However, if the table is complex or you need to
46
+ reuse it, you should consider moving the definition of the row into a partial.
37
47
 
38
- Use `Katalyst::TableComponent` to build a table component from an ActiveRecord
39
- collection, or from a `Katalyst::Tables::Collection::Base` instance.
48
+ ```erb
49
+ <%= table_with collection: @people do |row, person| %>
50
+ <% row.text :name do |cell| %>
51
+ <%= link_to cell.value, [:edit, person] %>
52
+ <% end %>
53
+ <% row.text :email %>
54
+ <% end %>
55
+ ```
40
56
 
41
- For example, if you render `Katalyst::TableComponent.new(collection: @people)`,
42
- the table component will look for a partial called `_person.html+row.erb` and
43
- render it for each row (and once for the header row).
57
+ By not providing a block to the `table_with` call, the gem will look for a partial called `_person.html+row.erb` to
58
+ render each row:
44
59
 
45
60
  ```erb
46
61
  <%# locals: { row:, person: nil } %>
47
- <% row.cell :name do |cell| %>
62
+ <% row.text :name do |cell| %>
48
63
  <%= link_to cell.value, [:edit, person] %>
49
64
  <% end %>
50
- <% row.cell :email %>
65
+ <% row.text :email %>
51
66
  ```
52
67
 
53
- The table component will call your partial once per row and accumulate the cells
68
+ The table will call your block / partial once per row and accumulate the cells
54
69
  you generate into rows, including a header row:
55
70
 
56
71
  ```html
@@ -79,52 +94,36 @@ You can customize the partial and/or the name of the resource in a similar style
79
94
  to view partials:
80
95
 
81
96
  ```erb
82
- <%= render Katalyst::TableComponent.new(collection: @employees, as: :person, partial: "person") %>
83
- ```
84
-
85
- ### Inline tables
86
-
87
- You can use the `table_with` helper to generate a table inline in your view without explicitly interacting with the
88
- table component. This is primarily intended for backwards compatibility, but it can be useful for simple tables.
89
-
90
- Add `include Katalyst::Tables::Frontend` to your `ApplicationHelper` or similar.
91
-
92
- ```erb
93
- <%= table_with collection: @people do |row, person| %>
94
- <% row.cell :name do |cell| %>
95
- <%= link_to cell.value, [:edit, person] %>
96
- <% end %>
97
- <% row.cell :email %>
98
- <% end %>
97
+ <%= table_with(collection: @employees, as: :person, partial: "person") %>
99
98
  ```
100
99
 
101
100
  ### HTML Attributes
102
101
 
103
102
  You can add custom attributes on table, row, and cell tags.
104
103
 
105
- The table tag takes attributes passed to `TableComponent` or via the call to `table_with`, similar to `form_with`:
104
+ The table tag takes attributes passed to `table_with` helper, similar to `form_with`:
106
105
 
107
106
  ```erb
108
- <%= TableComponent.new(collection: @people, id: "people-table")
107
+ <%= table_with(collection: @people, id: "people-table")
109
108
  ```
110
109
 
111
110
  Cells support the same approach:
112
111
 
113
112
  ```erb
114
- <%= row.cell :name, class: "name" %>
113
+ <%= row.text :name, class: "name" %>
115
114
  ```
116
115
 
117
116
  Rows do not get called directly, so instead you can assign to `html_attributes` on the row builder to customize row tag
118
117
  generation.
119
118
 
120
119
  ```erb
121
- <% row.html_attributes = { id: person.id } if row.body? %>
120
+ <% row.update_html_attributes(id: person.id) if row.body? %>
122
121
  ```
123
122
 
124
123
  Note: because the row builder gets called to generate the header row, you may need to guard calls that access the
125
124
  `person` directly as shown in the previous example. You could also check whether `person` is present.
126
125
 
127
- #### Headers
126
+ ### Headers
128
127
 
129
128
  Tables will automatically generate a header row for you by calling your row partial or provided block with no object.
130
129
  During this call, `row.header?` is true, `row.body?` is false, and the object (`person`) is nil.
@@ -133,14 +132,14 @@ All cells generated in the table header iteration will automatically be header c
133
132
  in your body rows by passing `heading: true` when you generate the cell.
134
133
 
135
134
  ```erb
136
- <% row.cell :id, heading: true %>
135
+ <% row.number :id, heading: true %>
137
136
  ```
138
137
 
139
138
  The table header cells default to showing the capitalized column name, but you can customize this in one of two ways:
140
139
 
141
140
  * Set the value inline
142
141
  ```erb
143
- <% row.cell :id, label: "ID" %>
142
+ <% row.number :id, label: "ID" %>
144
143
  ```
145
144
  * Define a translation for the attribute
146
145
  ```yml
@@ -163,194 +162,61 @@ the table cell. This is often all you need to do, but if you do want to customis
163
162
  the value you can pass a block instead:
164
163
 
165
164
  ```erb
166
- <% row.cell :status do %>
165
+ <% row.text :status do %>
167
166
  <%= person.password.present? ? "Active" : "Invited" %>
168
167
  <% end %>
169
168
  ```
170
169
 
171
- In the context of the block you have access the cell builder if you simply
170
+ In the context of the block you have access the cell component if you simply
172
171
  want to extend the default behaviour:
173
172
 
174
173
  ```erb
175
- <% row.cell :status do |cell| %>
176
- <%= link_to cell.value, person %>
174
+ <%# @type [Katalyst::Tables::CellComponent] cell %>
175
+ <% row.text :name do |cell| %>
176
+ <%= link_to cell, person %>
177
177
  <% end %>
178
178
  ```
179
179
 
180
- You can also assign to `html_attributes` on the cell builder, similar to the row
181
- builder, but please note that this will replace any options passed to the cell
182
- as arguments.
180
+ You can also update `html_attributes` on the cell builder, similar to the row
181
+ builder, see `katalyst-html-attributes` for details.
183
182
 
184
183
  ## Collections
185
184
 
186
185
  The `Katalyst::Tables::Collection::Base` class provides a convenient way to
187
186
  manage collections in your controller actions. It is designed to be used with
188
187
  Pagy for pagination and provides built-in sorting when used with ActiveRecord
189
- collections. Sorting and Pagination are off by default, but you can create
190
- a custom `ApplicationCollection` class that sets them on by default.
188
+ collections. Sorting and Pagination are off by default, you can either set them
189
+ on creation or create a custom `Collection` class that sets them on by default:
191
190
 
192
191
  ```ruby
193
- class ApplicationCollection < Katalyst::Tables::Collection::Base
194
- config.sorting = "name" # requires models have a name attribute
192
+ # in #index
193
+ Katalyst::Tables::Collection::Base.new(sorting: "name asc", pagination: true)
194
+ # or as a nested class in your controller
195
+ class Collection < Katalyst::Tables::Collection::Base
196
+ config.sorting = "name asc" # requires models have a name attribute
195
197
  config.pagination = true
196
198
  end
197
199
  ```
198
200
 
199
- You can then use this class in your controller actions:
200
-
201
- ```ruby
202
- class PeopleController < ApplicationController
203
- def index
204
- @people = ApplicationCollection.new.with_params(params).apply(People.all)
205
- end
206
- end
207
- ```
208
-
209
- Collections can be passed directly to `TableComponent` and it will automatically
201
+ Collections can be passed directly to `table_with` method and it will automatically
210
202
  detect features such as sorting and generate the appropriate table header links.
211
203
 
212
204
  ```erb
213
- <%= render TableComponent.new(collection: @people) %>
214
- ```
215
-
216
- ## Sort
217
-
218
- When sort is enabled, table columns will be automatically sortable in the
219
- frontend for any column that corresponds to an attribute on the model. You can
220
- also add sorting to non-attribute columns by defining a scope in your
221
- model:
222
-
223
- ```
224
- scope :order_by_status, ->(direction) { ... }
225
- ```
226
-
227
- You can also use sort without using collections, this was the primary backend
228
- interface for V1 and takes design cues from Pagy. Start by including the backend
229
- in your controller(s):
230
-
231
- ```ruby
232
- include Katalyst::Tables::Backend
233
- ```
234
-
235
- Now, in your controller index actions, you can sort your active record
236
- collections based on the `sort` param which is appended to the current URL as a
237
- get parameter when a user clicks on a column header.
238
-
239
- Building on our example from earlier:
240
-
241
- ```ruby
242
- class PeopleController < ApplicationController
243
- include Katalyst::Tables::Backend
244
-
245
- def index
246
- @people = People.all
247
-
248
- @sort, @people = table_sort(@people) # sort
249
- end
250
- end
251
- ```
252
-
253
- You then add the sort form object to your view so that it can add column header
254
- links and show the current sort state:
255
-
256
- ```erb
257
- <%= table_with collection: @people, sort: @sort do |row, person| %>
258
- <%= row.cell :name %>
259
- <%= row.cell :email %>
260
- <% end %>
261
- ```
262
-
263
- ## Pagination
264
-
265
- This gem designed to work with [pagy](https://github.com/ddnexus/pagy/).
266
-
267
- If you use collections and enable pagination then pagy will be called internally
268
- and the pagy metadata will be available as `pagination` on the collection.
269
-
270
- `Katalyst::Tables::PagyNavComponent` can be used to render the pagination links
271
- for a collection.
272
-
273
- ```erb
274
- <%= render Katalyst::Tables::PagyNavComponent.new(collection: @people) %>
205
+ <%= table_with(collection:) %>
275
206
  ```
276
207
 
277
208
  ## Extensions
278
209
 
279
- The following extensions are available:
210
+ The following extensions are available and activated by default:
280
211
 
212
+ * [Identifiable](docs/identifiable.md) - adds default dom ids to the table and data rows.
281
213
  * [Orderable](docs/orderable.md) - adds bulk-update for 'ordinal' columns via dragging rows in the table.
214
+ * [Pagination](docs/pagination.md) - handles paginating of data in the collection.
282
215
  * [Selectable](docs/selectable.md) - adds bulk-action support for rows in the table.
216
+ * [Sortable](docs/sortable.md) - table column headers that can be sorted will be wrapped in links.
217
+ * [Customization](docs/customization.md) - customize the table and cell rendering.
283
218
 
284
- ## Customization
285
-
286
- A common pattern we use is to have a cell at the end of the table for actions. For example:
287
-
288
- ```html
289
- <table class="action-table">
290
- <thead>
291
- <tr>
292
- <th>Name</th>
293
- <th class="actions"></th>
294
- </tr>
295
- </thead>
296
- <tbody>
297
- <tr>
298
- <td>Alice</td>
299
- <td class="actions">
300
- <a href="/people/1/edit">Edit</a>
301
- <a href="/people/1" method="delete">Delete</a>
302
- </td>
303
- </tr>
304
- </tbody>
305
- </table>
306
- ```
307
-
308
- You can write a custom component that helps generate this type of table by
309
- adding the required classes and adding helpers for generating the actions.
310
- This allows for a declarative table syntax, something like this:
311
-
312
- ```erb
313
- <%= render ActionTableComponent.new(collection:) do |row| %>
314
- <% row.cell :name %>
315
- <% row.actions do |cell| %>
316
- <%= cell.action "Edit", :edit %>
317
- <%= cell.action "Delete", :delete, method: :delete %>
318
- <% end %>
319
- <% end %>
320
- ```
321
-
322
- And the customized component:
323
-
324
- ```ruby
325
- class ActionTableComponent < Katalyst::TableComponent
326
-
327
- config.header_row = "ActionHeaderRow"
328
- config.body_row = "ActionBodyRow"
329
- config.body_cell = "ActionBodyCell"
330
-
331
- def default_html_attributes
332
- { class: "action-table" }
333
- end
334
-
335
- class ActionHeaderRow < Katalyst::Tables::HeaderRowComponent
336
- def actions(&block)
337
- cell(:actions, class: "actions", label: "", &block)
338
- end
339
- end
340
-
341
- class ActionBodyRow < Katalyst::Tables::BodyRowComponent
342
- def actions(&block)
343
- cell(:actions, class: "actions", &block)
344
- end
345
- end
346
-
347
- class ActionBodyCell < Katalyst::Tables::BodyCellComponent
348
- def action(label, href, **attrs)
349
- content_tag(:a, label, href: href, **attrs)
350
- end
351
- end
352
- end
353
- ```
219
+ You can disable extensions by altering the `Katalyst::Tables.config.component_extensions` before initialization.
354
220
 
355
221
  ## Development
356
222
 
@@ -1,41 +1,4 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
- import { Turbo } from '@hotwired/turbo-rails';
3
-
4
- class TurboCollectionController extends Controller {
5
- static values = {
6
- query: String,
7
- sort: String,
8
- };
9
-
10
- queryValueChanged(query) {
11
- Turbo.navigator.history.replace(this.#url(query));
12
- }
13
-
14
- sortValueChanged(sort) {
15
- document.querySelectorAll(this.#sortSelector).forEach((input) => {
16
- if (input) input.value = sort;
17
- });
18
- }
19
-
20
- get #sortSelector() {
21
- return "input[name='sort']";
22
- }
23
-
24
- #url(query) {
25
- const frame = this.element.closest("turbo-frame");
26
- let url;
27
-
28
- if (frame) {
29
- url = new URL(frame.baseURI);
30
- } else {
31
- url = new URL(window.location.href);
32
- }
33
-
34
- url.search = query;
35
-
36
- return url;
37
- }
38
- }
39
2
 
40
3
  class OrderableRowController extends Controller {
41
4
  static values = {
@@ -85,6 +48,15 @@ class OrderableRowController extends Controller {
85
48
  this.index = index;
86
49
  }
87
50
 
51
+ /** Retrieve params for use in the form */
52
+ params(scope) {
53
+ const { id_name, id_value, index_name } = this.paramsValue;
54
+ return [
55
+ { name: `${scope}[${id_value}][${id_name}]`, value: this.id },
56
+ { name: `${scope}[${id_value}][${index_name}]`, value: this.index },
57
+ ];
58
+ }
59
+
88
60
  /**
89
61
  * Restore any visual changes made during drag and remove the drag state.
90
62
  */
@@ -450,13 +422,15 @@ class DragState {
450
422
  }
451
423
 
452
424
  class OrderableFormController extends Controller {
425
+ static values = { scope: String };
426
+
453
427
  add(item) {
454
- const { id_name, id_value, index_name } = item.paramsValue;
455
- this.element.insertAdjacentHTML(
456
- "beforeend",
457
- `<input type="hidden" name="${id_name}" value="${id_value}" data-generated>
458
- <input type="hidden" name="${index_name}" value="${item.index}" data-generated>`,
459
- );
428
+ item.params(this.scopeValue).forEach(({ name, value }) => {
429
+ this.element.insertAdjacentHTML(
430
+ "beforeend",
431
+ `<input type="hidden" name="${name}" value="${value}" data-generated>`,
432
+ );
433
+ });
460
434
  }
461
435
 
462
436
  submit() {
@@ -560,10 +534,6 @@ class SelectionItemController extends Controller {
560
534
  }
561
535
 
562
536
  const Definitions = [
563
- {
564
- identifier: "tables--turbo--collection",
565
- controllerConstructor: TurboCollectionController,
566
- },
567
537
  {
568
538
  identifier: "tables--orderable--item",
569
539
  controllerConstructor: OrderableRowController,
@@ -1,41 +1,4 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
- import { Turbo } from '@hotwired/turbo-rails';
3
-
4
- class TurboCollectionController extends Controller {
5
- static values = {
6
- query: String,
7
- sort: String,
8
- };
9
-
10
- queryValueChanged(query) {
11
- Turbo.navigator.history.replace(this.#url(query));
12
- }
13
-
14
- sortValueChanged(sort) {
15
- document.querySelectorAll(this.#sortSelector).forEach((input) => {
16
- if (input) input.value = sort;
17
- });
18
- }
19
-
20
- get #sortSelector() {
21
- return "input[name='sort']";
22
- }
23
-
24
- #url(query) {
25
- const frame = this.element.closest("turbo-frame");
26
- let url;
27
-
28
- if (frame) {
29
- url = new URL(frame.baseURI);
30
- } else {
31
- url = new URL(window.location.href);
32
- }
33
-
34
- url.search = query;
35
-
36
- return url;
37
- }
38
- }
39
2
 
40
3
  class OrderableRowController extends Controller {
41
4
  static values = {
@@ -85,6 +48,15 @@ class OrderableRowController extends Controller {
85
48
  this.index = index;
86
49
  }
87
50
 
51
+ /** Retrieve params for use in the form */
52
+ params(scope) {
53
+ const { id_name, id_value, index_name } = this.paramsValue;
54
+ return [
55
+ { name: `${scope}[${id_value}][${id_name}]`, value: this.id },
56
+ { name: `${scope}[${id_value}][${index_name}]`, value: this.index },
57
+ ];
58
+ }
59
+
88
60
  /**
89
61
  * Restore any visual changes made during drag and remove the drag state.
90
62
  */
@@ -450,13 +422,15 @@ class DragState {
450
422
  }
451
423
 
452
424
  class OrderableFormController extends Controller {
425
+ static values = { scope: String };
426
+
453
427
  add(item) {
454
- const { id_name, id_value, index_name } = item.paramsValue;
455
- this.element.insertAdjacentHTML(
456
- "beforeend",
457
- `<input type="hidden" name="${id_name}" value="${id_value}" data-generated>
458
- <input type="hidden" name="${index_name}" value="${item.index}" data-generated>`,
459
- );
428
+ item.params(this.scopeValue).forEach(({ name, value }) => {
429
+ this.element.insertAdjacentHTML(
430
+ "beforeend",
431
+ `<input type="hidden" name="${name}" value="${value}" data-generated>`,
432
+ );
433
+ });
460
434
  }
461
435
 
462
436
  submit() {
@@ -560,10 +534,6 @@ class SelectionItemController extends Controller {
560
534
  }
561
535
 
562
536
  const Definitions = [
563
- {
564
- identifier: "tables--turbo--collection",
565
- controllerConstructor: TurboCollectionController,
566
- },
567
537
  {
568
538
  identifier: "tables--orderable--item",
569
539
  controllerConstructor: OrderableRowController,
@@ -1,2 +1,2 @@
1
- import{Controller as t}from"@hotwired/stimulus";import{Turbo as e}from"@hotwired/turbo-rails";class s{constructor(t,e,s){this.cursorOffset=e.offsetY,this.initialPosition=e.target.offsetTop-t.offsetTop,this.targetId=s}updateCursor(t,e,s,i){this.listOffset=t.getBoundingClientRect().top;let r=s.clientY-this.listOffset-this.cursorOffset;this.#t(t,e,r,i)}updateScroll(t,e,s){const i=this.listOffset;this.listOffset=t.getBoundingClientRect().top;const r=i-this.listOffset,a=this.position+r;this.#t(t,e,a,s)}#t(t,e,s,i){s=Math.max(s,0),s=Math.min(s,t.offsetHeight-e.offsetHeight),this.position=s;i(s-this.initialPosition)}}class i extends t{static outlets=["tables--selection--form"];static values={params:Object,checked:Boolean};tablesSelectionFormOutletConnected(t){this.checkedValue=t.isSelected(this.id)}change(t){t.preventDefault(),this.checkedValue=this.tablesSelectionFormOutlet.toggle(this.id)}get id(){return this.paramsValue.id}checkedValueChanged(t){this.element.querySelector("input").checked=t}}const r=[{identifier:"tables--turbo--collection",controllerConstructor:class extends t{static values={query:String,sort:String};queryValueChanged(t){e.navigator.history.replace(this.#e(t))}sortValueChanged(t){document.querySelectorAll(this.#s).forEach((e=>{e&&(e.value=t)}))}get#s(){return"input[name='sort']"}#e(t){const e=this.element.closest("turbo-frame");let s;return s=e?new URL(e.baseURI):new URL(window.location.href),s.search=t,s}}},{identifier:"tables--orderable--item",controllerConstructor:class extends t{static values={params:Object};connect(){var t;this.index=(t=this.row,Array.from(t.parentElement.children).indexOf(t))}paramsValueChanged(t){this.id=t.id_value}dragUpdate(t){this.dragOffset=t,this.row.style.position="relative",this.row.style.top=t+"px",this.row.style.zIndex="1",this.row.toggleAttribute("dragging",!0)}updateVisually(t){this.row.style.position="relative",this.row.style.top=this.row.offsetHeight*(t-this.dragIndex)+"px"}updateIndex(t){this.index=t}reset(){delete this.dragOffset,this.row.removeAttribute("style"),this.row.removeAttribute("dragging")}get hasChanges(){return this.paramsValue.index_value!==this.index}get dragIndex(){return this.dragOffset&&0!==this.dragOffset?this.index+Math.round(this.dragOffset/this.row.offsetHeight):this.index}get comparisonIndex(){return this.dragOffset?this.dragIndex+(this.dragOffset>0?.5:-.5):this.index}get row(){return this.element.parentElement}}},{identifier:"tables--orderable--list",controllerConstructor:class extends t{static outlets=["tables--orderable--item","tables--orderable--form"];startDragging(t){this.dragState=t,document.addEventListener("mousemove",this.mousemove),document.addEventListener("mouseup",this.mouseup),window.addEventListener("scroll",this.scroll,!0),this.element.style.position="relative"}stopDragging(){const t=this.dragState;return delete this.dragState,document.removeEventListener("mousemove",this.mousemove),document.removeEventListener("mouseup",this.mouseup),window.removeEventListener("scroll",this.scroll,!0),this.element.removeAttribute("style"),this.tablesOrderableItemOutlets.forEach((t=>t.reset())),t}drop(){const t=this.dragItem;if(!t)return;const e=t.dragIndex,s=this.tablesOrderableItemOutlets[e];s&&(e<t.index?s.row.insertAdjacentElement("beforebegin",t.row):e>t.index&&s.row.insertAdjacentElement("afterend",t.row),this.tablesOrderableItemOutlets.forEach(((t,e)=>t.updateIndex(e))),this.commitChanges())}commitChanges(){this.tablesOrderableFormOutlet.clear(),this.tablesOrderableItemOutlets.forEach((t=>{t.hasChanges&&this.tablesOrderableFormOutlet.add(t)})),this.tablesOrderableFormOutlet.submit()}mousedown(t){if(this.isDragging)return;const e=this.#i(t.target);e&&(t.preventDefault(),this.startDragging(new s(this.element,t,e.id)),this.dragState.updateCursor(this.element,e.row,t,this.animate))}mousemove=t=>{this.isDragging&&(t.preventDefault(),this.ticking||(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateCursor(this.element,this.dragItem.row,t,this.animate)}))))};scroll=t=>{this.isDragging&&!this.ticking&&(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateScroll(this.element,this.dragItem.row,this.animate)})))};mouseup=t=>{this.isDragging&&(this.drop(),this.stopDragging(),this.tablesOrderableFormOutlets.forEach((t=>delete t.dragState)))};tablesOrderableFormOutletConnected(t,e){t.dragState&&this.startDragging(t.dragState)}tablesOrderableFormOutletDisconnected(t,e){this.isDragging&&(t.dragState=this.stopDragging())}animate=t=>{const e=this.dragItem;e.dragUpdate(t),this.#r.forEach(((t,s)=>{t!==e&&t.updateVisually(s)}))};get isDragging(){return!!this.dragState}get dragItem(){return this.isDragging?this.tablesOrderableItemOutlets.find((t=>t.id===this.dragState.targetId)):null}get#r(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#i(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{add(t){const{id_name:e,id_value:s,index_name:i}=t.paramsValue;this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${e}" value="${s}" data-generated>\n <input type="hidden" name="${i}" value="${t.index}" data-generated>`)}submit(){0!==this.inputs.length&&this.element.requestSubmit()}clear(){this.inputs.forEach((t=>t.remove()))}get inputs(){return this.element.querySelectorAll("input[data-generated]")}}},{identifier:"tables--selection--form",controllerConstructor:class extends t{static values={count:Number,primaryKey:{type:String,default:"id"}};static targets=["count","singular","plural"];connect(){this.countValue=this.inputs.length}toggle(t){const e=this.input(t);return e?e.remove():this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${this.primaryKeyValue}[]" value="${t}">`),this.countValue=this.inputs.length,!e}isSelected(t){return!!this.input(t)}get inputs(){return this.element.querySelectorAll(`input[name="${this.primaryKeyValue}[]"]`)}input(t){return this.element.querySelector(`input[name="${this.primaryKeyValue}[]"][value="${t}"]`)}countValueChanged(t){this.element.toggleAttribute("hidden",0===t),this.countTarget.textContent=t,this.singularTarget.toggleAttribute("hidden",1!==t),this.pluralTarget.toggleAttribute("hidden",1===t)}}},{identifier:"tables--selection--item",controllerConstructor:i}];export{r as default};
1
+ import{Controller as t}from"@hotwired/stimulus";class e{constructor(t,e,s){this.cursorOffset=e.offsetY,this.initialPosition=e.target.offsetTop-t.offsetTop,this.targetId=s}updateCursor(t,e,s,i){this.listOffset=t.getBoundingClientRect().top;let r=s.clientY-this.listOffset-this.cursorOffset;this.#t(t,e,r,i)}updateScroll(t,e,s){const i=this.listOffset;this.listOffset=t.getBoundingClientRect().top;const r=i-this.listOffset,a=this.position+r;this.#t(t,e,a,s)}#t(t,e,s,i){s=Math.max(s,0),s=Math.min(s,t.offsetHeight-e.offsetHeight),this.position=s;i(s-this.initialPosition)}}class s extends t{static outlets=["tables--selection--form"];static values={params:Object,checked:Boolean};tablesSelectionFormOutletConnected(t){this.checkedValue=t.isSelected(this.id)}change(t){t.preventDefault(),this.checkedValue=this.tablesSelectionFormOutlet.toggle(this.id)}get id(){return this.paramsValue.id}checkedValueChanged(t){this.element.querySelector("input").checked=t}}const i=[{identifier:"tables--orderable--item",controllerConstructor:class extends t{static values={params:Object};connect(){var t;this.index=(t=this.row,Array.from(t.parentElement.children).indexOf(t))}paramsValueChanged(t){this.id=t.id_value}dragUpdate(t){this.dragOffset=t,this.row.style.position="relative",this.row.style.top=t+"px",this.row.style.zIndex="1",this.row.toggleAttribute("dragging",!0)}updateVisually(t){this.row.style.position="relative",this.row.style.top=this.row.offsetHeight*(t-this.dragIndex)+"px"}updateIndex(t){this.index=t}params(t){const{id_name:e,id_value:s,index_name:i}=this.paramsValue;return[{name:`${t}[${s}][${e}]`,value:this.id},{name:`${t}[${s}][${i}]`,value:this.index}]}reset(){delete this.dragOffset,this.row.removeAttribute("style"),this.row.removeAttribute("dragging")}get hasChanges(){return this.paramsValue.index_value!==this.index}get dragIndex(){return this.dragOffset&&0!==this.dragOffset?this.index+Math.round(this.dragOffset/this.row.offsetHeight):this.index}get comparisonIndex(){return this.dragOffset?this.dragIndex+(this.dragOffset>0?.5:-.5):this.index}get row(){return this.element.parentElement}}},{identifier:"tables--orderable--list",controllerConstructor:class extends t{static outlets=["tables--orderable--item","tables--orderable--form"];startDragging(t){this.dragState=t,document.addEventListener("mousemove",this.mousemove),document.addEventListener("mouseup",this.mouseup),window.addEventListener("scroll",this.scroll,!0),this.element.style.position="relative"}stopDragging(){const t=this.dragState;return delete this.dragState,document.removeEventListener("mousemove",this.mousemove),document.removeEventListener("mouseup",this.mouseup),window.removeEventListener("scroll",this.scroll,!0),this.element.removeAttribute("style"),this.tablesOrderableItemOutlets.forEach((t=>t.reset())),t}drop(){const t=this.dragItem;if(!t)return;const e=t.dragIndex,s=this.tablesOrderableItemOutlets[e];s&&(e<t.index?s.row.insertAdjacentElement("beforebegin",t.row):e>t.index&&s.row.insertAdjacentElement("afterend",t.row),this.tablesOrderableItemOutlets.forEach(((t,e)=>t.updateIndex(e))),this.commitChanges())}commitChanges(){this.tablesOrderableFormOutlet.clear(),this.tablesOrderableItemOutlets.forEach((t=>{t.hasChanges&&this.tablesOrderableFormOutlet.add(t)})),this.tablesOrderableFormOutlet.submit()}mousedown(t){if(this.isDragging)return;const s=this.#e(t.target);s&&(t.preventDefault(),this.startDragging(new e(this.element,t,s.id)),this.dragState.updateCursor(this.element,s.row,t,this.animate))}mousemove=t=>{this.isDragging&&(t.preventDefault(),this.ticking||(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateCursor(this.element,this.dragItem.row,t,this.animate)}))))};scroll=t=>{this.isDragging&&!this.ticking&&(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateScroll(this.element,this.dragItem.row,this.animate)})))};mouseup=t=>{this.isDragging&&(this.drop(),this.stopDragging(),this.tablesOrderableFormOutlets.forEach((t=>delete t.dragState)))};tablesOrderableFormOutletConnected(t,e){t.dragState&&this.startDragging(t.dragState)}tablesOrderableFormOutletDisconnected(t,e){this.isDragging&&(t.dragState=this.stopDragging())}animate=t=>{const e=this.dragItem;e.dragUpdate(t),this.#s.forEach(((t,s)=>{t!==e&&t.updateVisually(s)}))};get isDragging(){return!!this.dragState}get dragItem(){return this.isDragging?this.tablesOrderableItemOutlets.find((t=>t.id===this.dragState.targetId)):null}get#s(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#e(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{static values={scope:String};add(t){t.params(this.scopeValue).forEach((({name:t,value:e})=>{this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${t}" value="${e}" data-generated>`)}))}submit(){0!==this.inputs.length&&this.element.requestSubmit()}clear(){this.inputs.forEach((t=>t.remove()))}get inputs(){return this.element.querySelectorAll("input[data-generated]")}}},{identifier:"tables--selection--form",controllerConstructor:class extends t{static values={count:Number,primaryKey:{type:String,default:"id"}};static targets=["count","singular","plural"];connect(){this.countValue=this.inputs.length}toggle(t){const e=this.input(t);return e?e.remove():this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${this.primaryKeyValue}[]" value="${t}">`),this.countValue=this.inputs.length,!e}isSelected(t){return!!this.input(t)}get inputs(){return this.element.querySelectorAll(`input[name="${this.primaryKeyValue}[]"]`)}input(t){return this.element.querySelector(`input[name="${this.primaryKeyValue}[]"][value="${t}"]`)}countValueChanged(t){this.element.toggleAttribute("hidden",0===t),this.countTarget.textContent=t,this.singularTarget.toggleAttribute("hidden",1!==t),this.pluralTarget.toggleAttribute("hidden",1===t)}}},{identifier:"tables--selection--item",controllerConstructor:s}];export{i as default};
2
2
  //# sourceMappingURL=tables.min.js.map