@use-kona/editor 0.1.24-dnd.5 → 0.1.24-dnd.7
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/dist/plugins/DnDPlugin/DnDPlugin.js +65 -7
- package/dist/plugins/DnDPlugin/types.js +2 -0
- package/dist/plugins/NodeIdPlugin/NodeIdPlugin.js +6 -4
- package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +1 -1
- package/dist/src/plugins/DnDPlugin/types.d.ts +28 -13
- package/dist/src/plugins/NodeIdPlugin/NodeIdPlugin.d.ts +1 -0
- package/package.json +1 -1
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +118 -9
- package/src/plugins/DnDPlugin/types.ts +35 -13
- package/src/plugins/NodeIdPlugin/NodeIdPlugin.ts +9 -7
|
@@ -4,7 +4,7 @@ import { map } from "nanostores";
|
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { useDrag, useDrop } from "react-dnd";
|
|
6
6
|
import { NativeTypes } from "react-dnd-html5-backend";
|
|
7
|
-
import { Editor, Path, Transforms } from "slate";
|
|
7
|
+
import { Editor, Element, Path, Transforms } from "slate";
|
|
8
8
|
import { ReactEditor, useReadOnly, useSlate } from "slate-react";
|
|
9
9
|
import { getNodesByNodeIds } from "./utils.js";
|
|
10
10
|
class DnDPlugin {
|
|
@@ -42,7 +42,9 @@ class DnDPlugin {
|
|
|
42
42
|
renderBlock = (props)=>{
|
|
43
43
|
const editor = useSlate();
|
|
44
44
|
const $store = useStore(this.store);
|
|
45
|
+
const isCopying = useRef(false);
|
|
45
46
|
const options = this.options;
|
|
47
|
+
const generateId = this.options?.nodeIdPlugin?.generateId;
|
|
46
48
|
const isReadOnly = useReadOnly();
|
|
47
49
|
const [dropPosition, setDropPosition] = useState(null);
|
|
48
50
|
const dropTargetRef = useRef(null);
|
|
@@ -55,14 +57,18 @@ class DnDPlugin {
|
|
|
55
57
|
const nodes = selected.length > 0 && selected.includes(currentElement.nodeId) ? getNodesByNodeIds(editor, selected) : [
|
|
56
58
|
structuredClone(props.element)
|
|
57
59
|
];
|
|
60
|
+
const item = this.options.customTypes?.[currentElement.type]?.getData?.(currentElement);
|
|
58
61
|
const items = nodes.map((node)=>{
|
|
59
62
|
if (this.options.customTypes?.[node.type]) return this.options.customTypes[node.type].getData?.(node);
|
|
60
63
|
return null;
|
|
61
64
|
}).filter(Boolean);
|
|
62
65
|
const dragItem = {
|
|
66
|
+
kind: 'inner',
|
|
63
67
|
nodes,
|
|
68
|
+
item,
|
|
64
69
|
items,
|
|
65
70
|
source: {
|
|
71
|
+
editor,
|
|
66
72
|
editorId: this.getEditorId(editor),
|
|
67
73
|
documentId: options.documentId
|
|
68
74
|
}
|
|
@@ -98,32 +104,47 @@ class DnDPlugin {
|
|
|
98
104
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
|
99
105
|
hoverClientY < hoverMiddleY ? setDropPosition('top') : setDropPosition('bottom');
|
|
100
106
|
},
|
|
101
|
-
drop: (
|
|
107
|
+
drop: (data, monitor)=>{
|
|
102
108
|
const itemType = monitor.getItemType();
|
|
109
|
+
const sourceEditor = 'inner' === data.kind ? data.source?.editor : null;
|
|
110
|
+
const isSameDocument = 'inner' === data.kind && data.source?.documentId === this.options.documentId && void 0 !== this.options.documentId;
|
|
111
|
+
const isSameEditor = 'inner' === data.kind && data.source?.editorId === this.editors.get(editor) && void 0 !== this.editors.get(editor);
|
|
103
112
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
104
113
|
if (!sourceTo) return;
|
|
105
114
|
const insertAt = getDropPath(editor, sourceTo, dropPosition || 'bottom');
|
|
106
115
|
if (!insertAt) return;
|
|
107
116
|
const customDropHandler = itemType && options.customDropHandlers?.[itemType];
|
|
108
|
-
if (customDropHandler) return void customDropHandler.onDrop({
|
|
117
|
+
if (customDropHandler && 'outer' === data.kind) return void customDropHandler.onDrop({
|
|
109
118
|
editor,
|
|
110
|
-
|
|
119
|
+
data,
|
|
111
120
|
insertAt
|
|
112
121
|
});
|
|
113
122
|
switch(itemType){
|
|
114
123
|
case NativeTypes.FILE:
|
|
115
|
-
options.onDropFiles(editor,
|
|
124
|
+
options.onDropFiles(editor, data.files, insertAt);
|
|
116
125
|
break;
|
|
117
126
|
default:
|
|
118
127
|
{
|
|
119
|
-
const dragItem =
|
|
128
|
+
const dragItem = data;
|
|
129
|
+
if ('outer' === dragItem.kind) return;
|
|
120
130
|
if (!dropPosition || !dragItem.nodes.length) return;
|
|
121
131
|
const nodeIds = dragItem.nodes.map((node)=>node.nodeId);
|
|
122
|
-
|
|
132
|
+
if (isSameDocument || isSameEditor) if (isCopying.current) {
|
|
133
|
+
if (!generateId) return;
|
|
134
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
135
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
136
|
+
} else moveNode(editor, sourceTo, nodeIds, dropPosition);
|
|
137
|
+
else {
|
|
138
|
+
if (!generateId) return;
|
|
139
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
140
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
141
|
+
if (!isCopying.current && sourceEditor) removeNodes(sourceEditor, nodeIds);
|
|
142
|
+
}
|
|
123
143
|
$store.selected.clear();
|
|
124
144
|
break;
|
|
125
145
|
}
|
|
126
146
|
}
|
|
147
|
+
isCopying.current = false;
|
|
127
148
|
setDropPosition(null);
|
|
128
149
|
}
|
|
129
150
|
});
|
|
@@ -147,6 +168,9 @@ class DnDPlugin {
|
|
|
147
168
|
selected: 'string' == typeof currentElement.nodeId ? $store.selected.has(currentElement.nodeId) : false,
|
|
148
169
|
onToggleSelected: ()=>{
|
|
149
170
|
if ('string' == typeof currentElement.nodeId) this.handleToggleSelected(currentElement.nodeId);
|
|
171
|
+
},
|
|
172
|
+
onNativeDrop: (event)=>{
|
|
173
|
+
isCopying.current = event.altKey;
|
|
150
174
|
}
|
|
151
175
|
});
|
|
152
176
|
};
|
|
@@ -180,4 +204,38 @@ const moveNode = (editor, targetNodePath, nodeIds, position)=>{
|
|
|
180
204
|
});
|
|
181
205
|
Editor.normalize(editor);
|
|
182
206
|
};
|
|
207
|
+
const insertNodes = (editor, path, nodes)=>{
|
|
208
|
+
Editor.withoutNormalizing(editor, ()=>{
|
|
209
|
+
Transforms.insertNodes(editor, nodes, {
|
|
210
|
+
at: path
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
const removeNodes = (editor, nodeIds)=>{
|
|
215
|
+
try {
|
|
216
|
+
Transforms.removeNodes(editor, {
|
|
217
|
+
at: [],
|
|
218
|
+
match: (n)=>{
|
|
219
|
+
if (!Element.isElement(n)) return false;
|
|
220
|
+
if (!Editor.isBlock(editor, n)) return false;
|
|
221
|
+
const node = n;
|
|
222
|
+
return 'string' == typeof node.nodeId && nodeIds.includes(node.nodeId);
|
|
223
|
+
},
|
|
224
|
+
mode: 'highest'
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const cloneWithNewIds = (nodes, generateId)=>{
|
|
231
|
+
const copy = structuredClone(nodes);
|
|
232
|
+
const assignId = (node)=>{
|
|
233
|
+
node.nodeId = generateId();
|
|
234
|
+
node.children.forEach((node)=>{
|
|
235
|
+
if ('children' in node) assignId(node);
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
copy.forEach(assignId);
|
|
239
|
+
return copy;
|
|
240
|
+
};
|
|
183
241
|
export { DnDPlugin };
|
|
@@ -10,8 +10,10 @@ const assignId = (node, generateId)=>{
|
|
|
10
10
|
};
|
|
11
11
|
class NodeIdPlugin {
|
|
12
12
|
options;
|
|
13
|
+
generateId;
|
|
13
14
|
constructor(options){
|
|
14
15
|
this.options = options;
|
|
16
|
+
this.generateId = options.generateId;
|
|
15
17
|
}
|
|
16
18
|
init(editor) {
|
|
17
19
|
const { apply, normalizeNode } = editor;
|
|
@@ -20,22 +22,22 @@ class NodeIdPlugin {
|
|
|
20
22
|
const node = Node.get(editor, operation.path);
|
|
21
23
|
const oldType = operation.properties.type;
|
|
22
24
|
const newType = operation.newProperties.type;
|
|
23
|
-
if (!node || !Text.isText(node) && oldType !== newType) operation.newProperties.nodeId = this.
|
|
25
|
+
if (!node || !Text.isText(node) && oldType !== newType) operation.newProperties.nodeId = this.generateId();
|
|
24
26
|
return apply(operation);
|
|
25
27
|
}
|
|
26
28
|
if ('insert_node' === operation.type) {
|
|
27
|
-
assignId(operation.node, this.
|
|
29
|
+
assignId(operation.node, this.generateId);
|
|
28
30
|
return apply(operation);
|
|
29
31
|
}
|
|
30
32
|
if ('split_node' === operation.type) {
|
|
31
33
|
const node = Node.get(editor, operation.path);
|
|
32
|
-
if (!node || !Text.isText(node)) operation.properties.nodeId = this.
|
|
34
|
+
if (!node || !Text.isText(node)) operation.properties.nodeId = this.generateId();
|
|
33
35
|
}
|
|
34
36
|
return apply(operation);
|
|
35
37
|
};
|
|
36
38
|
editor.normalizeNode = (entry)=>{
|
|
37
39
|
const [node] = entry;
|
|
38
|
-
assignId(node, this.
|
|
40
|
+
assignId(node, this.generateId);
|
|
39
41
|
normalizeNode(entry);
|
|
40
42
|
};
|
|
41
43
|
return editor;
|
|
@@ -2,7 +2,7 @@ import { type MapStore } from 'nanostores';
|
|
|
2
2
|
import { Editor } from 'slate';
|
|
3
3
|
import { ReactEditor, type RenderElementProps } from 'slate-react';
|
|
4
4
|
import type { IPlugin } from '../../types';
|
|
5
|
-
import type
|
|
5
|
+
import { type Options } from './types';
|
|
6
6
|
export declare class DnDPlugin implements IPlugin {
|
|
7
7
|
private options;
|
|
8
8
|
store: MapStore<{
|
|
@@ -2,9 +2,16 @@ import type React from 'react';
|
|
|
2
2
|
import type { ConnectDragPreview, ConnectDragSource, ConnectDropTarget } from 'react-dnd';
|
|
3
3
|
import type { Editor, Path } from 'slate';
|
|
4
4
|
import type { RenderElementProps } from 'slate-react';
|
|
5
|
-
import type { CustomElement } from '../../../types';
|
|
6
|
-
|
|
5
|
+
import type { CustomElement, CustomText } from '../../../types';
|
|
6
|
+
import type { NodeIdPlugin } from '../NodeIdPlugin';
|
|
7
|
+
export type NodeWithId = Omit<CustomElement, 'children'> & {
|
|
7
8
|
nodeId: string;
|
|
9
|
+
children: (NodeWithId | CustomText)[];
|
|
10
|
+
};
|
|
11
|
+
export declare const isNodeWithId: (node: unknown) => node is NodeWithId;
|
|
12
|
+
export type CustomDropHandler<T = unknown> = {
|
|
13
|
+
type: string | symbol;
|
|
14
|
+
onDrop: (context: CustomDropContext<T>) => DropResult;
|
|
8
15
|
};
|
|
9
16
|
export type Options = {
|
|
10
17
|
editorId?: string;
|
|
@@ -18,6 +25,7 @@ export type Options = {
|
|
|
18
25
|
position: 'top' | 'bottom' | null;
|
|
19
26
|
selected?: boolean;
|
|
20
27
|
onToggleSelected: () => void;
|
|
28
|
+
onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
21
29
|
}) => React.ReactNode;
|
|
22
30
|
ignoreNodes?: string[];
|
|
23
31
|
customTypes?: {
|
|
@@ -27,25 +35,23 @@ export type Options = {
|
|
|
27
35
|
};
|
|
28
36
|
};
|
|
29
37
|
customDropHandlers?: {
|
|
30
|
-
[type: string | symbol]:
|
|
31
|
-
type: string | symbol;
|
|
32
|
-
onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
|
|
33
|
-
};
|
|
38
|
+
[type: string | symbol]: CustomDropHandler;
|
|
34
39
|
};
|
|
40
|
+
nodeIdPlugin?: NodeIdPlugin;
|
|
35
41
|
};
|
|
36
|
-
type CustomDropContext<
|
|
42
|
+
export type CustomDropContext<TItem = unknown> = {
|
|
37
43
|
editor: Editor;
|
|
38
|
-
|
|
44
|
+
data: EditorOuterDragItem<TItem>;
|
|
39
45
|
insertAt: Path;
|
|
40
46
|
};
|
|
41
47
|
type DropResult = 'handled' | 'ignored';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
export type EditorDragItem = {
|
|
48
|
+
export type EditorInnerDragItem<TItem = unknown> = {
|
|
49
|
+
kind: 'inner';
|
|
46
50
|
nodes: NodeWithId[];
|
|
47
|
-
|
|
51
|
+
item?: TItem;
|
|
52
|
+
items?: TItem[];
|
|
48
53
|
source: {
|
|
54
|
+
editor: Editor;
|
|
49
55
|
/**
|
|
50
56
|
* Different editors have different unique identifiers.
|
|
51
57
|
*/
|
|
@@ -59,4 +65,13 @@ export type EditorDragItem = {
|
|
|
59
65
|
};
|
|
60
66
|
data?: Record<string, unknown>;
|
|
61
67
|
};
|
|
68
|
+
export type EditorOuterDragItem<TItem = unknown> = {
|
|
69
|
+
kind: 'outer';
|
|
70
|
+
item: TItem;
|
|
71
|
+
items: TItem[];
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* We're using this type to represent a drag item that is being dragged from the editor.
|
|
75
|
+
*/
|
|
76
|
+
export type EditorDragItem<TItem = unknown> = EditorInnerDragItem<TItem> | EditorOuterDragItem<TItem>;
|
|
62
77
|
export {};
|
|
@@ -9,6 +9,7 @@ type Options = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
|
|
11
11
|
private options;
|
|
12
|
+
generateId: () => string;
|
|
12
13
|
constructor(options: Options);
|
|
13
14
|
init(editor: Editor): import("slate").BaseEditor & import("slate-react").ReactEditor & {
|
|
14
15
|
getCommands: <T extends IPlugin>(plugin: new () => T) => T["commands"];
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { type MapStore, map } from 'nanostores';
|
|
|
4
4
|
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { type ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
|
|
6
6
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
|
7
|
-
import { Editor, Path, Transforms } from 'slate';
|
|
7
|
+
import { Editor, Element, Path, Transforms } from 'slate';
|
|
8
8
|
import {
|
|
9
9
|
ReactEditor,
|
|
10
10
|
type RenderElementProps,
|
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
} from 'slate-react';
|
|
14
14
|
import type { CustomElement } from '../../../types';
|
|
15
15
|
import type { IPlugin } from '../../types';
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
type EditorDragItem,
|
|
18
|
+
isNodeWithId,
|
|
19
|
+
type NodeWithId,
|
|
20
|
+
type Options,
|
|
21
|
+
} from './types';
|
|
17
22
|
import { getNodesByNodeIds } from './utils';
|
|
18
23
|
|
|
19
24
|
export class DnDPlugin implements IPlugin {
|
|
@@ -63,7 +68,9 @@ export class DnDPlugin implements IPlugin {
|
|
|
63
68
|
|
|
64
69
|
const $store = useStore(this.store);
|
|
65
70
|
|
|
71
|
+
const isCopying = useRef(false);
|
|
66
72
|
const options = this.options;
|
|
73
|
+
const generateId = this.options?.nodeIdPlugin?.generateId;
|
|
67
74
|
const isReadOnly = useReadOnly();
|
|
68
75
|
|
|
69
76
|
const [dropPosition, setDropPosition] = useState<'top' | 'bottom' | null>(
|
|
@@ -90,6 +97,11 @@ export class DnDPlugin implements IPlugin {
|
|
|
90
97
|
? getNodesByNodeIds(editor, selected)
|
|
91
98
|
: [structuredClone(props.element as NodeWithId)];
|
|
92
99
|
|
|
100
|
+
const item =
|
|
101
|
+
this.options.customTypes?.[currentElement.type]?.getData?.(
|
|
102
|
+
currentElement,
|
|
103
|
+
);
|
|
104
|
+
|
|
93
105
|
const items = nodes
|
|
94
106
|
.map((node) => {
|
|
95
107
|
if (this.options.customTypes?.[node.type]) {
|
|
@@ -100,9 +112,12 @@ export class DnDPlugin implements IPlugin {
|
|
|
100
112
|
.filter(Boolean);
|
|
101
113
|
|
|
102
114
|
const dragItem: EditorDragItem = {
|
|
115
|
+
kind: 'inner',
|
|
103
116
|
nodes,
|
|
117
|
+
item,
|
|
104
118
|
items,
|
|
105
119
|
source: {
|
|
120
|
+
editor,
|
|
106
121
|
editorId: this.getEditorId(editor),
|
|
107
122
|
documentId: options.documentId,
|
|
108
123
|
},
|
|
@@ -120,7 +135,11 @@ export class DnDPlugin implements IPlugin {
|
|
|
120
135
|
(customType) => customType.type,
|
|
121
136
|
);
|
|
122
137
|
|
|
123
|
-
const [{ isOver }, drop] = useDrop
|
|
138
|
+
const [{ isOver }, drop] = useDrop<
|
|
139
|
+
EditorDragItem,
|
|
140
|
+
void,
|
|
141
|
+
{ isOver: boolean }
|
|
142
|
+
>({
|
|
124
143
|
accept: [
|
|
125
144
|
DnDPlugin.DND_BLOCK_ELEMENT,
|
|
126
145
|
NativeTypes.FILE,
|
|
@@ -151,8 +170,18 @@ export class DnDPlugin implements IPlugin {
|
|
|
151
170
|
setDropPosition('bottom');
|
|
152
171
|
}
|
|
153
172
|
},
|
|
154
|
-
drop: (
|
|
173
|
+
drop: (data, monitor) => {
|
|
155
174
|
const itemType = monitor.getItemType();
|
|
175
|
+
const sourceEditor = data.kind === 'inner' ? data.source?.editor : null;
|
|
176
|
+
|
|
177
|
+
const isSameDocument =
|
|
178
|
+
data.kind === 'inner' &&
|
|
179
|
+
data.source?.documentId === this.options.documentId &&
|
|
180
|
+
this.options.documentId !== undefined;
|
|
181
|
+
const isSameEditor =
|
|
182
|
+
data.kind === 'inner' &&
|
|
183
|
+
data.source?.editorId === this.editors.get(editor) &&
|
|
184
|
+
this.editors.get(editor) !== undefined;
|
|
156
185
|
|
|
157
186
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
158
187
|
|
|
@@ -169,8 +198,8 @@ export class DnDPlugin implements IPlugin {
|
|
|
169
198
|
const customDropHandler =
|
|
170
199
|
itemType && options.customDropHandlers?.[itemType];
|
|
171
200
|
|
|
172
|
-
if (customDropHandler) {
|
|
173
|
-
customDropHandler.onDrop({ editor,
|
|
201
|
+
if (customDropHandler && data.kind === 'outer') {
|
|
202
|
+
customDropHandler.onDrop({ editor, data, insertAt });
|
|
174
203
|
return;
|
|
175
204
|
}
|
|
176
205
|
|
|
@@ -178,12 +207,17 @@ export class DnDPlugin implements IPlugin {
|
|
|
178
207
|
case NativeTypes.FILE:
|
|
179
208
|
options.onDropFiles(
|
|
180
209
|
editor,
|
|
181
|
-
(
|
|
210
|
+
(data as unknown as { files: File[] }).files,
|
|
182
211
|
insertAt,
|
|
183
212
|
);
|
|
184
213
|
break;
|
|
185
214
|
default: {
|
|
186
|
-
const dragItem =
|
|
215
|
+
const dragItem = data as EditorDragItem;
|
|
216
|
+
|
|
217
|
+
// We don't know what "default" behavior for the outer items is
|
|
218
|
+
if (dragItem.kind === 'outer') {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
187
221
|
|
|
188
222
|
if (!dropPosition || !dragItem.nodes.length) {
|
|
189
223
|
return;
|
|
@@ -191,12 +225,35 @@ export class DnDPlugin implements IPlugin {
|
|
|
191
225
|
|
|
192
226
|
const nodeIds = dragItem.nodes.map((node) => node.nodeId);
|
|
193
227
|
|
|
194
|
-
|
|
228
|
+
if (isSameDocument || isSameEditor) {
|
|
229
|
+
if (!isCopying.current) {
|
|
230
|
+
moveNode(editor, sourceTo, nodeIds, dropPosition);
|
|
231
|
+
} else {
|
|
232
|
+
if (!generateId) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
237
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
if (!generateId) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
245
|
+
|
|
246
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
247
|
+
if (!isCopying.current && sourceEditor) {
|
|
248
|
+
removeNodes(sourceEditor, nodeIds);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
195
251
|
$store.selected.clear();
|
|
196
252
|
break;
|
|
197
253
|
}
|
|
198
254
|
}
|
|
199
255
|
|
|
256
|
+
isCopying.current = false;
|
|
200
257
|
setDropPosition(null);
|
|
201
258
|
},
|
|
202
259
|
});
|
|
@@ -234,6 +291,9 @@ export class DnDPlugin implements IPlugin {
|
|
|
234
291
|
this.handleToggleSelected(currentElement.nodeId);
|
|
235
292
|
}
|
|
236
293
|
},
|
|
294
|
+
onNativeDrop: (event) => {
|
|
295
|
+
isCopying.current = event.altKey;
|
|
296
|
+
},
|
|
237
297
|
});
|
|
238
298
|
};
|
|
239
299
|
}
|
|
@@ -289,3 +349,52 @@ const moveNode = (
|
|
|
289
349
|
});
|
|
290
350
|
Editor.normalize(editor);
|
|
291
351
|
};
|
|
352
|
+
|
|
353
|
+
const insertNodes = (editor: Editor, path: Path, nodes: NodeWithId[]) => {
|
|
354
|
+
Editor.withoutNormalizing(editor, () => {
|
|
355
|
+
Transforms.insertNodes(editor, nodes, { at: path });
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const removeNodes = (editor: Editor, nodeIds: string[]) => {
|
|
360
|
+
try {
|
|
361
|
+
Transforms.removeNodes(editor, {
|
|
362
|
+
at: [],
|
|
363
|
+
match: (n) => {
|
|
364
|
+
if (!Element.isElement(n)) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!Editor.isBlock(editor, n)) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const node = n as NodeWithId;
|
|
373
|
+
return typeof node.nodeId === 'string' && nodeIds.includes(node.nodeId);
|
|
374
|
+
},
|
|
375
|
+
mode: 'highest',
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const cloneWithNewIds = (
|
|
383
|
+
nodes: NodeWithId[],
|
|
384
|
+
generateId: () => string,
|
|
385
|
+
): NodeWithId[] => {
|
|
386
|
+
const copy = structuredClone(nodes);
|
|
387
|
+
|
|
388
|
+
const assignId = (node: NodeWithId): void => {
|
|
389
|
+
node.nodeId = generateId();
|
|
390
|
+
node.children.forEach((node) => {
|
|
391
|
+
if ('children' in node) {
|
|
392
|
+
assignId(node);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
copy.forEach(assignId);
|
|
398
|
+
|
|
399
|
+
return copy;
|
|
400
|
+
};
|
|
@@ -7,15 +7,25 @@ import type {
|
|
|
7
7
|
} from 'react-dnd';
|
|
8
8
|
import type { Editor, Path } from 'slate';
|
|
9
9
|
import type { RenderElementProps } from 'slate-react';
|
|
10
|
-
import type { CustomElement } from '../../../types';
|
|
10
|
+
import type { CustomElement, CustomText } from '../../../types';
|
|
11
|
+
import type { NodeIdPlugin } from '../NodeIdPlugin';
|
|
11
12
|
|
|
12
13
|
type DropParams = {
|
|
13
14
|
editor: Editor;
|
|
14
15
|
item: unknown;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export type NodeWithId = CustomElement & {
|
|
18
|
+
export type NodeWithId = Omit<CustomElement, 'children'> & {
|
|
18
19
|
nodeId: string;
|
|
20
|
+
children: (NodeWithId | CustomText)[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const isNodeWithId = (node: unknown): node is NodeWithId =>
|
|
24
|
+
typeof node === 'object' && node !== null && 'nodeId' in node;
|
|
25
|
+
|
|
26
|
+
export type CustomDropHandler<T = unknown> = {
|
|
27
|
+
type: string | symbol;
|
|
28
|
+
onDrop: (context: CustomDropContext<T>) => DropResult;
|
|
19
29
|
};
|
|
20
30
|
|
|
21
31
|
export type Options = {
|
|
@@ -30,6 +40,7 @@ export type Options = {
|
|
|
30
40
|
position: 'top' | 'bottom' | null;
|
|
31
41
|
selected?: boolean;
|
|
32
42
|
onToggleSelected: () => void;
|
|
43
|
+
onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
33
44
|
}) => React.ReactNode;
|
|
34
45
|
ignoreNodes?: string[];
|
|
35
46
|
customTypes?: {
|
|
@@ -39,28 +50,26 @@ export type Options = {
|
|
|
39
50
|
};
|
|
40
51
|
};
|
|
41
52
|
customDropHandlers?: {
|
|
42
|
-
[type: string | symbol]:
|
|
43
|
-
type: string | symbol;
|
|
44
|
-
onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
|
|
45
|
-
};
|
|
53
|
+
[type: string | symbol]: CustomDropHandler;
|
|
46
54
|
};
|
|
55
|
+
nodeIdPlugin?: NodeIdPlugin;
|
|
47
56
|
};
|
|
48
57
|
|
|
49
|
-
type CustomDropContext<
|
|
58
|
+
export type CustomDropContext<TItem = unknown> = {
|
|
50
59
|
editor: Editor;
|
|
51
|
-
|
|
60
|
+
data: EditorOuterDragItem<TItem>;
|
|
52
61
|
insertAt: Path;
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
type DropResult = 'handled' | 'ignored';
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export type EditorDragItem = {
|
|
66
|
+
export type EditorInnerDragItem<TItem = unknown> = {
|
|
67
|
+
kind: 'inner';
|
|
61
68
|
nodes: NodeWithId[];
|
|
62
|
-
|
|
69
|
+
item?: TItem;
|
|
70
|
+
items?: TItem[];
|
|
63
71
|
source: {
|
|
72
|
+
editor: Editor;
|
|
64
73
|
/**
|
|
65
74
|
* Different editors have different unique identifiers.
|
|
66
75
|
*/
|
|
@@ -74,3 +83,16 @@ export type EditorDragItem = {
|
|
|
74
83
|
};
|
|
75
84
|
data?: Record<string, unknown>;
|
|
76
85
|
};
|
|
86
|
+
|
|
87
|
+
export type EditorOuterDragItem<TItem = unknown> = {
|
|
88
|
+
kind: 'outer';
|
|
89
|
+
item: TItem;
|
|
90
|
+
items: TItem[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* We're using this type to represent a drag item that is being dragged from the editor.
|
|
95
|
+
*/
|
|
96
|
+
export type EditorDragItem<TItem = unknown> =
|
|
97
|
+
| EditorInnerDragItem<TItem>
|
|
98
|
+
| EditorOuterDragItem<TItem>;
|
|
@@ -26,7 +26,11 @@ type Options = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
export class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
|
|
29
|
-
|
|
29
|
+
generateId: () => string;
|
|
30
|
+
|
|
31
|
+
constructor(private options: Options) {
|
|
32
|
+
this.generateId = options.generateId;
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
init(editor: Editor) {
|
|
32
36
|
const { apply, normalizeNode } = editor;
|
|
@@ -39,14 +43,13 @@ export class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
|
|
|
39
43
|
const newType = (operation.newProperties as CustomElement).type;
|
|
40
44
|
|
|
41
45
|
if (!node || (!Text.isText(node) && oldType !== newType)) {
|
|
42
|
-
(operation.newProperties as NodeIdBlock).nodeId =
|
|
43
|
-
this.options.generateId();
|
|
46
|
+
(operation.newProperties as NodeIdBlock).nodeId = this.generateId();
|
|
44
47
|
}
|
|
45
48
|
return apply(operation);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
if (operation.type === 'insert_node') {
|
|
49
|
-
assignId(operation.node, this.
|
|
52
|
+
assignId(operation.node, this.generateId);
|
|
50
53
|
return apply(operation);
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -54,8 +57,7 @@ export class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
|
|
|
54
57
|
const node = Node.get(editor, operation.path);
|
|
55
58
|
|
|
56
59
|
if (!node || !Text.isText(node)) {
|
|
57
|
-
(operation.properties as NodeIdBlock).nodeId =
|
|
58
|
-
this.options.generateId();
|
|
60
|
+
(operation.properties as NodeIdBlock).nodeId = this.generateId();
|
|
59
61
|
}
|
|
60
62
|
return apply(operation);
|
|
61
63
|
}
|
|
@@ -66,7 +68,7 @@ export class NodeIdPlugin implements IPlugin<Editor, NodeIdBlock> {
|
|
|
66
68
|
editor.normalizeNode = (entry: NodeEntry) => {
|
|
67
69
|
const [node] = entry;
|
|
68
70
|
|
|
69
|
-
assignId(node, this.
|
|
71
|
+
assignId(node, this.generateId);
|
|
70
72
|
|
|
71
73
|
normalizeNode(entry);
|
|
72
74
|
};
|