@os-design/drag-sort 1.0.19 → 1.0.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-design/drag-sort",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design.git",
6
6
  "main": "dist/cjs/index.js",
@@ -14,7 +14,14 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "src",
19
+ "!**/*.test.ts",
20
+ "!**/*.test.tsx",
21
+ "!**/__tests__",
22
+ "!**/*.stories.tsx",
23
+ "!**/*.stories.mdx",
24
+ "!**/*.example.tsx"
18
25
  ],
19
26
  "sideEffects": false,
20
27
  "scripts": {
@@ -29,22 +36,22 @@
29
36
  "access": "public"
30
37
  },
31
38
  "dependencies": {
32
- "@os-design/portal": "^1.0.10",
33
- "@os-design/use-auto-scroll": "^1.0.11",
34
- "@os-design/use-drag": "^1.0.9",
35
- "@os-design/use-event": "^1.0.13",
36
- "@os-design/use-memo-object": "^1.0.8",
37
- "@os-design/use-prevent-default-event": "^1.0.9",
38
- "@os-design/use-throttle": "^1.0.14"
39
+ "@os-design/portal": "^1.0.12",
40
+ "@os-design/use-auto-scroll": "^1.0.13",
41
+ "@os-design/use-drag": "^1.0.11",
42
+ "@os-design/use-event": "^1.0.15",
43
+ "@os-design/use-memo-object": "^1.0.10",
44
+ "@os-design/use-prevent-default-event": "^1.0.11",
45
+ "@os-design/use-throttle": "^1.0.16"
39
46
  },
40
47
  "devDependencies": {
41
- "@os-design/omit-emotion-props": "^1.0.11",
42
- "@os-design/use-size": "^1.0.14",
48
+ "@os-design/omit-emotion-props": "^1.0.13",
49
+ "@os-design/use-size": "^1.0.16",
43
50
  "react-window": "^1.8.9"
44
51
  },
45
52
  "peerDependencies": {
46
53
  "react": ">=18",
47
54
  "react-dom": ">=18"
48
55
  },
49
- "gitHead": "0e88d3afc41e36cee61222a039ef1aa4d08115b5"
56
+ "gitHead": "e5d8409760608145d2c738aa5789d0465ae5416f"
50
57
  }
