katalyst-tables 2.2.2 → 2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/controllers/tables/orderable/form_controller.js +2 -2
- data/app/assets/javascripts/controllers/tables/orderable/item_controller.js +101 -2
- data/app/assets/javascripts/controllers/tables/orderable/list_controller.js +269 -38
- data/app/components/concerns/katalyst/tables/orderable.rb +2 -13
- data/lib/katalyst/tables/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 450377488700ebc511da5b3d515c217811ef60ce631df86edf62778ef8a3c5c9
|
4
|
+
data.tar.gz: 90040f0e73ad2858fd6b683b8046c9726d6a9223676edafc555dadf3e416e97b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65cb752a4d5ad8620572f123f960b8378e31b5775b34b84bcc7787cd5d89234a5fee3ef2e5d850d19a4c422278aaed74eab8fddf254a92445c11d1e9f17296af
|
7
|
+
data.tar.gz: c72888fbed46322cc3adf8bb4a562079699ccacea0fac36a8b85879ddf4378c252f7090c35206b4ac0e7696b2d67e7baadd455c279be9e813efc81103efe7724
|
@@ -1,12 +1,12 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
2
2
|
|
3
3
|
export default class OrderableFormController extends Controller {
|
4
|
-
add(item
|
4
|
+
add(item) {
|
5
5
|
const { id_name, id_value, index_name } = item.paramsValue;
|
6
6
|
this.element.insertAdjacentHTML(
|
7
7
|
"beforeend",
|
8
8
|
`<input type="hidden" name="${id_name}" value="${id_value}" data-generated>
|
9
|
-
<input type="hidden" name="${index_name}" value="${index}" data-generated>`,
|
9
|
+
<input type="hidden" name="${index_name}" value="${item.index}" data-generated>`,
|
10
10
|
);
|
11
11
|
}
|
12
12
|
|
@@ -5,7 +5,106 @@ export default class OrderableRowController extends Controller {
|
|
5
5
|
params: Object,
|
6
6
|
};
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
connect() {
|
9
|
+
// index from server may be inconsistent with the visual ordering,
|
10
|
+
// especially if this is a new node. Use positional indexes instead,
|
11
|
+
// as these are the values we will send on save.
|
12
|
+
this.index = domIndex(this.row);
|
10
13
|
}
|
14
|
+
|
15
|
+
paramsValueChanged(params) {
|
16
|
+
this.id = params.id_value;
|
17
|
+
}
|
18
|
+
|
19
|
+
dragUpdate(offset) {
|
20
|
+
this.dragOffset = offset;
|
21
|
+
this.row.style.position = "relative";
|
22
|
+
this.row.style.top = offset + "px";
|
23
|
+
this.row.style.zIndex = "1";
|
24
|
+
this.row.toggleAttribute("dragging", true);
|
25
|
+
}
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Called on items that are not the dragged item during drag. Updates the
|
29
|
+
* visual position of the item relative to the dragged item.
|
30
|
+
*
|
31
|
+
* @param index {number} intended index of the item during drag
|
32
|
+
*/
|
33
|
+
updateVisually(index) {
|
34
|
+
this.row.style.position = "relative";
|
35
|
+
this.row.style.top = `${
|
36
|
+
this.row.offsetHeight * (index - this.dragIndex)
|
37
|
+
}px`;
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Set the index value of the item. This is called on all items after a drop
|
42
|
+
* event. If the index is different to the params index then this item has
|
43
|
+
* changed.
|
44
|
+
*
|
45
|
+
* @param index {number} the new index value
|
46
|
+
*/
|
47
|
+
updateIndex(index) {
|
48
|
+
this.index = index;
|
49
|
+
}
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Restore any visual changes made during drag and remove the drag state.
|
53
|
+
*/
|
54
|
+
reset() {
|
55
|
+
delete this.dragOffset;
|
56
|
+
this.row.removeAttribute("style");
|
57
|
+
this.row.removeAttribute("dragging");
|
58
|
+
}
|
59
|
+
|
60
|
+
/**
|
61
|
+
* @returns {boolean} true when the item has a change to its index value
|
62
|
+
*/
|
63
|
+
get hasChanges() {
|
64
|
+
return this.paramsValue.index_value !== this.index;
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Calculate the relative index of the item during drag. This is used to
|
69
|
+
* sort items during drag as it takes into account any uncommitted changes
|
70
|
+
* to index caused by the drag offset.
|
71
|
+
*
|
72
|
+
* @returns {number} index for the purposes of drag and drop ordering
|
73
|
+
*/
|
74
|
+
get dragIndex() {
|
75
|
+
if (this.dragOffset && this.dragOffset !== 0) {
|
76
|
+
return this.index + Math.round(this.dragOffset / this.row.offsetHeight);
|
77
|
+
} else {
|
78
|
+
return this.index;
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Index value for use in comparisons during drag. This is used to determine
|
84
|
+
* whether the dragged item is above or below another item. If this item is
|
85
|
+
* being dragged then we offset the index by 0.5 to ensure that it jumps up
|
86
|
+
* or down when it reaches the midpoint of the item above or below it.
|
87
|
+
*
|
88
|
+
* @returns {number}
|
89
|
+
*/
|
90
|
+
get comparisonIndex() {
|
91
|
+
if (this.dragOffset) {
|
92
|
+
return this.dragIndex + (this.dragOffset > 0 ? 0.5 : -0.5);
|
93
|
+
} else {
|
94
|
+
return this.index;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* The containing row element.
|
100
|
+
*
|
101
|
+
* @returns {HTMLElement}
|
102
|
+
*/
|
103
|
+
get row() {
|
104
|
+
return this.element.parentElement;
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
function domIndex(element) {
|
109
|
+
return Array.from(element.parentElement.children).indexOf(element);
|
11
110
|
}
|
@@ -3,71 +3,302 @@ 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
|
-
|
7
|
-
if (this.element !== event.target.parentElement) return;
|
6
|
+
//region State transitions
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
startDragging(dragState) {
|
9
|
+
this.dragState = dragState;
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
document.addEventListener("mousemove", this.mousemove);
|
12
|
+
document.addEventListener("mouseup", this.mouseup);
|
13
|
+
window.addEventListener("scroll", this.scroll, true);
|
14
|
+
|
15
|
+
this.element.style.position = "relative";
|
14
16
|
}
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
+
stopDragging() {
|
19
|
+
const dragState = this.dragState;
|
20
|
+
delete this.dragState;
|
18
21
|
|
19
|
-
|
22
|
+
document.removeEventListener("mousemove", this.mousemove);
|
23
|
+
document.removeEventListener("mouseup", this.mouseup);
|
24
|
+
window.removeEventListener("scroll", this.scroll, true);
|
20
25
|
|
21
|
-
|
22
|
-
|
23
|
-
}
|
26
|
+
this.element.removeAttribute("style");
|
27
|
+
this.tablesOrderableItemOutlets.forEach((item) => item.reset());
|
24
28
|
|
25
|
-
|
26
|
-
event.preventDefault();
|
29
|
+
return dragState;
|
27
30
|
}
|
28
31
|
|
29
|
-
drop(
|
30
|
-
|
32
|
+
drop() {
|
33
|
+
// note: early returns guard against turbo updates that prevent us finding
|
34
|
+
// the right item to drop on. In this situation it's better to discard the
|
35
|
+
// drop than to drop in the wrong place.
|
36
|
+
|
37
|
+
const dragItem = this.dragItem;
|
38
|
+
|
39
|
+
if (!dragItem) return;
|
40
|
+
|
41
|
+
const newIndex = dragItem.dragIndex;
|
42
|
+
const targetItem = this.tablesOrderableItemOutlets[newIndex];
|
31
43
|
|
32
|
-
|
33
|
-
delete this.dragItem.dataset.dragging;
|
44
|
+
if (!targetItem) return;
|
34
45
|
|
35
|
-
|
46
|
+
// swap the dragged item into the correct position for its current offset
|
47
|
+
if (newIndex < dragItem.index) {
|
48
|
+
targetItem.row.insertAdjacentElement("beforebegin", dragItem.row);
|
49
|
+
} else if (newIndex > dragItem.index) {
|
50
|
+
targetItem.row.insertAdjacentElement("afterend", dragItem.row);
|
51
|
+
}
|
52
|
+
|
53
|
+
// reindex all items based on their new positions
|
54
|
+
this.tablesOrderableItemOutlets.forEach((item, index) =>
|
55
|
+
item.updateIndex(index),
|
56
|
+
);
|
57
|
+
|
58
|
+
// save the changes
|
59
|
+
this.commitChanges();
|
36
60
|
}
|
37
61
|
|
38
|
-
|
62
|
+
commitChanges() {
|
39
63
|
// clear any existing inputs to prevent duplicates
|
40
64
|
this.tablesOrderableFormOutlet.clear();
|
41
65
|
|
42
66
|
// insert any items that have changed position
|
43
|
-
this.tablesOrderableItemOutlets.forEach((item
|
44
|
-
if (item.
|
45
|
-
this.tablesOrderableFormOutlet.add(item, index);
|
46
|
-
}
|
67
|
+
this.tablesOrderableItemOutlets.forEach((item) => {
|
68
|
+
if (item.hasChanges) this.tablesOrderableFormOutlet.add(item);
|
47
69
|
});
|
48
70
|
|
49
71
|
this.tablesOrderableFormOutlet.submit();
|
50
72
|
}
|
51
73
|
|
52
|
-
|
53
|
-
|
74
|
+
//endregion
|
75
|
+
|
76
|
+
//region Events
|
77
|
+
|
78
|
+
mousedown(event) {
|
79
|
+
if (this.isDragging) return;
|
80
|
+
|
81
|
+
const target = this.#targetItem(event.target);
|
82
|
+
|
83
|
+
if (!target) return;
|
84
|
+
|
85
|
+
event.preventDefault(); // prevent built-in drag
|
86
|
+
|
87
|
+
this.startDragging(new DragState(this.element, event, target.id));
|
88
|
+
|
89
|
+
this.dragState.updateCursor(this.element, target.row, event, this.animate);
|
54
90
|
}
|
55
91
|
|
56
|
-
|
57
|
-
|
58
|
-
|
92
|
+
mousemove = (event) => {
|
93
|
+
if (!this.isDragging) return;
|
94
|
+
|
95
|
+
event.preventDefault(); // prevent build-in drag
|
96
|
+
|
97
|
+
if (this.ticking) return;
|
98
|
+
|
99
|
+
this.ticking = true;
|
100
|
+
|
101
|
+
window.requestAnimationFrame(() => {
|
102
|
+
this.ticking = false;
|
103
|
+
this.dragState.updateCursor(
|
104
|
+
this.element,
|
105
|
+
this.dragItem.row,
|
106
|
+
event,
|
107
|
+
this.animate,
|
108
|
+
);
|
109
|
+
});
|
110
|
+
};
|
111
|
+
|
112
|
+
scroll = (event) => {
|
113
|
+
if (!this.isDragging || this.ticking) return;
|
114
|
+
|
115
|
+
this.ticking = true;
|
116
|
+
|
117
|
+
window.requestAnimationFrame(() => {
|
118
|
+
this.ticking = false;
|
119
|
+
this.dragState.updateScroll(
|
120
|
+
this.element,
|
121
|
+
this.dragItem.row,
|
122
|
+
this.animate,
|
123
|
+
);
|
124
|
+
});
|
125
|
+
};
|
126
|
+
|
127
|
+
mouseup = (event) => {
|
128
|
+
if (!this.isDragging) return;
|
129
|
+
|
130
|
+
this.drop();
|
131
|
+
this.stopDragging();
|
132
|
+
this.tablesOrderableFormOutlets.forEach((form) => delete form.dragState);
|
133
|
+
};
|
134
|
+
|
135
|
+
tablesOrderableFormOutletConnected(form, element) {
|
136
|
+
if (form.dragState) {
|
137
|
+
// restore the previous controller's state
|
138
|
+
this.startDragging(form.dragState);
|
59
139
|
}
|
60
|
-
return $e;
|
61
140
|
}
|
62
|
-
}
|
63
141
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
target.insertAdjacentElement("beforebegin", item);
|
69
|
-
} else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {
|
70
|
-
target.insertAdjacentElement("afterend", item);
|
142
|
+
tablesOrderableFormOutletDisconnected(form, element) {
|
143
|
+
if (this.isDragging) {
|
144
|
+
// cache drag state in the form
|
145
|
+
form.dragState = this.stopDragging();
|
71
146
|
}
|
72
147
|
}
|
148
|
+
|
149
|
+
//endregion
|
150
|
+
|
151
|
+
//region Helpers
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Updates the position of the drag item with a relative offset. Updates
|
155
|
+
* other items relative to the new position of the drag item, as required.
|
156
|
+
*
|
157
|
+
* @callback {OrderableListController~animate}
|
158
|
+
* @param {number} offset
|
159
|
+
*/
|
160
|
+
animate = (offset) => {
|
161
|
+
const dragItem = this.dragItem;
|
162
|
+
|
163
|
+
// Visually update the dragItem so it follows the cursor
|
164
|
+
dragItem.dragUpdate(offset);
|
165
|
+
|
166
|
+
// Visually updates the position of all items in the list relative to the
|
167
|
+
// dragged item. No actual changes to orderings at this stage.
|
168
|
+
this.#currentItems.forEach((item, index) => {
|
169
|
+
if (item === dragItem) return;
|
170
|
+
item.updateVisually(index);
|
171
|
+
});
|
172
|
+
};
|
173
|
+
|
174
|
+
get isDragging() {
|
175
|
+
return !!this.dragState;
|
176
|
+
}
|
177
|
+
|
178
|
+
get dragItem() {
|
179
|
+
if (!this.isDragging) return null;
|
180
|
+
|
181
|
+
return this.tablesOrderableItemOutlets.find(
|
182
|
+
(item) => item.id === this.dragState.targetId,
|
183
|
+
);
|
184
|
+
}
|
185
|
+
|
186
|
+
/**
|
187
|
+
* Returns the current items in the list, sorted by their current index.
|
188
|
+
* Current uses the drag index if the item is being dragged, if set.
|
189
|
+
*
|
190
|
+
* @returns {Array[OrderableRowController]}
|
191
|
+
*/
|
192
|
+
get #currentItems() {
|
193
|
+
return this.tablesOrderableItemOutlets.toSorted(
|
194
|
+
(a, b) => a.comparisonIndex - b.comparisonIndex,
|
195
|
+
);
|
196
|
+
}
|
197
|
+
|
198
|
+
/**
|
199
|
+
* Returns the item outlet that was clicked on, if any.
|
200
|
+
*
|
201
|
+
* @param element {HTMLElement} the clicked ordinal cell
|
202
|
+
* @returns {OrderableRowController}
|
203
|
+
*/
|
204
|
+
#targetItem(element) {
|
205
|
+
return this.tablesOrderableItemOutlets.find(
|
206
|
+
(item) => item.element === element,
|
207
|
+
);
|
208
|
+
}
|
209
|
+
|
210
|
+
//endregion
|
211
|
+
}
|
212
|
+
|
213
|
+
/**
|
214
|
+
* During drag we want to be able to translate a document-relative coordinate
|
215
|
+
* into a coordinate relative to the list element. This state object calculates
|
216
|
+
* and stores internal state so that we can translate absolute page coordinates
|
217
|
+
* from mouse events into relative offsets for the list items within the list
|
218
|
+
* element.
|
219
|
+
*
|
220
|
+
* We also keep track of the drag target so that if the controller is attached
|
221
|
+
* to a new element during the drag we can continue after the turbo update.
|
222
|
+
*/
|
223
|
+
class DragState {
|
224
|
+
/**
|
225
|
+
* @param list {HTMLElement} the list controller's element (tbody)
|
226
|
+
* @param event {MouseEvent} the initial event
|
227
|
+
* @param id {String} the id of the element being dragged
|
228
|
+
*/
|
229
|
+
constructor(list, event, id) {
|
230
|
+
// cursor offset is the offset of the cursor relative to the drag item
|
231
|
+
this.cursorOffset = event.offsetY;
|
232
|
+
|
233
|
+
// initial offset is the offset position of the drag item at drag start
|
234
|
+
this.initialPosition = event.target.offsetTop - list.offsetTop;
|
235
|
+
|
236
|
+
// id of the item being dragged
|
237
|
+
this.targetId = id;
|
238
|
+
}
|
239
|
+
|
240
|
+
/**
|
241
|
+
* Calculates the offset of the drag item relative to its initial position.
|
242
|
+
*
|
243
|
+
* @param list {HTMLElement} the list controller's element (tbody)
|
244
|
+
* @param row {HTMLElement} the row being dragged
|
245
|
+
* @param event {MouseEvent} the current event
|
246
|
+
* @param callback {OrderableListController~animate} updates the drag item with a relative offset
|
247
|
+
*/
|
248
|
+
updateCursor(list, row, event, callback) {
|
249
|
+
// Calculate and store the list offset relative to the viewport
|
250
|
+
// This value is cached so we can calculate the outcome of any scroll events
|
251
|
+
this.listOffset = list.getBoundingClientRect().top;
|
252
|
+
|
253
|
+
// Calculate the position of the cursor relative to the list.
|
254
|
+
// Accounts for scroll offsets by using the item's bounding client rect.
|
255
|
+
const cursorPosition = event.clientY - this.listOffset;
|
256
|
+
|
257
|
+
// intended item position relative to the list, from cursor position
|
258
|
+
let itemPosition = cursorPosition - this.cursorOffset;
|
259
|
+
|
260
|
+
this.#updateItemPosition(list, row, itemPosition, callback);
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Animates the item's position as the list scrolls. Requires a previous call
|
265
|
+
* to set the scroll offset.
|
266
|
+
*
|
267
|
+
* @param list {HTMLElement} the list controller's element (tbody)
|
268
|
+
* @param row {HTMLElement} the row being dragged
|
269
|
+
* @param callback {OrderableListController~animate} updates the drag item with a relative offset
|
270
|
+
*/
|
271
|
+
updateScroll(list, row, callback) {
|
272
|
+
const previousScrollOffset = this.listOffset;
|
273
|
+
|
274
|
+
// Calculate and store the list offset relative to the viewport
|
275
|
+
// This value is cached so we can calculate the outcome of any scroll events
|
276
|
+
this.listOffset = list.getBoundingClientRect().top;
|
277
|
+
|
278
|
+
// Calculate the change in scroll offset since the last update
|
279
|
+
const scrollDelta = previousScrollOffset - this.listOffset;
|
280
|
+
|
281
|
+
// intended item position relative to the list, from cursor position
|
282
|
+
const position = this.position + scrollDelta;
|
283
|
+
|
284
|
+
this.#updateItemPosition(list, row, position, callback);
|
285
|
+
}
|
286
|
+
|
287
|
+
#updateItemPosition(list, row, position, callback) {
|
288
|
+
// ensure itemPosition is within the bounds of the list (tbody)
|
289
|
+
position = Math.max(position, 0);
|
290
|
+
position = Math.min(position, list.offsetHeight - row.offsetHeight);
|
291
|
+
|
292
|
+
// cache the item's position relative to the list for use in scroll events
|
293
|
+
this.position = position;
|
294
|
+
|
295
|
+
// Item has position: relative, so we want to calculate the amount to move
|
296
|
+
// the item relative to it's DOM position to represent how much it has been
|
297
|
+
// dragged by.
|
298
|
+
const offset = position - this.initialPosition;
|
299
|
+
|
300
|
+
// Convert itemPosition from offset relative to list to offset relative to
|
301
|
+
// its position within the DOM (if it hadn't moved).
|
302
|
+
callback(offset);
|
303
|
+
}
|
73
304
|
}
|
@@ -45,10 +45,7 @@ module Katalyst
|
|
45
45
|
super.merge_html(
|
46
46
|
{ data: { controller: LIST_CONTROLLER,
|
47
47
|
action: <<~ACTIONS.squish,
|
48
|
-
|
49
|
-
dragenter->#{LIST_CONTROLLER}#dragenter
|
50
|
-
dragover->#{LIST_CONTROLLER}#dragover
|
51
|
-
drop->#{LIST_CONTROLLER}#drop
|
48
|
+
mousedown->#{LIST_CONTROLLER}#mousedown
|
52
49
|
ACTIONS
|
53
50
|
"#{LIST_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{orderable.id}",
|
54
51
|
"#{LIST_CONTROLLER}-#{ITEM_CONTROLLER}-outlet" => "td.ordinal" } },
|
@@ -95,19 +92,11 @@ module Katalyst
|
|
95
92
|
index_name: @table.orderable.record_scope(id, attribute),
|
96
93
|
index_value: @record.public_send(attribute),
|
97
94
|
}
|
98
|
-
cell(attribute, class: "ordinal", data: {
|
95
|
+
cell(attribute, class: "ordinal", draggable: true, data: {
|
99
96
|
controller: ITEM_CONTROLLER,
|
100
97
|
"#{ITEM_CONTROLLER}-params-value": params.to_json,
|
101
98
|
}) { t("katalyst.tables.orderable.value") }
|
102
99
|
end
|
103
|
-
|
104
|
-
def html_attributes
|
105
|
-
super.merge_html(
|
106
|
-
{
|
107
|
-
draggable: "true",
|
108
|
-
},
|
109
|
-
)
|
110
|
-
end
|
111
100
|
end
|
112
101
|
|
113
102
|
class FormComponent < ViewComponent::Base # :nodoc:
|
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.
|
4
|
+
version: 2.2.4
|
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-
|
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
|