katalyst-tables 2.2.2 → 2.2.3
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 +91 -2
- data/app/assets/javascripts/controllers/tables/orderable/list_controller.js +211 -37
- 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: 8224ebb910f428c3f7750ae6e79c92f21440f5546a2baaede473bb286f217a82
|
4
|
+
data.tar.gz: 6be02c8a263dda22b7faae734cb20e8cec5879db532adb50f257bd502fa51265
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61052d361a93ab2675274f9ed3b3f59e0acd08519261cf016c4db55006097d72f78114ba489e5eed9d066a82aa6bbaa9dd00675d378e24c996a5ccba775bce75
|
7
|
+
data.tar.gz: e411c014472cd5d34338ad0a02d869d69352fd4c6abe70c68ead05d75b3d2f0f52bf11c263031cd6606d27d09c7565e76770eee0b9149b58ef5e91ec383d86bf
|
@@ -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,96 @@ export default class OrderableRowController extends Controller {
|
|
5
5
|
params: Object,
|
6
6
|
};
|
7
7
|
|
8
|
-
|
9
|
-
|
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;
|
10
99
|
}
|
11
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
|
-
|
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
|
+
|
14
|
+
this.element.style.position = "relative";
|
14
15
|
}
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
stopDragging() {
|
18
|
+
const dragState = this.dragState;
|
19
|
+
delete this.dragState;
|
18
20
|
|
19
|
-
|
21
|
+
document.removeEventListener("mousemove", this.mousemove);
|
22
|
+
document.removeEventListener("mouseup", this.mouseup);
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
}
|
24
|
+
this.element.removeAttribute("style");
|
25
|
+
this.tablesOrderableItemOutlets.forEach((item) => item.reset());
|
24
26
|
|
25
|
-
|
26
|
-
event.preventDefault();
|
27
|
+
return dragState;
|
27
28
|
}
|
28
29
|
|
29
|
-
drop(
|
30
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
56
|
+
// save the changes
|
57
|
+
this.commitChanges();
|
36
58
|
}
|
37
59
|
|
38
|
-
|
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
|
44
|
-
if (item.
|
45
|
-
this.tablesOrderableFormOutlet.add(item, 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
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
}
|
@@ -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.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-
|
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
|