@os-design/drag-sort 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -12
- package/src/DragAndDrop.tsx +235 -0
- package/src/Draggable.tsx +109 -0
- package/src/Droppable.tsx +142 -0
- package/src/index.ts +7 -0
- package/src/utils/ListStore.ts +47 -0
- package/src/utils/NodeList.ts +245 -0
- package/src/utils/getElementOffset.ts +13 -0
- package/src/utils/getElementScroll.ts +13 -0
- package/src/utils/getNodeRect.ts +29 -0
- package/src/utils/useAppendClassName.ts +20 -0
- package/src/utils/useBlankNode.ts +104 -0
- package/src/utils/useDragAndDrop.ts +32 -0
- package/src/utils/useDragEffect.ts +490 -0
- package/src/utils/useDroppable.ts +21 -0
- package/src/utils/useGeneratedId.ts +6 -0
- package/src/utils/useGetNodeStyle.ts +34 -0
- package/src/utils/useInitRect.ts +17 -0
- package/src/utils/useInitScrollOffset.ts +16 -0
- package/src/utils/useMoveNode.ts +97 -0
- package/src/utils/useScrollEventByPoint.ts +56 -0
- package/src/utils/useTargetList.ts +31 -0
- package/src/utils/useTransitionStyle.ts +26 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import React, { CSSProperties, RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any,no-param-reassign,prefer-destructuring,no-constant-condition */
|
|
4
|
+
|
|
5
|
+
// [prev, next, ref, setStyle, index, id]
|
|
6
|
+
export type Node =
|
|
7
|
+
| [Node, Node, RefObject<any>, (style: CSSProperties) => void, number, string]
|
|
8
|
+
| null;
|
|
9
|
+
|
|
10
|
+
export type ExistingNode = Exclude<Node, null>;
|
|
11
|
+
|
|
12
|
+
export interface NodeProps {
|
|
13
|
+
ref: React.MutableRefObject<any>;
|
|
14
|
+
setStyle: (style: CSSProperties) => void;
|
|
15
|
+
index: number;
|
|
16
|
+
id: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RenderDraggedNodeProps {
|
|
20
|
+
/**
|
|
21
|
+
* The index of the dragged node.
|
|
22
|
+
*/
|
|
23
|
+
index: number;
|
|
24
|
+
/**
|
|
25
|
+
* The ID of the dragged node.
|
|
26
|
+
*/
|
|
27
|
+
id: string;
|
|
28
|
+
/**
|
|
29
|
+
* The style of the dragged node with position.
|
|
30
|
+
*/
|
|
31
|
+
style: CSSProperties;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type RenderDraggedNode = (
|
|
35
|
+
props: RenderDraggedNodeProps
|
|
36
|
+
) => React.ReactNode;
|
|
37
|
+
|
|
38
|
+
interface InitProps {
|
|
39
|
+
id: string;
|
|
40
|
+
ref: RefObject<HTMLDivElement>;
|
|
41
|
+
innerRef: RefObject<HTMLDivElement>;
|
|
42
|
+
horizontal: boolean;
|
|
43
|
+
renderDraggedNode: RenderDraggedNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stores all the draggable items in the list.
|
|
48
|
+
* The structure of the doubly linked list is used.
|
|
49
|
+
*/
|
|
50
|
+
class NodeList {
|
|
51
|
+
/**
|
|
52
|
+
* The ID of the list.
|
|
53
|
+
*/
|
|
54
|
+
public id: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The ref to the list.
|
|
58
|
+
*/
|
|
59
|
+
public ref: RefObject<HTMLDivElement>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The inner ref to the list.
|
|
63
|
+
* Used by the virtual list.
|
|
64
|
+
*/
|
|
65
|
+
public innerRef: RefObject<HTMLDivElement>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the list is horizontal.
|
|
69
|
+
*/
|
|
70
|
+
public horizontal: boolean;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The callback that renders the dragged node.
|
|
74
|
+
*/
|
|
75
|
+
public renderDraggedNode: RenderDraggedNode;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The head of draggable nodes.
|
|
79
|
+
*/
|
|
80
|
+
private head: Node;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The tail of draggable nodes.
|
|
84
|
+
*/
|
|
85
|
+
private tail: Node;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Listeners of node addition events.
|
|
89
|
+
*/
|
|
90
|
+
private listeners: Array<(props: NodeProps) => void>;
|
|
91
|
+
|
|
92
|
+
public constructor(props: InitProps) {
|
|
93
|
+
this.id = props.id;
|
|
94
|
+
this.ref = props.ref;
|
|
95
|
+
this.innerRef = props.innerRef;
|
|
96
|
+
this.horizontal = props.horizontal;
|
|
97
|
+
this.renderDraggedNode = props.renderDraggedNode;
|
|
98
|
+
this.head = null;
|
|
99
|
+
this.tail = null;
|
|
100
|
+
this.listeners = [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public getHead() {
|
|
104
|
+
return this.head;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public getTail() {
|
|
108
|
+
return this.tail;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Adds the node to the beginning.
|
|
113
|
+
* TL: O(1).
|
|
114
|
+
*/
|
|
115
|
+
private addToTheBeginning(props: NodeProps): ExistingNode {
|
|
116
|
+
this.head = [
|
|
117
|
+
null,
|
|
118
|
+
this.head,
|
|
119
|
+
props.ref,
|
|
120
|
+
props.setStyle,
|
|
121
|
+
props.index,
|
|
122
|
+
props.id,
|
|
123
|
+
];
|
|
124
|
+
const [, next] = this.head;
|
|
125
|
+
if (next) next[0] = this.head; // Set the prev cursor of the next element
|
|
126
|
+
if (!this.tail) this.tail = this.head;
|
|
127
|
+
return this.head;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Adds the node to the end.
|
|
132
|
+
* TL: O(1).
|
|
133
|
+
*/
|
|
134
|
+
private addToTheEnd(props: NodeProps): ExistingNode {
|
|
135
|
+
this.tail = [
|
|
136
|
+
this.tail,
|
|
137
|
+
null,
|
|
138
|
+
props.ref,
|
|
139
|
+
props.setStyle,
|
|
140
|
+
props.index,
|
|
141
|
+
props.id,
|
|
142
|
+
];
|
|
143
|
+
const [prev] = this.tail;
|
|
144
|
+
if (prev) prev[1] = this.tail; // Set the next cursor of the prev element
|
|
145
|
+
if (!this.head) this.head = this.tail;
|
|
146
|
+
return this.tail;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Adds the node after the specified one.
|
|
151
|
+
* TL: O(1).
|
|
152
|
+
*/
|
|
153
|
+
private static addAfter(node: ExistingNode, props: NodeProps): ExistingNode {
|
|
154
|
+
node[1] = [node, node[1], props.ref, props.setStyle, props.index, props.id];
|
|
155
|
+
if (node[1][1]) node[1][1][0] = node[1];
|
|
156
|
+
return node[1];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Adds a new node depends on its position.
|
|
161
|
+
* Called when a new node is mounted.
|
|
162
|
+
* TL: O(1) – add to the beginning or to the end, O(n) – add to the middle.
|
|
163
|
+
*/
|
|
164
|
+
public add(props: NodeProps): ExistingNode {
|
|
165
|
+
const { index } = props;
|
|
166
|
+
|
|
167
|
+
// Run listeners
|
|
168
|
+
this.listeners.forEach((listener) => listener(props));
|
|
169
|
+
|
|
170
|
+
// Add the first node to the beginning
|
|
171
|
+
if (!this.head || !this.tail) {
|
|
172
|
+
return this.addToTheBeginning(props);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Add the node to the beginning if it is located above the first one
|
|
176
|
+
const [, , , , headIndex] = this.head;
|
|
177
|
+
if (index < headIndex) {
|
|
178
|
+
return this.addToTheBeginning(props);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add the node to the end if it is located below the last one
|
|
182
|
+
const [, , , , tailIndex] = this.tail;
|
|
183
|
+
if (index === tailIndex) {
|
|
184
|
+
// The tail is the blank node
|
|
185
|
+
const [prev] = this.tail;
|
|
186
|
+
if (prev) {
|
|
187
|
+
this.tail[4] += 1;
|
|
188
|
+
return NodeList.addAfter(prev, props);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (index > tailIndex) {
|
|
192
|
+
return this.addToTheEnd(props);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add the node after the one that is located above the current one
|
|
196
|
+
let node = this.head;
|
|
197
|
+
while (true) {
|
|
198
|
+
const [, next] = node;
|
|
199
|
+
if (!next) break;
|
|
200
|
+
const [, , , , nextIndex] = next;
|
|
201
|
+
if (index < nextIndex) break;
|
|
202
|
+
node = next;
|
|
203
|
+
}
|
|
204
|
+
return NodeList.addAfter(node, props);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Removes the existing node.
|
|
209
|
+
* Called when the node is unmounted.
|
|
210
|
+
* TL: O(1).
|
|
211
|
+
*/
|
|
212
|
+
public remove(node: ExistingNode) {
|
|
213
|
+
const [prev, next] = node;
|
|
214
|
+
if (!prev && !next) {
|
|
215
|
+
this.head = null;
|
|
216
|
+
this.tail = null;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!prev && next) {
|
|
220
|
+
next[0] = null;
|
|
221
|
+
this.head = next;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (prev && !next) {
|
|
225
|
+
prev[1] = null;
|
|
226
|
+
this.tail = prev;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (prev && next) {
|
|
230
|
+
prev[1] = next;
|
|
231
|
+
next[0] = prev;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public addListener(callback: (props: NodeProps) => void) {
|
|
236
|
+
this.listeners.push(callback);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public removeListener(callback: (props: NodeProps) => void) {
|
|
240
|
+
const index = this.listeners.findIndex((listener) => listener === callback);
|
|
241
|
+
this.listeners.splice(index, 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default NodeList;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const getElementOffset = (element: HTMLElement) => {
|
|
2
|
+
let offsetLeft = 0;
|
|
3
|
+
let offsetTop = 0;
|
|
4
|
+
let el: HTMLElement | null = element;
|
|
5
|
+
while (el) {
|
|
6
|
+
offsetLeft += el.offsetLeft;
|
|
7
|
+
offsetTop += el.offsetTop;
|
|
8
|
+
el = el.offsetParent as HTMLElement;
|
|
9
|
+
}
|
|
10
|
+
return { offsetLeft, offsetTop };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default getElementOffset;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const getElementScroll = (element: HTMLElement) => {
|
|
2
|
+
let scrollLeft = 0;
|
|
3
|
+
let scrollTop = 0;
|
|
4
|
+
let el: HTMLElement | null = element;
|
|
5
|
+
while (el) {
|
|
6
|
+
scrollLeft += el.scrollLeft;
|
|
7
|
+
scrollTop += el.scrollTop;
|
|
8
|
+
el = el.parentElement;
|
|
9
|
+
}
|
|
10
|
+
return { scrollLeft, scrollTop };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default getElementScroll;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import getElementOffset from './getElementOffset';
|
|
3
|
+
import getElementScroll from './getElementScroll';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Computes the bounds of the existing node without considering transforms.
|
|
7
|
+
*/
|
|
8
|
+
const getNodeRect = (ref: RefObject<HTMLElement>) => {
|
|
9
|
+
if (!ref.current) return null;
|
|
10
|
+
|
|
11
|
+
const parent = ref.current.parentElement;
|
|
12
|
+
const { width, height } = ref.current.getBoundingClientRect();
|
|
13
|
+
const { offsetLeft, offsetTop } = getElementOffset(ref.current);
|
|
14
|
+
const { scrollLeft, scrollTop } = parent
|
|
15
|
+
? getElementScroll(parent)
|
|
16
|
+
: { scrollLeft: 0, scrollTop: 0 };
|
|
17
|
+
|
|
18
|
+
const left = offsetLeft - scrollLeft;
|
|
19
|
+
const top = offsetTop - scrollTop;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
left,
|
|
23
|
+
top,
|
|
24
|
+
right: left + width,
|
|
25
|
+
bottom: top + height,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default getNodeRect;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RefObject, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adds a new class name to the element.
|
|
5
|
+
*/
|
|
6
|
+
const useAppendClassName = (ref: RefObject<HTMLElement>, className: string) => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const element = ref.current;
|
|
9
|
+
if (!element) return () => {};
|
|
10
|
+
|
|
11
|
+
const initClassName = element.className;
|
|
12
|
+
element.className = `${initClassName} ${className}`.trim();
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
element.className = initClassName;
|
|
16
|
+
};
|
|
17
|
+
}, [className, ref]);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default useAppendClassName;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import NodeList, { ExistingNode } from './NodeList';
|
|
3
|
+
|
|
4
|
+
interface InitSize {
|
|
5
|
+
initWidth: number;
|
|
6
|
+
initHeight: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DraggedNode {
|
|
10
|
+
list: NodeList;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface UseBlankNodeProps {
|
|
14
|
+
draggedNode: DraggedNode | null;
|
|
15
|
+
targetList: NodeList | null;
|
|
16
|
+
initDraggedNodeRect: InitSize | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface BlankNode {
|
|
20
|
+
list: NodeList;
|
|
21
|
+
node: ExistingNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Appends the blank node to the list to increase the height of it.
|
|
26
|
+
* Used when the dragged node is located inside another list.
|
|
27
|
+
*/
|
|
28
|
+
const useBlankNode = (props: UseBlankNodeProps) => {
|
|
29
|
+
const { draggedNode, targetList, initDraggedNodeRect } = props;
|
|
30
|
+
const initDraggedNodeRectRef = useRef(initDraggedNodeRect);
|
|
31
|
+
const blankNode = useRef<BlankNode | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
initDraggedNodeRectRef.current = initDraggedNodeRect;
|
|
35
|
+
}, [initDraggedNodeRect]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (
|
|
39
|
+
!draggedNode ||
|
|
40
|
+
!targetList ||
|
|
41
|
+
!targetList.ref.current ||
|
|
42
|
+
draggedNode.list === targetList ||
|
|
43
|
+
!initDraggedNodeRectRef.current
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create a new blank div
|
|
49
|
+
const { initWidth, initHeight } = initDraggedNodeRectRef.current;
|
|
50
|
+
const div = document.createElement('div');
|
|
51
|
+
div.style.width = `${initWidth}px`;
|
|
52
|
+
div.style.height = `${initHeight}px`;
|
|
53
|
+
div.style.minWidth = `${initWidth}px`;
|
|
54
|
+
div.style.minHeight = `${initHeight}px`;
|
|
55
|
+
|
|
56
|
+
let parent = targetList.ref.current;
|
|
57
|
+
const innerList = targetList.innerRef.current;
|
|
58
|
+
|
|
59
|
+
// The inner ref used only in the virtual list
|
|
60
|
+
if (innerList) {
|
|
61
|
+
const { width, height } = innerList.getBoundingClientRect();
|
|
62
|
+
div.style.position = 'absolute';
|
|
63
|
+
div.style.left = targetList.horizontal ? `${width}px` : '0px';
|
|
64
|
+
div.style.top = targetList.horizontal ? '0px' : `${height}px`;
|
|
65
|
+
parent = innerList;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Append the blank div to increase the height of the list
|
|
69
|
+
parent.appendChild(div);
|
|
70
|
+
|
|
71
|
+
// Add the blank node to the node list
|
|
72
|
+
const tail = targetList.getTail();
|
|
73
|
+
blankNode.current = {
|
|
74
|
+
list: targetList,
|
|
75
|
+
node: targetList.add({
|
|
76
|
+
ref: { current: div },
|
|
77
|
+
setStyle: () => {},
|
|
78
|
+
index: tail ? tail[4] + 1 : 0,
|
|
79
|
+
id: 'blank',
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
}, [draggedNode, targetList]);
|
|
83
|
+
|
|
84
|
+
const removeBlankNode = useCallback(() => {
|
|
85
|
+
const blank = blankNode.current;
|
|
86
|
+
if (!blank) return;
|
|
87
|
+
blankNode.current = null;
|
|
88
|
+
blank.list.remove(blank.node);
|
|
89
|
+
const [, , blankNodeRef] = blank.node;
|
|
90
|
+
if (!blank.list.ref.current || !blankNodeRef.current) return;
|
|
91
|
+
const parent = blankNodeRef.current.parentElement;
|
|
92
|
+
if (parent) parent.removeChild(blankNodeRef.current);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
// Remove the blank node if the dragged node has been dropped
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!draggedNode) return () => {};
|
|
98
|
+
return () => removeBlankNode();
|
|
99
|
+
}, [draggedNode, removeBlankNode]);
|
|
100
|
+
|
|
101
|
+
return removeBlankNode;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default useBlankNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Position } from '@os-design/use-drag';
|
|
2
|
+
import React, { MouseEvent, TouchEvent, useContext } from 'react';
|
|
3
|
+
import NodeList, { ExistingNode } from './NodeList';
|
|
4
|
+
|
|
5
|
+
export interface SetDraggedNodeProps {
|
|
6
|
+
list: NodeList;
|
|
7
|
+
node: ExistingNode;
|
|
8
|
+
position: Position;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type NodeEventHandler<T> = (e: T, list: NodeList, node: ExistingNode) => void;
|
|
12
|
+
|
|
13
|
+
interface DragAndDropContextProps {
|
|
14
|
+
registerList: (list: NodeList) => void;
|
|
15
|
+
deregisterList: (id: string) => void;
|
|
16
|
+
onMouseDown: NodeEventHandler<MouseEvent>;
|
|
17
|
+
onTouchStart: NodeEventHandler<TouchEvent>;
|
|
18
|
+
nodeClassName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DragAndDropContext = React.createContext<DragAndDropContextProps>({
|
|
22
|
+
registerList: () => {},
|
|
23
|
+
deregisterList: () => {},
|
|
24
|
+
onMouseDown: () => {},
|
|
25
|
+
onTouchStart: () => {},
|
|
26
|
+
nodeClassName: '',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const useDragAndDrop = (): DragAndDropContextProps =>
|
|
30
|
+
useContext(DragAndDropContext);
|
|
31
|
+
|
|
32
|
+
export default useDragAndDrop;
|