@use-kona/editor 0.1.22 → 0.1.24-dnd.1
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/core/createEditor.js +1 -1
- package/dist/plugins/DnDPlugin/DnDPlugin.js +62 -21
- package/dist/plugins/DnDPlugin/types.js +0 -0
- package/dist/plugins/DnDPlugin/utils.js +24 -0
- package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +8 -26
- package/dist/src/plugins/DnDPlugin/types.d.ts +62 -0
- package/dist/src/plugins/DnDPlugin/utils.d.ts +8 -0
- package/package.json +2 -1
- package/src/core/createEditor.ts +2 -8
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +87 -60
- package/src/plugins/DnDPlugin/types.ts +76 -0
- package/src/plugins/DnDPlugin/utils.ts +52 -0
|
@@ -8,7 +8,7 @@ const createEditor_createEditor = (plugins)=>()=>{
|
|
|
8
8
|
if (plugin.init) return plugin.init(editor);
|
|
9
9
|
return editor;
|
|
10
10
|
}, baseEditor);
|
|
11
|
-
const { isInline, isVoid,
|
|
11
|
+
const { isInline, isVoid, deleteFragment, deleteBackward, deleteForward } = editorWithPlugins;
|
|
12
12
|
editorWithPlugins.getCommands = (plugin)=>{
|
|
13
13
|
const pluginInstance = pluginsMap.get(plugin);
|
|
14
14
|
if (!pluginInstance) throw new Error(`Plugin ${plugin.name} not found`);
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { useStore } from "@nanostores/react";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
2
3
|
import { map } from "nanostores";
|
|
3
4
|
import { useEffect, useRef, useState } from "react";
|
|
4
5
|
import { useDrag, useDrop } from "react-dnd";
|
|
5
6
|
import { NativeTypes } from "react-dnd-html5-backend";
|
|
6
|
-
import { Editor, Transforms } from "slate";
|
|
7
|
+
import { Editor, Path, Transforms } from "slate";
|
|
7
8
|
import { ReactEditor, useReadOnly, useSlate } from "slate-react";
|
|
9
|
+
import { getNodesByNodeIds } from "./utils.js";
|
|
8
10
|
class DnDPlugin {
|
|
9
11
|
options;
|
|
10
12
|
store;
|
|
13
|
+
editors;
|
|
11
14
|
constructor(options){
|
|
12
15
|
this.options = options;
|
|
13
16
|
this.store = map({
|
|
14
17
|
selected: new Set()
|
|
15
18
|
});
|
|
19
|
+
this.editors = new WeakMap();
|
|
20
|
+
}
|
|
21
|
+
init(editor) {
|
|
22
|
+
this.editors.set(editor, nanoid());
|
|
23
|
+
return editor;
|
|
24
|
+
}
|
|
25
|
+
getEditorId(editor) {
|
|
26
|
+
return this.editors.get(editor);
|
|
16
27
|
}
|
|
17
28
|
handleToggleSelected = (nodeId)=>{
|
|
18
29
|
const selected = new Set(this.store.get().selected);
|
|
@@ -35,20 +46,33 @@ class DnDPlugin {
|
|
|
35
46
|
const isReadOnly = useReadOnly();
|
|
36
47
|
const [dropPosition, setDropPosition] = useState(null);
|
|
37
48
|
const dropTargetRef = useRef(null);
|
|
38
|
-
|
|
49
|
+
options.customTypes?.[props.element.type];
|
|
39
50
|
const currentElement = props.element;
|
|
40
|
-
const selected = Array.from($store.selected.values());
|
|
41
51
|
const [, drag, preview] = useDrag({
|
|
42
|
-
type:
|
|
43
|
-
item: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
type: DnDPlugin.DND_BLOCK_ELEMENT,
|
|
53
|
+
item: ()=>{
|
|
54
|
+
const selected = Array.from($store.selected.values());
|
|
55
|
+
const nodes = selected.length > 0 && selected.includes(currentElement.nodeId) ? getNodesByNodeIds(editor, selected) : [
|
|
56
|
+
structuredClone(props.element)
|
|
57
|
+
];
|
|
58
|
+
const items = nodes.map((node)=>{
|
|
59
|
+
if (this.options.customTypes?.[node.type]) return this.options.customTypes[node.type].getData?.(node);
|
|
60
|
+
return null;
|
|
61
|
+
}).filter(Boolean);
|
|
62
|
+
const dragItem = {
|
|
63
|
+
nodes,
|
|
64
|
+
items,
|
|
65
|
+
source: {
|
|
66
|
+
editorId: this.getEditorId(editor),
|
|
67
|
+
documentId: options.documentId
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return dragItem;
|
|
49
71
|
},
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
canDrag: !isReadOnly,
|
|
73
|
+
end: ()=>{
|
|
74
|
+
$store.selected.clear();
|
|
75
|
+
}
|
|
52
76
|
});
|
|
53
77
|
const allCustomTypes = Object.values(options.customTypes || {}).map((customType)=>customType.type);
|
|
54
78
|
const [{ isOver }, drop] = useDrop({
|
|
@@ -78,17 +102,29 @@ class DnDPlugin {
|
|
|
78
102
|
const itemType = monitor.getItemType();
|
|
79
103
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
80
104
|
if (!sourceTo) return;
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
105
|
+
const insertAt = getDropPath(editor, sourceTo, dropPosition || 'bottom');
|
|
106
|
+
if (!insertAt) return;
|
|
107
|
+
const customDropHandler = itemType && options.customDropHandlers?.[itemType];
|
|
108
|
+
if (customDropHandler) {
|
|
109
|
+
console.log('customDropHandler.onDrop', itemType);
|
|
110
|
+
customDropHandler.onDrop({
|
|
111
|
+
editor,
|
|
112
|
+
item,
|
|
113
|
+
insertAt
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log('customDropHandler not found', itemType);
|
|
83
118
|
switch(itemType){
|
|
84
119
|
case NativeTypes.FILE:
|
|
85
|
-
options.onDropFiles(editor, item.files,
|
|
120
|
+
options.onDropFiles(editor, item.files, insertAt);
|
|
86
121
|
break;
|
|
87
122
|
default:
|
|
88
123
|
{
|
|
89
124
|
const dragItem = item;
|
|
90
|
-
if (!dropPosition || !dragItem.
|
|
91
|
-
|
|
125
|
+
if (!dropPosition || !dragItem.nodes.length) return;
|
|
126
|
+
const nodeIds = dragItem.nodes.map((node)=>node.nodeId);
|
|
127
|
+
moveNode(editor, sourceTo, nodeIds, dropPosition);
|
|
92
128
|
$store.selected.clear();
|
|
93
129
|
break;
|
|
94
130
|
}
|
|
@@ -120,12 +156,17 @@ class DnDPlugin {
|
|
|
120
156
|
});
|
|
121
157
|
};
|
|
122
158
|
}
|
|
123
|
-
const getDropPath = (editor, targetNodePath)=>{
|
|
159
|
+
const getDropPath = (editor, targetNodePath, position)=>{
|
|
124
160
|
const target = Editor.node(editor, targetNodePath);
|
|
125
|
-
|
|
161
|
+
switch(position){
|
|
162
|
+
case 'top':
|
|
163
|
+
return target[1];
|
|
164
|
+
case 'bottom':
|
|
165
|
+
return Path.next(target[1]);
|
|
166
|
+
}
|
|
126
167
|
};
|
|
127
|
-
const moveNode = (editor, targetNodePath, nodeIds)=>{
|
|
128
|
-
const dropPath = getDropPath(editor, targetNodePath);
|
|
168
|
+
const moveNode = (editor, targetNodePath, nodeIds, position)=>{
|
|
169
|
+
const dropPath = getDropPath(editor, targetNodePath, position);
|
|
129
170
|
if (!dropPath) return;
|
|
130
171
|
if (!Editor.hasPath(editor, dropPath)) return;
|
|
131
172
|
Editor.withoutNormalizing(editor, ()=>{
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Editor, Element } from "slate";
|
|
2
|
+
const cloneSlateNode = (node)=>structuredClone(node);
|
|
3
|
+
const getNodesByNodeIds = (editor, nodeIds)=>{
|
|
4
|
+
if (!nodeIds.length) return [];
|
|
5
|
+
const ids = new Set(nodeIds);
|
|
6
|
+
const nodes = [];
|
|
7
|
+
const entries = Editor.nodes(editor, {
|
|
8
|
+
at: [],
|
|
9
|
+
match: (n)=>{
|
|
10
|
+
if (!Element.isElement(n)) return false;
|
|
11
|
+
if (!Editor.isBlock(editor, n)) return false;
|
|
12
|
+
const nodeId = n.nodeId;
|
|
13
|
+
return 'string' == typeof nodeId && ids.has(nodeId);
|
|
14
|
+
},
|
|
15
|
+
mode: 'highest'
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
for (const entry of entries)nodes.push(entry);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return nodes.map(([node])=>cloneSlateNode(node));
|
|
23
|
+
};
|
|
24
|
+
export { getNodesByNodeIds };
|
|
@@ -1,36 +1,19 @@
|
|
|
1
1
|
import { type MapStore } from 'nanostores';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { Editor, Path } from 'slate';
|
|
5
|
-
import { type RenderElementProps } from 'slate-react';
|
|
6
|
-
import type { CustomElement } from '../../../types';
|
|
2
|
+
import { Editor } from 'slate';
|
|
3
|
+
import { ReactEditor, type RenderElementProps } from 'slate-react';
|
|
7
4
|
import type { IPlugin } from '../../types';
|
|
8
|
-
type Options
|
|
9
|
-
onDropFiles: (editor: Editor, files: File[], path: Path) => void;
|
|
10
|
-
renderBlock?: (params: {
|
|
11
|
-
props: RenderElementProps;
|
|
12
|
-
dragRef: ConnectDragSource;
|
|
13
|
-
dropRef: ConnectDropTarget;
|
|
14
|
-
previewRef: ConnectDragPreview;
|
|
15
|
-
position: 'top' | 'bottom' | null;
|
|
16
|
-
selected?: boolean;
|
|
17
|
-
onToggleSelected: () => void;
|
|
18
|
-
}) => React.ReactNode;
|
|
19
|
-
ignoreNodes?: string[];
|
|
20
|
-
customTypes?: {
|
|
21
|
-
[type: string]: {
|
|
22
|
-
type: string | symbol;
|
|
23
|
-
getData?: (element: CustomElement) => Record<string, unknown>;
|
|
24
|
-
getDndItem?: (element: CustomElement) => Record<string, unknown>;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
};
|
|
5
|
+
import type { Options } from './types';
|
|
28
6
|
export declare class DnDPlugin implements IPlugin {
|
|
29
7
|
private options;
|
|
30
8
|
store: MapStore<{
|
|
31
9
|
selected: Set<string>;
|
|
32
10
|
}>;
|
|
11
|
+
editors: WeakMap<Editor, string>;
|
|
33
12
|
constructor(options: Options);
|
|
13
|
+
init(editor: Editor): import("slate").BaseEditor & ReactEditor & {
|
|
14
|
+
getCommands: <T extends IPlugin>(plugin: new () => T) => T["commands"];
|
|
15
|
+
};
|
|
16
|
+
getEditorId(editor: Editor): string | undefined;
|
|
34
17
|
handleToggleSelected: (nodeId: string) => void;
|
|
35
18
|
static DND_BLOCK_ELEMENT: string;
|
|
36
19
|
handlers: {
|
|
@@ -38,4 +21,3 @@ export declare class DnDPlugin implements IPlugin {
|
|
|
38
21
|
};
|
|
39
22
|
renderBlock: (props: RenderElementProps) => any;
|
|
40
23
|
}
|
|
41
|
-
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { ConnectDragPreview, ConnectDragSource, ConnectDropTarget } from 'react-dnd';
|
|
3
|
+
import type { Editor, Path } from 'slate';
|
|
4
|
+
import type { RenderElementProps } from 'slate-react';
|
|
5
|
+
import type { CustomElement } from '../../../types';
|
|
6
|
+
export type NodeWithId = CustomElement & {
|
|
7
|
+
nodeId: string;
|
|
8
|
+
};
|
|
9
|
+
export type Options = {
|
|
10
|
+
editorId?: string;
|
|
11
|
+
documentId?: string | null;
|
|
12
|
+
onDropFiles: (editor: Editor, files: File[], path: Path) => void;
|
|
13
|
+
renderBlock?: (params: {
|
|
14
|
+
props: RenderElementProps;
|
|
15
|
+
dragRef: ConnectDragSource;
|
|
16
|
+
dropRef: ConnectDropTarget;
|
|
17
|
+
previewRef: ConnectDragPreview;
|
|
18
|
+
position: 'top' | 'bottom' | null;
|
|
19
|
+
selected?: boolean;
|
|
20
|
+
onToggleSelected: () => void;
|
|
21
|
+
}) => React.ReactNode;
|
|
22
|
+
ignoreNodes?: string[];
|
|
23
|
+
customTypes?: {
|
|
24
|
+
[type: string]: {
|
|
25
|
+
type: string | symbol;
|
|
26
|
+
getData?: (element: CustomElement) => unknown;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
customDropHandlers?: {
|
|
30
|
+
[type: string | symbol]: {
|
|
31
|
+
type: string | symbol;
|
|
32
|
+
onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type CustomDropContext<T = unknown> = {
|
|
37
|
+
editor: Editor;
|
|
38
|
+
item: T;
|
|
39
|
+
insertAt: Path;
|
|
40
|
+
};
|
|
41
|
+
type DropResult = 'handled' | 'ignored';
|
|
42
|
+
/**
|
|
43
|
+
* We're using this type to represent a drag item that is being dragged from the editor.
|
|
44
|
+
*/
|
|
45
|
+
export type EditorDragItem = {
|
|
46
|
+
nodes: NodeWithId[];
|
|
47
|
+
items?: unknown[];
|
|
48
|
+
source: {
|
|
49
|
+
/**
|
|
50
|
+
* Different editors have different unique identifiers.
|
|
51
|
+
*/
|
|
52
|
+
editorId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* But different editors may have the same document opened. In this case,
|
|
55
|
+
* editor1.documentId will be the same as editor2.documentId.
|
|
56
|
+
*/
|
|
57
|
+
documentId?: string | null;
|
|
58
|
+
path?: Path;
|
|
59
|
+
};
|
|
60
|
+
data?: Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-kona/editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24-dnd.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"escape-html": "^1.0.3",
|
|
70
70
|
"is-hotkey": "^0.2.0",
|
|
71
71
|
"is-url": "^1.2.4",
|
|
72
|
+
"nanoid": "^5.1.16",
|
|
72
73
|
"nanostores": "^1.0.1",
|
|
73
74
|
"prismjs": "^1.30.0"
|
|
74
75
|
}
|
package/src/core/createEditor.ts
CHANGED
|
@@ -24,14 +24,8 @@ export const createEditor = (plugins: IPlugin[]) => () => {
|
|
|
24
24
|
return editor;
|
|
25
25
|
}, baseEditor);
|
|
26
26
|
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
isVoid,
|
|
30
|
-
normalizeNode,
|
|
31
|
-
deleteFragment,
|
|
32
|
-
deleteBackward,
|
|
33
|
-
deleteForward,
|
|
34
|
-
} = editorWithPlugins;
|
|
27
|
+
const { isInline, isVoid, deleteFragment, deleteBackward, deleteForward } =
|
|
28
|
+
editorWithPlugins;
|
|
35
29
|
|
|
36
30
|
editorWithPlugins.getCommands = (plugin: new () => IPlugin) => {
|
|
37
31
|
const pluginInstance = pluginsMap.get(plugin);
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
2
3
|
import { type MapStore, map } from 'nanostores';
|
|
3
|
-
import type React from 'react';
|
|
4
4
|
import { useEffect, useRef, useState } from 'react';
|
|
5
|
-
import {
|
|
6
|
-
type ConnectDragPreview,
|
|
7
|
-
type ConnectDragSource,
|
|
8
|
-
type ConnectDropTarget,
|
|
9
|
-
useDrag,
|
|
10
|
-
useDrop,
|
|
11
|
-
} from 'react-dnd';
|
|
5
|
+
import { type ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
|
|
12
6
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
|
13
7
|
import { Editor, Path, Transforms } from 'slate';
|
|
14
8
|
import {
|
|
@@ -19,44 +13,28 @@ import {
|
|
|
19
13
|
} from 'slate-react';
|
|
20
14
|
import type { CustomElement } from '../../../types';
|
|
21
15
|
import type { IPlugin } from '../../types';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
onDropFiles: (editor: Editor, files: File[], path: Path) => void;
|
|
25
|
-
renderBlock?: (params: {
|
|
26
|
-
props: RenderElementProps;
|
|
27
|
-
dragRef: ConnectDragSource;
|
|
28
|
-
dropRef: ConnectDropTarget;
|
|
29
|
-
previewRef: ConnectDragPreview;
|
|
30
|
-
position: 'top' | 'bottom' | null;
|
|
31
|
-
selected?: boolean;
|
|
32
|
-
onToggleSelected: () => void;
|
|
33
|
-
}) => React.ReactNode;
|
|
34
|
-
ignoreNodes?: string[];
|
|
35
|
-
customTypes?: {
|
|
36
|
-
[type: string]: {
|
|
37
|
-
type: string | symbol;
|
|
38
|
-
getData?: (element: CustomElement) => Record<string, unknown>;
|
|
39
|
-
getDndItem?: (element: CustomElement) => Record<string, unknown>;
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
type DnDItem = {
|
|
45
|
-
element: CustomElement;
|
|
46
|
-
nodeIds?: string[];
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type DnDNode = CustomElement & {
|
|
50
|
-
nodeId?: string;
|
|
51
|
-
};
|
|
16
|
+
import type { EditorDragItem, NodeWithId, Options } from './types';
|
|
17
|
+
import { getNodesByNodeIds } from './utils';
|
|
52
18
|
|
|
53
19
|
export class DnDPlugin implements IPlugin {
|
|
54
20
|
store: MapStore<{ selected: Set<string> }>;
|
|
21
|
+
editors: WeakMap<Editor, string>;
|
|
55
22
|
|
|
56
23
|
constructor(private options: Options) {
|
|
57
24
|
this.store = map({
|
|
58
25
|
selected: new Set(),
|
|
59
26
|
});
|
|
27
|
+
this.editors = new WeakMap();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
init(editor: Editor) {
|
|
31
|
+
this.editors.set(editor, nanoid());
|
|
32
|
+
|
|
33
|
+
return editor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getEditorId(editor: Editor) {
|
|
37
|
+
return this.editors.get(editor);
|
|
60
38
|
}
|
|
61
39
|
|
|
62
40
|
handleToggleSelected = (nodeId: string) => {
|
|
@@ -95,21 +73,47 @@ export class DnDPlugin implements IPlugin {
|
|
|
95
73
|
const dropTargetRef = useRef<HTMLElement | null>(null);
|
|
96
74
|
|
|
97
75
|
const customType = options.customTypes?.[props.element.type];
|
|
98
|
-
const currentElement = props.element as
|
|
99
|
-
|
|
100
|
-
const selected = Array.from($store.selected.values());
|
|
76
|
+
const currentElement = props.element as NodeWithId;
|
|
101
77
|
|
|
102
78
|
const [, drag, preview] = useDrag({
|
|
103
|
-
type:
|
|
104
|
-
item: {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
type: DnDPlugin.DND_BLOCK_ELEMENT,
|
|
80
|
+
item: () => {
|
|
81
|
+
const selected = Array.from($store.selected.values());
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* We copy nodes that the user is dragging.
|
|
85
|
+
* If a user drags multiple nodes, and the current node is one of them,
|
|
86
|
+
* we copy selected nodes. Otherwise, we copy only the current node.
|
|
87
|
+
*/
|
|
88
|
+
const nodes =
|
|
89
|
+
selected.length > 0 && selected.includes(currentElement.nodeId)
|
|
90
|
+
? getNodesByNodeIds(editor, selected)
|
|
91
|
+
: [structuredClone(props.element as NodeWithId)];
|
|
92
|
+
|
|
93
|
+
const items = nodes
|
|
94
|
+
.map((node) => {
|
|
95
|
+
if (this.options.customTypes?.[node.type]) {
|
|
96
|
+
return this.options.customTypes[node.type].getData?.(node);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
|
|
102
|
+
const dragItem: EditorDragItem = {
|
|
103
|
+
nodes,
|
|
104
|
+
items,
|
|
105
|
+
source: {
|
|
106
|
+
editorId: this.getEditorId(editor),
|
|
107
|
+
documentId: options.documentId,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return dragItem;
|
|
110
112
|
},
|
|
111
|
-
...(customType?.getDndItem?.(props.element) || {}),
|
|
112
113
|
canDrag: !isReadOnly,
|
|
114
|
+
end: () => {
|
|
115
|
+
$store.selected.clear();
|
|
116
|
+
},
|
|
113
117
|
});
|
|
114
118
|
|
|
115
119
|
const allCustomTypes = Object.values(options.customTypes || {}).map(
|
|
@@ -154,26 +158,43 @@ export class DnDPlugin implements IPlugin {
|
|
|
154
158
|
|
|
155
159
|
if (!sourceTo) return;
|
|
156
160
|
|
|
157
|
-
const
|
|
161
|
+
const insertAt = getDropPath(
|
|
162
|
+
editor,
|
|
163
|
+
sourceTo,
|
|
164
|
+
dropPosition || 'bottom',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!insertAt) return;
|
|
158
168
|
|
|
159
|
-
|
|
169
|
+
const customDropHandler =
|
|
170
|
+
itemType && options.customDropHandlers?.[itemType];
|
|
171
|
+
|
|
172
|
+
if (customDropHandler) {
|
|
173
|
+
console.log('customDropHandler.onDrop', itemType);
|
|
174
|
+
customDropHandler.onDrop({ editor, item, insertAt });
|
|
175
|
+
return;
|
|
176
|
+
} else {
|
|
177
|
+
console.log('customDropHandler not found', itemType);
|
|
178
|
+
}
|
|
160
179
|
|
|
161
180
|
switch (itemType) {
|
|
162
181
|
case NativeTypes.FILE:
|
|
163
182
|
options.onDropFiles(
|
|
164
183
|
editor,
|
|
165
184
|
(item as { files: File[] }).files,
|
|
166
|
-
|
|
185
|
+
insertAt,
|
|
167
186
|
);
|
|
168
187
|
break;
|
|
169
188
|
default: {
|
|
170
|
-
const dragItem = item as
|
|
189
|
+
const dragItem = item as EditorDragItem;
|
|
171
190
|
|
|
172
|
-
if (!dropPosition || !dragItem.
|
|
191
|
+
if (!dropPosition || !dragItem.nodes.length) {
|
|
173
192
|
return;
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
const nodeIds = dragItem.nodes.map((node) => node.nodeId);
|
|
196
|
+
|
|
197
|
+
moveNode(editor, sourceTo, nodeIds, dropPosition);
|
|
177
198
|
$store.selected.clear();
|
|
178
199
|
break;
|
|
179
200
|
}
|
|
@@ -223,22 +244,29 @@ export class DnDPlugin implements IPlugin {
|
|
|
223
244
|
const getDropPath = (
|
|
224
245
|
editor: Editor,
|
|
225
246
|
targetNodePath: Path,
|
|
247
|
+
position: 'top' | 'bottom',
|
|
226
248
|
) => {
|
|
227
249
|
const target = Editor.node(editor, targetNodePath);
|
|
228
250
|
|
|
229
|
-
|
|
251
|
+
switch (position) {
|
|
252
|
+
case 'top':
|
|
253
|
+
return target[1];
|
|
254
|
+
case 'bottom':
|
|
255
|
+
return Path.next(target[1]);
|
|
256
|
+
}
|
|
230
257
|
};
|
|
231
258
|
|
|
232
259
|
const moveNode = (
|
|
233
260
|
editor: Editor,
|
|
234
261
|
targetNodePath: Path,
|
|
235
262
|
nodeIds: string[],
|
|
263
|
+
position: 'top' | 'bottom',
|
|
236
264
|
) => {
|
|
237
|
-
const dropPath = getDropPath(editor, targetNodePath);
|
|
265
|
+
const dropPath = getDropPath(editor, targetNodePath, position);
|
|
238
266
|
|
|
239
267
|
if (!dropPath) return;
|
|
240
268
|
if (!Editor.hasPath(editor, dropPath)) {
|
|
241
|
-
return
|
|
269
|
+
return;
|
|
242
270
|
}
|
|
243
271
|
|
|
244
272
|
Editor.withoutNormalizing(editor, () => {
|
|
@@ -253,7 +281,7 @@ const moveNode = (
|
|
|
253
281
|
return false;
|
|
254
282
|
}
|
|
255
283
|
|
|
256
|
-
const node = n as
|
|
284
|
+
const node = n as NodeWithId;
|
|
257
285
|
const shouldMove =
|
|
258
286
|
typeof node.nodeId === 'string' && nodeIds.includes(node.nodeId);
|
|
259
287
|
return shouldMove;
|
|
@@ -263,5 +291,4 @@ const moveNode = (
|
|
|
263
291
|
});
|
|
264
292
|
});
|
|
265
293
|
Editor.normalize(editor);
|
|
266
|
-
|
|
267
294
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type MapStore, map } from 'nanostores';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import type {
|
|
4
|
+
ConnectDragPreview,
|
|
5
|
+
ConnectDragSource,
|
|
6
|
+
ConnectDropTarget,
|
|
7
|
+
} from 'react-dnd';
|
|
8
|
+
import type { Editor, Path } from 'slate';
|
|
9
|
+
import type { RenderElementProps } from 'slate-react';
|
|
10
|
+
import type { CustomElement } from '../../../types';
|
|
11
|
+
|
|
12
|
+
type DropParams = {
|
|
13
|
+
editor: Editor;
|
|
14
|
+
item: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type NodeWithId = CustomElement & {
|
|
18
|
+
nodeId: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Options = {
|
|
22
|
+
editorId?: string;
|
|
23
|
+
documentId?: string | null;
|
|
24
|
+
onDropFiles: (editor: Editor, files: File[], path: Path) => void;
|
|
25
|
+
renderBlock?: (params: {
|
|
26
|
+
props: RenderElementProps;
|
|
27
|
+
dragRef: ConnectDragSource;
|
|
28
|
+
dropRef: ConnectDropTarget;
|
|
29
|
+
previewRef: ConnectDragPreview;
|
|
30
|
+
position: 'top' | 'bottom' | null;
|
|
31
|
+
selected?: boolean;
|
|
32
|
+
onToggleSelected: () => void;
|
|
33
|
+
}) => React.ReactNode;
|
|
34
|
+
ignoreNodes?: string[];
|
|
35
|
+
customTypes?: {
|
|
36
|
+
[type: string]: {
|
|
37
|
+
type: string | symbol;
|
|
38
|
+
getData?: (element: CustomElement) => unknown;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
customDropHandlers?: {
|
|
42
|
+
[type: string | symbol]: {
|
|
43
|
+
type: string | symbol;
|
|
44
|
+
onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type CustomDropContext<T = unknown> = {
|
|
50
|
+
editor: Editor;
|
|
51
|
+
item: T;
|
|
52
|
+
insertAt: Path;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type DropResult = 'handled' | 'ignored';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* We're using this type to represent a drag item that is being dragged from the editor.
|
|
59
|
+
*/
|
|
60
|
+
export type EditorDragItem = {
|
|
61
|
+
nodes: NodeWithId[];
|
|
62
|
+
items?: unknown[];
|
|
63
|
+
source: {
|
|
64
|
+
/**
|
|
65
|
+
* Different editors have different unique identifiers.
|
|
66
|
+
*/
|
|
67
|
+
editorId?: string;
|
|
68
|
+
/**
|
|
69
|
+
* But different editors may have the same document opened. In this case,
|
|
70
|
+
* editor1.documentId will be the same as editor2.documentId.
|
|
71
|
+
*/
|
|
72
|
+
documentId?: string | null;
|
|
73
|
+
path?: Path;
|
|
74
|
+
};
|
|
75
|
+
data?: Record<string, unknown>;
|
|
76
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Editor, Element, type Path } from 'slate';
|
|
2
|
+
import type { NodeWithId } from './types';
|
|
3
|
+
|
|
4
|
+
type NodeWithIdEntry = [NodeWithId, Path];
|
|
5
|
+
|
|
6
|
+
const cloneSlateNode = (node: NodeWithId): NodeWithId => {
|
|
7
|
+
return structuredClone(node);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets a copy of listed notes by their ids
|
|
12
|
+
* @param editor
|
|
13
|
+
* @param nodeIds
|
|
14
|
+
*/
|
|
15
|
+
export const getNodesByNodeIds = (
|
|
16
|
+
editor: Editor,
|
|
17
|
+
nodeIds: string[],
|
|
18
|
+
): NodeWithId[] => {
|
|
19
|
+
if (!nodeIds.length) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ids = new Set(nodeIds);
|
|
24
|
+
const nodes: NodeWithIdEntry[] = [];
|
|
25
|
+
|
|
26
|
+
const entries = Editor.nodes(editor, {
|
|
27
|
+
at: [],
|
|
28
|
+
match: (n) => {
|
|
29
|
+
if (!Element.isElement(n)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!Editor.isBlock(editor, n)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nodeId = (n as NodeWithId).nodeId;
|
|
38
|
+
return typeof nodeId === 'string' && ids.has(nodeId);
|
|
39
|
+
},
|
|
40
|
+
mode: 'highest',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
nodes.push(entry as NodeWithIdEntry);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return nodes.map(([node]) => cloneSlateNode(node));
|
|
52
|
+
};
|