@@ -0,0 +1,235 @@
1
+ import Portal, { PortalProps } from '@os-design/portal';
2
+ import useAutoScroll from '@os-design/use-auto-scroll';
3
+ import useDrag, {
4
+ OnDragEnd,
5
+ OnDragMove,
6
+ OnDragStart,
7
+ Position,
8
+ } from '@os-design/use-drag';
9
+ import useMemoObject from '@os-design/use-memo-object';
10
+ import usePreventDefaultEvent from '@os-design/use-prevent-default-event';
11
+
12
+ import React, {
13
+ MouseEvent,
14
+ TouchEvent,
15
+ useCallback,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'react';
20
+ import ListStore from './utils/ListStore';
21
+ import NodeList, { ExistingNode } from './utils/NodeList';
22
+ import { DragAndDropContext } from './utils/useDragAndDrop';
23
+
24
+ import useDragEffect, {
25
+ DragEndHandler,
26
+ DraggedNode,
27
+ } from './utils/useDragEffect';
28
+ import useGeneratedId from './utils/useGeneratedId';
29
+ import useInitRect from './utils/useInitRect';
30
+
31
+ import useTransitionStyle from './utils/useTransitionStyle';
32
+
33
+ export interface DragAndDropProps {
34
+ /**
35
+ * The container in which the dragged node will be rendered.
36
+ * @default document.body
37
+ */
38
+ draggedNodeContainer?: PortalProps['container'];
39
+ /**
40
+ * The min distance required to start dragging a node (in px).
41
+ * @default 10
42
+ */
43
+ minMouseDistPx?: number;
44
+ /**
45
+ * The delay of the long press event required to start dragging a node on the touch device (in ms).
46
+ * @default 500
47
+ */
48
+ longPressMs?: number;
49
+ /**
50
+ * The distance to the bound at which the list starts to scroll automatically (in percent).
51
+ * @default 20
52
+ */
53
+ autoScrollDistPercent?: number;
54
+ /**
55
+ * The max auto scroll speed (in px).
56
+ * @default 100
57
+ */
58
+ autoScrollMaxSpeedPx?: number;
59
+ /**
60
+ * The animation duration (in ms).
61
+ * @default 250
62
+ */
63
+ transitionDelayMs?: number;
64
+ /**
65
+ * The callback that is called when the drag is completed.
66
+ * @default undefined
67
+ */
68
+ onDragEnd?: DragEndHandler;
69
+ /**
70
+ * The children.
71
+ * @default undefined
72
+ */
73
+ children?: React.ReactNode;
74
+ }
75
+
76
+ interface StartNode {
77
+ list: NodeList;
78
+ node: ExistingNode;
79
+ }
80
+
81
+ /**
82
+ * The container containing one or more lists with draggable nodes.
83
+ */
84
+ const DragAndDrop: React.FC<DragAndDropProps> = ({
85
+ draggedNodeContainer,
86
+ minMouseDistPx = 10,
87
+ longPressMs = 500,
88
+ autoScrollDistPercent = 20,
89
+ autoScrollMaxSpeedPx = 100,
90
+ transitionDelayMs = 250,
91
+ onDragEnd = () => {},
92
+ children,
93
+ }) => {
94
+ // The user can drag a node between the lists (the Droppable components).
95
+ // To determine which list a node should be dropped in, we need to store refs to all the lists.
96
+ const listStoreRef = useRef<ListStore>(new ListStore());
97
+
98
+ // The class name for a node used to set the transition style
99
+ const generatedId = useGeneratedId();
100
+ const nodeClassName = useMemo(() => `n${generatedId}`, [generatedId]);
101
+
102
+ // The node that is currently being dragged
103
+ const [draggedNode, setDraggedNode] = useState<DraggedNode | null>(null);
104
+ const startNodeRef = useRef<StartNode | null>(null);
105
+ const [cursorPosition, setCursorPosition] = useState<Position>({
106
+ x: 0,
107
+ y: 0,
108
+ });
109
+
110
+ // Add a new list to the store
111
+ const registerList = useCallback((list: NodeList) => {
112
+ listStoreRef.current.add(list);
113
+ }, []);
114
+
115
+ // Remove the existing list from the store
116
+ const deregisterList = useCallback((id: string) => {
117
+ listStoreRef.current.remove(id);
118
+ }, []);
119
+
120
+ // Set the style to delay transitions when the node is dragging
121
+ useTransitionStyle({
122
+ className: nodeClassName,
123
+ ms: transitionDelayMs,
124
+ enabled: !!draggedNode,
125
+ });
126
+
127
+ const onDragStart = useCallback<OnDragStart>(
128
+ (pos: Position, startPos: Position) => {
129
+ const startNode = startNodeRef.current;
130
+ if (!startNode) return;
131
+ const [, , nodeRef] = startNode.node;
132
+ if (!nodeRef.current) return;
133
+ const rect = nodeRef.current.getBoundingClientRect();
134
+ setCursorPosition(pos);
135
+ setDraggedNode({
136
+ list: startNode.list,
137
+ node: startNode.node,
138
+ position: {
139
+ x: startPos.x - rect.x,
140
+ y: startPos.y - rect.y,
141
+ },
142
+ });
143
+ startNodeRef.current = null;
144
+ },
145
+ []
146
+ );
147
+
148
+ const onDragMove = useCallback<OnDragMove>((pos: Position) => {
149
+ setCursorPosition(pos);
150
+ }, []);
151
+
152
+ const dragEndHandler = useCallback<OnDragEnd>(() => {
153
+ setDraggedNode(null);
154
+ }, []);
155
+
156
+ const { onMouseDown, onTouchStart } = useDrag({
157
+ onDragStart,
158
+ onDragMove,
159
+ onDragEnd: dragEndHandler,
160
+ minMouseDistPx,
161
+ longPressMs,
162
+ });
163
+
164
+ // Handlers that determine whether a user clicks on the node
165
+ const mouseDownHandler = useCallback(
166
+ (e: MouseEvent, list: NodeList, node: ExistingNode) => {
167
+ startNodeRef.current = { list, node };
168
+ onMouseDown(e);
169
+ },
170
+ [onMouseDown]
171
+ );
172
+ const touchStartHandler = useCallback(
173
+ (e: TouchEvent, list: NodeList, node: ExistingNode) => {
174
+ startNodeRef.current = { list, node };
175
+ onTouchStart(e);
176
+ },
177
+ [onTouchStart]
178
+ );
179
+
180
+ // Prevent body scrolling when the node is dragging.
181
+ // It's important to attach the event to the body (not to the document). Otherwise, it won't work in mobile chrome.
182
+ usePreventDefaultEvent(document.body, 'touchmove', !!draggedNode);
183
+
184
+ // Implement the drag animation
185
+ useDragEffect({
186
+ draggedNode,
187
+ cursorPosition,
188
+ listStoreRef,
189
+ onDragEnd,
190
+ });
191
+
192
+ // Auto scroll if the cursor position is located near the border
193
+ useAutoScroll({
194
+ enabled: !!draggedNode,
195
+ distPercent: autoScrollDistPercent,
196
+ maxSpeedPx: autoScrollMaxSpeedPx,
197
+ });
198
+
199
+ const initRect = useInitRect(draggedNode ? draggedNode.node[2] : undefined);
200
+
201
+ const dragAndDropContext = useMemoObject({
202
+ registerList,
203
+ deregisterList,
204
+ onMouseDown: mouseDownHandler,
205
+ onTouchStart: touchStartHandler,
206
+ nodeClassName,
207
+ });
208
+
209
+ return (
210
+ <DragAndDropContext.Provider value={dragAndDropContext}>
211
+ {children}
212
+
213
+ {draggedNode && (
214
+ <Portal container={draggedNodeContainer}>
215
+ {draggedNode.list.renderDraggedNode({
216
+ index: draggedNode.node[4],
217
+ id: draggedNode.node[5],
218
+ style: {
219
+ position: 'fixed',
220
+ left: cursorPosition.x - draggedNode.position.x,
221
+ top: cursorPosition.y - draggedNode.position.y,
222
+ width: initRect ? initRect.initWidth : undefined,
223
+ height: initRect ? initRect.initHeight : undefined,
224
+ zIndex: 1001,
225
+ },
226
+ })}
227
+ </Portal>
228
+ )}
229
+ </DragAndDropContext.Provider>
230
+ );
231
+ };
232
+
233
+ export type { DragEndHandler } from './utils/useDragEffect';
234
+
235
+ export default DragAndDrop;
@@ -0,0 +1,109 @@
1
+ import useMemoObject from '@os-design/use-memo-object';
2
+
3
+ import React, {
4
+ CSSProperties,
5
+ MouseEvent,
6
+ RefObject,
7
+ TouchEvent,
8
+ useCallback,
9
+ useEffect,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import { Node } from './utils/NodeList';
14
+
15
+ import useAppendClassName from './utils/useAppendClassName';
16
+ import useDragAndDrop from './utils/useDragAndDrop';
17
+ import useDroppable from './utils/useDroppable';
18
+
19
+ export interface DraggableHandlers {
20
+ /**
21
+ * The handler that should be called when the mouse down event occurs.
22
+ */
23
+ onMouseDown: (e: MouseEvent) => void;
24
+ /**
25
+ * The handler that should be called when the touch start event occurs.
26
+ */
27
+ onTouchStart: (e: TouchEvent) => void;
28
+ }
29
+
30
+ export interface DraggableChildrenProps {
31
+ /**
32
+ * The reference to the draggable list item.
33
+ */
34
+ ref: RefObject<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
35
+ /**
36
+ * Additional styles for moving the list item.
37
+ */
38
+ style: CSSProperties;
39
+ /**
40
+ * The handlers to catch events when the user starts dragging the list item.
41
+ */
42
+ handlers: DraggableHandlers;
43
+ }
44
+
45
+ export interface DraggableProps {
46
+ /**
47
+ * The index of the draggable node.
48
+ */
49
+ index: number;
50
+ /**
51
+ * The ID of the draggable node.
52
+ */
53
+ id: string;
54
+ /**
55
+ * The function that renders the draggable node.
56
+ */
57
+ children: (props: DraggableChildrenProps) => React.ReactNode;
58
+ }
59
+
60
+ const Draggable: React.FC<DraggableProps> = ({ index, id, children }) => {
61
+ // The reference to the list item
62
+ const ref = useRef<HTMLDivElement | null>(null);
63
+ // The reference to the node containing the refs to the prev and next nodes
64
+ const nodeRef = useRef<Node>(null);
65
+ // Additional styles for moving the list item
66
+ const [style, setStyle] = useState<CSSProperties>({});
67
+
68
+ const { nodeClassName } = useDragAndDrop();
69
+ const { registerNode, deregisterNode, onMouseDown, onTouchStart } =
70
+ useDroppable();
71
+
72
+ // Register the node in the list
73
+ useEffect(() => {
74
+ nodeRef.current = registerNode({ ref, setStyle, index, id });
75
+ return () => {
76
+ if (!nodeRef.current) return;
77
+ deregisterNode(nodeRef.current);
78
+ nodeRef.current = null;
79
+ };
80
+ }, [deregisterNode, id, index, registerNode]);
81
+
82
+ // Set the class name for the node to apply the transition style (see the DragAndDrop container)
83
+ useAppendClassName(ref, nodeClassName);
84
+
85
+ // Handlers that determine whether a user clicks on the node
86
+ const mouseDownHandler = useCallback(
87
+ (e: MouseEvent) => {
88
+ if (!nodeRef.current) return;
89
+ onMouseDown(e, nodeRef.current);
90
+ },
91
+ [onMouseDown]
92
+ );
93
+ const touchStartHandler = useCallback(
94
+ (e: TouchEvent) => {
95
+ if (!nodeRef.current) return;
96
+ onTouchStart(e, nodeRef.current);
97
+ },
98
+ [onTouchStart]
99
+ );
100
+
101
+ const handlers = useMemoObject({
102
+ onMouseDown: mouseDownHandler,
103
+ onTouchStart: touchStartHandler,
104
+ });
105
+
106
+ return <>{children({ ref, style, handlers })}</>;
107
+ };
108
+
109
+ export default Draggable;
@@ -0,0 +1,142 @@
1
+ import useMemoObject from '@os-design/use-memo-object';
2
+
3
+ import React, {
4
+ MouseEvent,
5
+ RefObject,
6
+ TouchEvent,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ } from 'react';
12
+ import NodeList, {
13
+ ExistingNode,
14
+ NodeProps,
15
+ RenderDraggedNode,
16
+ } from './utils/NodeList';
17
+ import useDragAndDrop from './utils/useDragAndDrop';
18
+ import { DroppableContext } from './utils/useDroppable';
19
+ import useGeneratedId from './utils/useGeneratedId';
20
+
21
+ export interface DroppableChildrenProps {
22
+ /**
23
+ * The reference to the list.
24
+ * If a virtual list is used, pass it to the outerRef prop.
25
+ */
26
+ ref: RefObject<HTMLDivElement>;
27
+ /**
28
+ * The reference to the container inside the virtual list.
29
+ * Pass it to the innerRef prop.
30
+ */
31
+ innerRef: RefObject<HTMLDivElement>;
32
+ }
33
+
34
+ export interface DroppableProps {
35
+ /**
36
+ * The function that renders the dragged node.
37
+ */
38
+ renderDraggedNode: RenderDraggedNode;
39
+ /**
40
+ * The ID of the list with draggable nodes.
41
+ * Used to determine in which list the dragged node was dropped.
42
+ * @default undefined
43
+ */
44
+ id?: string;
45
+ /**
46
+ * Whether the list is horizontal.
47
+ * @default false
48
+ */
49
+ horizontal?: boolean;
50
+ /**
51
+ * The function that renders the list with draggable nodes.
52
+ */
53
+ children: (props: DroppableChildrenProps) => React.ReactNode;
54
+ }
55
+
56
+ const Droppable: React.FC<DroppableProps> = ({
57
+ renderDraggedNode,
58
+ id,
59
+ horizontal = false,
60
+ children,
61
+ }) => {
62
+ // The reference to the list
63
+ const ref = useRef<HTMLDivElement>(null);
64
+ // The reference to the container inside the virtual list
65
+ const innerRef = useRef<HTMLDivElement>(null);
66
+ // The unique ID of the list. If no ID was specified, the generated one is used.
67
+ const generatedId = useGeneratedId();
68
+ const droppableId = useMemo(() => id || generatedId, [generatedId, id]);
69
+ // The reference to the list of nodes
70
+ const listRef = useRef(
71
+ new NodeList({
72
+ id: droppableId,
73
+ ref,
74
+ innerRef,
75
+ horizontal,
76
+ renderDraggedNode,
77
+ })
78
+ );
79
+
80
+ // Update the ID of the list if it changes
81
+ useEffect(() => {
82
+ listRef.current.id = droppableId;
83
+ }, [droppableId]);
84
+
85
+ // Update the list orientation if it changes.
86
+ // There is no need to check the order of the nodes in the list because it will be the same.
87
+ useEffect(() => {
88
+ listRef.current.horizontal = horizontal;
89
+ }, [horizontal]);
90
+
91
+ // Update the callback that renders the dragged node if it changes
92
+ useEffect(() => {
93
+ listRef.current.renderDraggedNode = renderDraggedNode;
94
+ }, [renderDraggedNode]);
95
+
96
+ const { registerList, deregisterList, onMouseDown, onTouchStart } =
97
+ useDragAndDrop();
98
+
99
+ // Register the list in the store
100
+ useEffect(() => {
101
+ registerList(listRef.current);
102
+ const listId = listRef.current.id;
103
+ return () => deregisterList(listId);
104
+ }, [deregisterList, registerList]);
105
+
106
+ const registerNode = useCallback(
107
+ (props: NodeProps) => listRef.current.add(props),
108
+ []
109
+ );
110
+ const deregisterNode = useCallback((node: ExistingNode) => {
111
+ listRef.current.remove(node);
112
+ }, []);
113
+
114
+ // Handlers that determine whether a user clicks on the node
115
+ const mouseDownHandler = useCallback(
116
+ (e: MouseEvent, node: ExistingNode) => {
117
+ onMouseDown(e, listRef.current, node);
118
+ },
119
+ [onMouseDown]
120
+ );
121
+ const touchStartHandler = useCallback(
122
+ (e: TouchEvent, node: ExistingNode) => {
123
+ onTouchStart(e, listRef.current, node);
124
+ },
125
+ [onTouchStart]
126
+ );
127
+
128
+ const droppableContext = useMemoObject({
129
+ registerNode,
130
+ deregisterNode,
131
+ onMouseDown: mouseDownHandler,
132
+ onTouchStart: touchStartHandler,
133
+ });
134
+
135
+ return (
136
+ <DroppableContext.Provider value={droppableContext}>
137
+ {(children as Function)({ ref, innerRef })}
138
+ </DroppableContext.Provider>
139
+ );
140
+ };
141
+
142
+ export default Droppable;
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { default as DragAndDrop } from './DragAndDrop';
2
+ export { default as Droppable } from './Droppable';
3
+ export { default as Draggable } from './Draggable';
4
+
5
+ export * from './DragAndDrop';
6
+ export * from './Droppable';
7
+ export * from './Draggable';
@@ -0,0 +1,47 @@
1
+ import NodeList from './NodeList';
2
+
3
+ /**
4
+ * Stores all the lists where the draggable items is located.
5
+ */
6
+ class ListStore {
7
+ private lists: NodeList[];
8
+
9
+ public constructor() {
10
+ this.lists = [];
11
+ }
12
+
13
+ /**
14
+ * Adds a new list to the store.
15
+ */
16
+ public add(list: NodeList) {
17
+ this.lists.push(list);
18
+ }
19
+
20
+ /**
21
+ * Removes the list from the store.
22
+ */
23
+ public remove(id: string) {
24
+ const index = this.lists.findIndex((item) => item.id === id);
25
+ if (index === -1) return;
26
+ this.lists.splice(index, 1);
27
+ }
28
+
29
+ /**
30
+ * Returns the list in which the position is located.
31
+ */
32
+ public findByPosition(x: number, y: number) {
33
+ return this.lists.find((list) => {
34
+ const { current } = list.ref;
35
+ if (!current) return false;
36
+ const rect = current.getBoundingClientRect();
37
+ return (
38
+ x >= rect.x &&
39
+ x <= rect.x + rect.width &&
40
+ y >= rect.y &&
41
+ y <= rect.y + rect.height
42
+ );
43
+ });
44
+ }
45
+ }
46
+
47
+ export default ListStore;