katalyst-tables 2.2.1 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 187059d630f338d5bcdd0ecc98eb3850fa7d4d01e6d37f0918143873c7a50033
4
- data.tar.gz: ac469214c3cdcf72b97ec5e5ab973356307d4d218d13face3ed48a82f9c09035
3
+ metadata.gz: 8224ebb910f428c3f7750ae6e79c92f21440f5546a2baaede473bb286f217a82
4
+ data.tar.gz: 6be02c8a263dda22b7faae734cb20e8cec5879db532adb50f257bd502fa51265
5
5
  SHA512:
6
- metadata.gz: 0a134c643f8780ff95287de876492b356adaa0e822b4fd72afe68980135859813cc5e9a1ac0c391bb227b27bc597a32c79266c1c88afcd005cc3186c3c30e27c
7
- data.tar.gz: 5eb871b2122d8edd2e95724f52d8a073ff4109e8281aadb1b7e8df231dd1f00d1096de80af370f8a3a6b42bfa8b9025254e29e2e16fb18ccc488c56222bee61e
6
+ metadata.gz: 61052d361a93ab2675274f9ed3b3f59e0acd08519261cf016c4db55006097d72f78114ba489e5eed9d066a82aa6bbaa9dd00675d378e24c996a5ccba775bce75
7
+ data.tar.gz: e411c014472cd5d34338ad0a02d869d69352fd4c6abe70c68ead05d75b3d2f0f52bf11c263031cd6606d27d09c7565e76770eee0b9149b58ef5e91ec383d86bf
@@ -1,20 +1,26 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  export default class OrderableFormController extends Controller {
4
- add(name, value) {
4
+ add(item) {
5
+ const { id_name, id_value, index_name } = item.paramsValue;
5
6
  this.element.insertAdjacentHTML(
6
7
  "beforeend",
7
- `<input type="hidden" name="${name}" value="${value}" data-generated>`,
8
+ `<input type="hidden" name="${id_name}" value="${id_value}" data-generated>
9
+ <input type="hidden" name="${index_name}" value="${item.index}" data-generated>`,
8
10
  );
9
11
  }
10
12
 
11
13
  submit() {
14
+ if (this.inputs.length === 0) return;
15
+
12
16
  this.element.requestSubmit();
13
17
  }
14
18
 
15
19
  clear() {
16
- this.element
17
- .querySelectorAll("input[data-generated]")
18
- .forEach((input) => input.remove());
20
+ this.inputs.forEach((input) => input.remove());
21
+ }
22
+
23
+ get inputs() {
24
+ return this.element.querySelectorAll("input[data-generated]");
19
25
  }
20
26
  }
@@ -2,7 +2,99 @@ import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  export default class OrderableRowController extends Controller {
4
4
  static values = {
5
- name: String,
6
- value: Number,
5
+ params: Object,
7
6
  };
7
+
8
+ paramsValueChanged(params) {
9
+ this.id = params.id_value;
10
+ this.index = params.index_value;
11
+ }
12
+
13
+ dragUpdate(offset) {
14
+ this.dragOffset = offset;
15
+ this.row.style.position = "relative";
16
+ this.row.style.top = offset + "px";
17
+ this.row.style.zIndex = 1;
18
+ this.row.toggleAttribute("dragging", true);
19
+ }
20
+
21
+ /**
22
+ * Called on items that are not the dragged item during drag. Updates the
23
+ * visual position of the item relative to the dragged item.
24
+ *
25
+ * @param index {number} intended index of the item during drag
26
+ */
27
+ updateVisually(index) {
28
+ this.row.style.position = "relative";
29
+ this.row.style.top = `${
30
+ this.row.offsetHeight * (index - this.dragIndex)
31
+ }px`;
32
+ }
33
+
34
+ /**
35
+ * Set the index value of the item. This is called on all items after a drop
36
+ * event. If the index is different to the params index then this item has
37
+ * changed.
38
+ *
39
+ * @param index {number} the new index value
40
+ */
41
+ updateIndex(index) {
42
+ this.index = index;
43
+ }
44
+
45
+ /**
46
+ * Restore any visual changes made during drag and remove the drag state.
47
+ */
48
+ reset() {
49
+ delete this.dragOffset;
50
+ this.row.removeAttribute("style");
51
+ this.row.removeAttribute("dragging");
52
+ }
53
+
54
+ /**
55
+ * @returns {boolean} true when the item has a change to its index value
56
+ */
57
+ get hasChanges() {
58
+ return this.paramsValue.index_value !== this.index;
59
+ }
60
+
61
+ /**
62
+ * Calculate the relative index of the item during drag. This is used to
63
+ * sort items during drag as it takes into account any uncommitted changes
64
+ * to index caused by the drag offset.
65
+ *
66
+ * @returns {number} index for the purposes of drag and drop ordering
67
+ */
68
+ get dragIndex() {
69
+ if (this.dragOffset && this.dragOffset !== 0) {
70
+ return this.index + Math.round(this.dragOffset / this.row.offsetHeight);
71
+ } else {
72
+ return this.index;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Index value for use in comparisons during drag. This is used to determine
78
+ * whether the dragged item is above or below another item. If this item is
79
+ * being dragged then we offset the index by 0.5 to ensure that it jumps up
80
+ * or down when it reaches the midpoint of the item above or below it.
81
+ *
82
+ * @returns {number}
83
+ */
84
+ get comparisonIndex() {
85
+ if (this.dragOffset) {
86
+ return this.dragIndex + (this.dragOffset > 0 ? 0.5 : -0.5);
87
+ } else {
88
+ return this.index;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The containing row element.
94
+ *
95
+ * @returns {HTMLElement}
96
+ */
97
+ get row() {
98
+ return this.element.parentElement;
99
+ }
8
100
  }
@@ -3,71 +3,245 @@ import { Controller } from "@hotwired/stimulus";
3
3
  export default class OrderableListController extends Controller {
4
4
  static outlets = ["tables--orderable--item", "tables--orderable--form"];
5
5
 
6
- dragstart(event) {
7
- if (this.element !== event.target.parentElement) return;
6
+ //region State transitions
8
7
 
9
- const target = event.target;
10
- event.dataTransfer.effectAllowed = "move";
8
+ startDragging(dragState) {
9
+ this.dragState = dragState;
11
10
 
12
- // update element style after drag has begun
13
- setTimeout(() => (target.dataset.dragging = ""));
11
+ document.addEventListener("mousemove", this.mousemove);
12
+ document.addEventListener("mouseup", this.mouseup);
13
+
14
+ this.element.style.position = "relative";
14
15
  }
15
16
 
16
- dragover(event) {
17
- if (!this.dragItem) return;
17
+ stopDragging() {
18
+ const dragState = this.dragState;
19
+ delete this.dragState;
18
20
 
19
- swap(this.dropTarget(event.target), this.dragItem);
21
+ document.removeEventListener("mousemove", this.mousemove);
22
+ document.removeEventListener("mouseup", this.mouseup);
20
23
 
21
- event.preventDefault();
22
- return true;
23
- }
24
+ this.element.removeAttribute("style");
25
+ this.tablesOrderableItemOutlets.forEach((item) => item.reset());
24
26
 
25
- dragenter(event) {
26
- event.preventDefault();
27
+ return dragState;
27
28
  }
28
29
 
29
- drop(event) {
30
- if (!this.dragItem) return;
30
+ drop() {
31
+ // note: early returns guard against turbo updates that prevent us finding
32
+ // the right item to drop on. In this situation it's better to discard the
33
+ // drop than to drop in the wrong place.
34
+
35
+ const dragItem = this.dragItem;
36
+
37
+ if (!dragItem) return;
38
+
39
+ const newIndex = dragItem.dragIndex;
40
+ const targetItem = this.tablesOrderableItemOutlets[newIndex];
41
+
42
+ if (!targetItem) return;
31
43
 
32
- event.preventDefault();
33
- delete this.dragItem.dataset.dragging;
44
+ // swap the dragged item into the correct position for its current offset
45
+ if (newIndex < dragItem.index) {
46
+ targetItem.row.insertAdjacentElement("beforebegin", dragItem.row);
47
+ } else if (newIndex > dragItem.index) {
48
+ targetItem.row.insertAdjacentElement("afterend", dragItem.row);
49
+ }
50
+
51
+ // reindex all items based on their new positions
52
+ this.tablesOrderableItemOutlets.forEach((item, index) =>
53
+ item.updateIndex(index),
54
+ );
34
55
 
35
- this.update();
56
+ // save the changes
57
+ this.commitChanges();
36
58
  }
37
59
 
38
- update() {
60
+ commitChanges() {
39
61
  // clear any existing inputs to prevent duplicates
40
62
  this.tablesOrderableFormOutlet.clear();
41
63
 
42
64
  // insert any items that have changed position
43
- this.tablesOrderableItemOutlets.forEach((item, index) => {
44
- if (item.valueValue !== index) {
45
- this.tablesOrderableFormOutlet.add(item.nameValue, index);
46
- }
65
+ this.tablesOrderableItemOutlets.forEach((item) => {
66
+ if (item.hasChanges) this.tablesOrderableFormOutlet.add(item);
47
67
  });
48
68
 
49
69
  this.tablesOrderableFormOutlet.submit();
50
70
  }
51
71
 
52
- get dragItem() {
53
- return this.element.querySelector("[data-dragging]");
72
+ //endregion
73
+
74
+ //region Events
75
+
76
+ mousedown(event) {
77
+ if (this.isDragging) return;
78
+
79
+ const target = this.#targetItem(event.target);
80
+
81
+ if (!target) return;
82
+
83
+ event.preventDefault(); // prevent built-in drag
84
+
85
+ this.startDragging(new DragState(this.element, event, target.id));
86
+
87
+ this.#updateDragItem(event);
54
88
  }
55
89
 
56
- dropTarget($e) {
57
- while ($e && $e.parentElement !== this.element) {
58
- $e = $e.parentElement;
90
+ mousemove = (event) => {
91
+ if (!this.isDragging) return;
92
+
93
+ event.preventDefault(); // prevent build-in drag
94
+
95
+ const dragItem = this.#updateDragItem(event);
96
+
97
+ // Visually updates the position of all items in the list relative to the
98
+ // dragged item. No actual changes to orderings at this stage.
99
+ this.#currentItems.forEach((item, index) => {
100
+ if (item === dragItem) return;
101
+ item.updateVisually(index);
102
+ });
103
+ };
104
+
105
+ mouseup = (event) => {
106
+ if (!this.isDragging) return;
107
+
108
+ this.drop();
109
+ this.stopDragging();
110
+ this.tablesOrderableFormOutlets.forEach((form) => delete form.dragState);
111
+ };
112
+
113
+ tablesOrderableFormOutletConnected(form, element) {
114
+ if (form.dragState) {
115
+ // restore the previous controller's state
116
+ this.startDragging(form.dragState);
59
117
  }
60
- return $e;
61
118
  }
119
+
120
+ tablesOrderableFormOutletDisconnected(form, element) {
121
+ if (this.isDragging) {
122
+ // cache drag state in the form
123
+ form.dragState = this.stopDragging();
124
+ }
125
+ }
126
+
127
+ //endregion
128
+
129
+ //region Helpers
130
+
131
+ get isDragging() {
132
+ return !!this.dragState;
133
+ }
134
+
135
+ get dragItem() {
136
+ if (!this.isDragging) return null;
137
+
138
+ return this.tablesOrderableItemOutlets.find(
139
+ (item) => item.id === this.dragState.targetId,
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Returns the current items in the list, sorted by their current index.
145
+ * Current uses the drag index if the item is being dragged, if set.
146
+ *
147
+ * @returns {Array[OrderableRowController]}
148
+ */
149
+ get #currentItems() {
150
+ return this.tablesOrderableItemOutlets.toSorted(
151
+ (a, b) => a.comparisonIndex - b.comparisonIndex,
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Returns the item outlet that was clicked on, if any.
157
+ *
158
+ * @param event {HTMLElement} the clicked ordinal cell
159
+ * @returns {OrderableRowController}
160
+ */
161
+ #targetItem(element) {
162
+ return this.tablesOrderableItemOutlets.find(
163
+ (item) => item.element === element,
164
+ );
165
+ }
166
+
167
+ #updateDragItem(event) {
168
+ const offset = this.dragState.itemOffset(
169
+ this.element,
170
+ this.dragItem.row,
171
+ event,
172
+ );
173
+ const item = this.dragItem;
174
+ item.dragUpdate(offset);
175
+ return item;
176
+ }
177
+
178
+ //endregion
62
179
  }
63
180
 
64
- function swap(target, item) {
65
- if (target && target !== item) {
66
- const positionComparison = target.compareDocumentPosition(item);
67
- if (positionComparison & Node.DOCUMENT_POSITION_FOLLOWING) {
68
- target.insertAdjacentElement("beforebegin", item);
69
- } else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {
70
- target.insertAdjacentElement("afterend", item);
181
+ /**
182
+ * During drag we want to be able to translate a document-relative coordinate
183
+ * into a coordinate relative to the list element. This state object calculates
184
+ * and stores internal state so that we can translate absolute page coordinates
185
+ * from mouse events into relative offsets for the list items within the list
186
+ * element.
187
+ *
188
+ * We also keep track of the drag target so that if the controller is attached
189
+ * to a new element during the drag we can continue after the turbo update.
190
+ */
191
+ class DragState {
192
+ /**
193
+ * @param list {HTMLElement} the list controller's element (tbody)
194
+ * @param event {MouseEvent} the initial event
195
+ * @param id {String} the id of the element being dragged
196
+ */
197
+ constructor(list, event, id) {
198
+ // calculate the offset top of the tbody element relative to offsetParent
199
+ const offsetParent = list.offsetParent;
200
+ let offsetTop = event.offsetY;
201
+ let current = event.target;
202
+ while (current && current !== offsetParent) {
203
+ offsetTop += current.offsetTop;
204
+ current = current.offsetParent;
71
205
  }
206
+
207
+ // page offset is the offset of the tbody element relative to the page
208
+ this.pageOffset = event.pageY - offsetTop + list.offsetTop;
209
+
210
+ // cursor offset is the offset of the cursor relative to the drag item
211
+ this.cursorOffset = event.offsetY;
212
+
213
+ // initial offset is the offset position of the drag item at drag start
214
+ this.initialOffset = event.target.offsetTop - list.offsetTop;
215
+
216
+ // id of the item being dragged
217
+ this.targetId = id;
218
+ }
219
+
220
+ /**
221
+ * Calculates the offset of the drag item relative to its initial position.
222
+ *
223
+ * @param list {HTMLElement} the list controller's element (tbody)
224
+ * @param row {HTMLElement} the row being dragged
225
+ * @param event {MouseEvent} the current event
226
+ * @returns {number} relative offset for the item being dragged
227
+ */
228
+ itemOffset(list, row, event) {
229
+ // cursor position relative to the list
230
+ const cursorPosition = event.pageY - this.pageOffset;
231
+
232
+ // item position relative to the list
233
+ let itemPosition = cursorPosition - this.cursorOffset;
234
+
235
+ // ensure itemPosition is within the bounds of the list (tbody)
236
+ itemPosition = Math.max(itemPosition, 0);
237
+ itemPosition = Math.min(itemPosition, list.offsetHeight - row.offsetHeight);
238
+
239
+ // Item has position: relative, so we want to calculate the amount to move
240
+ // the item relative to it's DOM position to represent how much it has been
241
+ // dragged by.
242
+
243
+ // Convert itemPosition from offset relative to list to offset relative to
244
+ // its position within the DOM (if it hadn't moved).
245
+ return itemPosition - this.initialOffset;
72
246
  }
73
247
  }
@@ -26,7 +26,7 @@ module Katalyst
26
26
  def row_partial(row, record = nil)
27
27
  partial = @partial || model_name&.param_key&.to_s
28
28
  as = @as || model_name&.param_key&.to_sym
29
- render(partial: partial, variants: [:row], locals: { as => record, row: row })
29
+ render(partial: partial, variants: [:row], formats: [:html], locals: { as => record, row: row })
30
30
  end
31
31
  end
32
32
  end
@@ -13,32 +13,30 @@ module Katalyst
13
13
 
14
14
  using HasHtmlAttributes
15
15
 
16
- # Enhance a given table component class with orderable support.
17
- # Supports extension via `included` and `extended` hooks.
18
- def self.make_orderable(table_class)
16
+ # Support for inclusion in a table component class
17
+ # Adds an `orderable` slot and component configuration
18
+ included do
19
19
  # Add `orderable` slot to table component
20
- table_class.config_component :orderable, default: "FormComponent"
21
- table_class.renders_one(:orderable, lambda do |**attrs|
20
+ config_component :orderable, default: "Katalyst::Tables::Orderable::FormComponent"
21
+ renders_one(:orderable, lambda do |**attrs|
22
22
  orderable_component.new(table: self, **attrs)
23
23
  end)
24
24
  end
25
25
 
26
- # Support for inclusion in a table component class
27
- included do
28
- Orderable.make_orderable(self)
29
- end
30
-
31
26
  # Support for extending a table component instance
27
+ # Adds methods to the table component instance
32
28
  def self.extended(table)
33
- Orderable.make_orderable(table.class)
29
+ table.extend(TableMethods)
30
+
31
+ # ensure row components support orderable column calls
32
+ table.send(:add_orderable_columns)
34
33
  end
35
34
 
36
35
  def initialize(**attributes)
37
36
  super
38
37
 
39
- # Add `orderable` columns to row components
40
- header_row_component.include(HeaderRow)
41
- body_row_component.include(BodyRow)
38
+ # ensure row components support orderable column calls
39
+ add_orderable_columns
42
40
  end
43
41
 
44
42
  def tbody_attributes
@@ -47,16 +45,38 @@ module Katalyst
47
45
  super.merge_html(
48
46
  { data: { controller: LIST_CONTROLLER,
49
47
  action: <<~ACTIONS.squish,
50
- dragstart->#{LIST_CONTROLLER}#dragstart
51
- dragenter->#{LIST_CONTROLLER}#dragenter
52
- dragover->#{LIST_CONTROLLER}#dragover
53
- drop->#{LIST_CONTROLLER}#drop
48
+ mousedown->#{LIST_CONTROLLER}#mousedown
54
49
  ACTIONS
55
50
  "#{LIST_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{orderable.id}",
56
51
  "#{LIST_CONTROLLER}-#{ITEM_CONTROLLER}-outlet" => "td.ordinal" } },
57
52
  )
58
53
  end
59
54
 
55
+ private
56
+
57
+ # Add `orderable` columns to row components
58
+ def add_orderable_columns
59
+ header_row_component.include(HeaderRow)
60
+ body_row_component.include(BodyRow)
61
+ end
62
+
63
+ # Methods required to emulate a slot when extending an existing table.
64
+ module TableMethods
65
+ def with_orderable(**attrs)
66
+ @orderable = FormComponent.new(table: self, **attrs)
67
+
68
+ self
69
+ end
70
+
71
+ def orderable?
72
+ @orderable.present?
73
+ end
74
+
75
+ def orderable
76
+ @orderable
77
+ end
78
+ end
79
+
60
80
  module HeaderRow # :nodoc:
61
81
  def ordinal(attribute = :ordinal, **)
62
82
  cell(attribute, class: "ordinal", label: "")
@@ -64,23 +84,19 @@ module Katalyst
64
84
  end
65
85
 
66
86
  module BodyRow # :nodoc:
67
- def ordinal(attribute = :ordinal, id: :id)
68
- name = @table.orderable.record_scope(@record, id, attribute)
69
- value = @record.public_send(attribute)
70
- cell(attribute, class: "ordinal", data: {
71
- controller: ITEM_CONTROLLER,
72
- "#{ITEM_CONTROLLER}-name-value" => name,
73
- "#{ITEM_CONTROLLER}-value-value" => value,
87
+ def ordinal(attribute = :ordinal, primary_key: :id)
88
+ id = @record.public_send(primary_key)
89
+ params = {
90
+ id_name: @table.orderable.record_scope(id, primary_key),
91
+ id_value: id,
92
+ index_name: @table.orderable.record_scope(id, attribute),
93
+ index_value: @record.public_send(attribute),
94
+ }
95
+ cell(attribute, class: "ordinal", draggable: true, data: {
96
+ controller: ITEM_CONTROLLER,
97
+ "#{ITEM_CONTROLLER}-params-value": params.to_json,
74
98
  }) { t("katalyst.tables.orderable.value") }
75
99
  end
76
-
77
- def html_attributes
78
- super.merge_html(
79
- {
80
- draggable: "true",
81
- },
82
- )
83
- end
84
100
  end
85
101
 
86
102
  class FormComponent < ViewComponent::Base # :nodoc:
@@ -98,8 +114,8 @@ module Katalyst
98
114
  @scope = scope
99
115
  end
100
116
 
101
- def record_scope(record, id, attribute)
102
- "#{scope}[#{record.public_send(id)}][#{attribute}]"
117
+ def record_scope(id, attribute)
118
+ "#{scope}[#{id}][#{attribute}]"
103
119
  end
104
120
 
105
121
  def call
@@ -13,10 +13,16 @@ module Katalyst
13
13
 
14
14
  include ::Turbo::StreamsHelper
15
15
 
16
+ # Is turbo rendering enabled for this component?
16
17
  def turbo?
17
18
  @turbo
18
19
  end
19
20
 
21
+ # Are we rendering a turbo stream response?
22
+ def turbo_stream_response?
23
+ response.media_type.eql?("text/vnd.turbo-stream.html")
24
+ end
25
+
20
26
  def initialize(turbo: true, **options)
21
27
  super(**options)
22
28
 
@@ -44,16 +50,27 @@ module Katalyst
44
50
  super
45
51
 
46
52
  redefinition_lock.synchronize do
47
- component_class.alias_method(:vc_render_template_for, :render_template_for)
48
- component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
49
- def render_template_for(variant = nil)
50
- if turbo? && response.media_type.eql?("text/vnd.turbo-stream.html")
51
- turbo_stream.replace(id, vc_render_template_for(variant))
53
+ # Capture the instance method added by the default compiler and
54
+ # wrap it in a turbo stream replacement. Take care to ensure that
55
+ # subclasses of this component don't break delegation, as each
56
+ # subclass of ViewComponent::Base defines its own version of this
57
+ # method.
58
+ vc_render_template = component_class.instance_method(:render_template_for)
59
+ component_class.define_method(:render_template_for) do |variant = nil|
60
+ # VC discards the output from this method and uses the buffer
61
+ # if both are set. Capture and wrap the output.
62
+ content = capture { vc_render_template.bind_call(self, variant) }
63
+ # In turbo mode, replace the inner-most element using a turbo
64
+ # stream. Note that we only want one turbo stream per component
65
+ # from this mechanism, as subclasses may want to concat their
66
+ # own additional streams.
67
+ if turbo? && turbo_stream_response? && !@streamed
68
+ @streamed = true
69
+ concat(turbo_stream.replace(id, content))
52
70
  else
53
- vc_render_template_for(variant)
71
+ concat(content)
54
72
  end
55
73
  end
56
- RUBY
57
74
  end
58
75
  end
59
76
  end
@@ -7,6 +7,8 @@ module Katalyst
7
7
  class TableComponent < ::Katalyst::TableComponent
8
8
  include Tables::TurboReplaceable
9
9
 
10
+ attr_reader :id
11
+
10
12
  def initialize(collection:, id:, header: true, **options)
11
13
  header = if header.is_a?(Hash)
12
14
  default_header_options.merge(header)
@@ -14,11 +16,9 @@ module Katalyst
14
16
  default_header_options
15
17
  end
16
18
 
17
- super(collection: collection, header: header, id: id, **options)
18
- end
19
+ @id = id
19
20
 
20
- def id
21
- html_attributes[:id]
21
+ super(collection: collection, header: header, id: id, **options)
22
22
  end
23
23
 
24
24
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Katalyst
4
4
  module Tables
5
- VERSION = "2.2.1"
5
+ VERSION = "2.2.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-tables
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-20 00:00:00.000000000 Z
11
+ date: 2023-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: html-attributes-utils