@use-kona/editor 0.1.24 → 0.1.25
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/createEditable.js +146 -15
- package/dist/core/createEditor.js +1 -1
- package/dist/core/styles.module.js +3 -1
- package/dist/core/styles_module.css +13 -0
- package/dist/elements/Block.js +25 -0
- package/dist/index.js +2 -1
- package/dist/plugins/CodeBlockPlugin/CodeBlockPlugin.js +1 -1
- package/dist/plugins/DnDPlugin/DnDPlugin.js +93 -45
- package/dist/plugins/DnDPlugin/types.js +2 -0
- package/dist/plugins/HeadingsPlugin/HeadingsPlugin.js +1 -1
- package/dist/plugins/ListsPlugin/ListsPlugin.js +1 -1
- package/dist/plugins/NodeIdPlugin/NodeIdPlugin.js +6 -4
- package/dist/provider.js +15 -0
- package/dist/src/core/createEditable.d.ts +1 -1
- package/dist/src/elements/Block.d.ts +2 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +0 -7
- package/dist/src/plugins/DnDPlugin/types.d.ts +29 -15
- package/dist/src/plugins/NodeIdPlugin/NodeIdPlugin.d.ts +1 -0
- package/dist/src/provider.d.ts +8 -0
- package/dist/src/types.d.ts +4 -0
- package/package.json +9 -8
- package/src/core/createEditable.tsx +170 -18
- package/src/core/createEditor.ts +1 -1
- package/src/core/styles.module.css +12 -0
- package/src/editor.tsx +4 -3
- package/src/elements/Block.tsx +31 -0
- package/src/index.ts +1 -0
- package/src/plugins/CodeBlockPlugin/CodeBlockPlugin.tsx +1 -3
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +147 -62
- package/src/plugins/DnDPlugin/types.ts +35 -20
- package/src/plugins/HeadingsPlugin/HeadingsPlugin.tsx +1 -2
- package/src/plugins/ListsPlugin/ListsPlugin.tsx +5 -5
- package/src/plugins/NodeIdPlugin/NodeIdPlugin.ts +9 -7
- package/src/provider.tsx +28 -0
- package/src/types.ts +5 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
-
import { nanoid } from 'nanoid';
|
|
3
|
-
import { type MapStore, map } from 'nanostores';
|
|
4
2
|
import { useEffect, useRef, useState } from 'react';
|
|
5
3
|
import { type ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
|
|
6
4
|
import { NativeTypes } from 'react-dnd-html5-backend';
|
|
7
|
-
import { Editor, Path, Transforms } from 'slate';
|
|
5
|
+
import { Editor, Element, Path, Transforms } from 'slate';
|
|
8
6
|
import {
|
|
9
7
|
ReactEditor,
|
|
10
8
|
type RenderElementProps,
|
|
@@ -12,44 +10,18 @@ import {
|
|
|
12
10
|
useSlate,
|
|
13
11
|
} from 'slate-react';
|
|
14
12
|
import type { CustomElement } from '../../../types';
|
|
13
|
+
import { useEditorContext } from '../../provider';
|
|
15
14
|
import type { IPlugin } from '../../types';
|
|
16
15
|
import type { EditorDragItem, NodeWithId, Options } from './types';
|
|
17
16
|
import { getNodesByNodeIds } from './utils';
|
|
18
17
|
|
|
19
18
|
export class DnDPlugin implements IPlugin {
|
|
20
|
-
|
|
21
|
-
editors: WeakMap<Editor, string>;
|
|
22
|
-
|
|
23
|
-
constructor(private options: Options) {
|
|
24
|
-
this.store = map({
|
|
25
|
-
selected: new Set(),
|
|
26
|
-
});
|
|
27
|
-
this.editors = new WeakMap();
|
|
28
|
-
}
|
|
19
|
+
constructor(private options: Options) {}
|
|
29
20
|
|
|
30
21
|
init(editor: Editor) {
|
|
31
|
-
this.editors.set(editor, nanoid());
|
|
32
|
-
|
|
33
22
|
return editor;
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
getEditorId(editor: Editor) {
|
|
37
|
-
return this.editors.get(editor);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
handleToggleSelected = (nodeId: string) => {
|
|
41
|
-
const selected = new Set(this.store.get().selected);
|
|
42
|
-
if (selected.has(nodeId)) {
|
|
43
|
-
selected.delete(nodeId);
|
|
44
|
-
} else {
|
|
45
|
-
selected.add(nodeId);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.store.set({
|
|
49
|
-
selected,
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
25
|
static DND_BLOCK_ELEMENT = 'block';
|
|
54
26
|
|
|
55
27
|
handlers = {
|
|
@@ -61,9 +33,12 @@ export class DnDPlugin implements IPlugin {
|
|
|
61
33
|
renderBlock = (props: RenderElementProps) => {
|
|
62
34
|
const editor = useSlate();
|
|
63
35
|
|
|
64
|
-
const
|
|
36
|
+
const { selectedNodes } = useEditorContext();
|
|
37
|
+
const $selectedNodes = useStore(selectedNodes);
|
|
65
38
|
|
|
39
|
+
const isCopying = useRef(false);
|
|
66
40
|
const options = this.options;
|
|
41
|
+
const generateId = this.options?.nodeIdPlugin?.generateId;
|
|
67
42
|
const isReadOnly = useReadOnly();
|
|
68
43
|
|
|
69
44
|
const [dropPosition, setDropPosition] = useState<'top' | 'bottom' | null>(
|
|
@@ -76,9 +51,9 @@ export class DnDPlugin implements IPlugin {
|
|
|
76
51
|
const currentElement = props.element as NodeWithId;
|
|
77
52
|
|
|
78
53
|
const [, drag, preview] = useDrag({
|
|
79
|
-
type: DnDPlugin.DND_BLOCK_ELEMENT,
|
|
54
|
+
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
80
55
|
item: () => {
|
|
81
|
-
const selected =
|
|
56
|
+
const selected: Array<NodeWithId> = $selectedNodes;
|
|
82
57
|
|
|
83
58
|
/**
|
|
84
59
|
* We copy nodes that the user is dragging.
|
|
@@ -86,10 +61,19 @@ export class DnDPlugin implements IPlugin {
|
|
|
86
61
|
* we copy selected nodes. Otherwise, we copy only the current node.
|
|
87
62
|
*/
|
|
88
63
|
const nodes =
|
|
89
|
-
selected.length > 0 &&
|
|
90
|
-
|
|
64
|
+
selected.length > 0 &&
|
|
65
|
+
selected.find((node) => node.nodeId === currentElement.nodeId)
|
|
66
|
+
? getNodesByNodeIds(
|
|
67
|
+
editor,
|
|
68
|
+
selected.map((node) => node.nodeId),
|
|
69
|
+
)
|
|
91
70
|
: [structuredClone(props.element as NodeWithId)];
|
|
92
71
|
|
|
72
|
+
const item =
|
|
73
|
+
this.options.customTypes?.[currentElement.type]?.getData?.(
|
|
74
|
+
currentElement,
|
|
75
|
+
);
|
|
76
|
+
|
|
93
77
|
const items = nodes
|
|
94
78
|
.map((node) => {
|
|
95
79
|
if (this.options.customTypes?.[node.type]) {
|
|
@@ -100,10 +84,12 @@ export class DnDPlugin implements IPlugin {
|
|
|
100
84
|
.filter(Boolean);
|
|
101
85
|
|
|
102
86
|
const dragItem: EditorDragItem = {
|
|
87
|
+
kind: 'inner',
|
|
103
88
|
nodes,
|
|
89
|
+
item,
|
|
104
90
|
items,
|
|
105
91
|
source: {
|
|
106
|
-
|
|
92
|
+
editor,
|
|
107
93
|
documentId: options.documentId,
|
|
108
94
|
},
|
|
109
95
|
};
|
|
@@ -112,7 +98,7 @@ export class DnDPlugin implements IPlugin {
|
|
|
112
98
|
},
|
|
113
99
|
canDrag: !isReadOnly,
|
|
114
100
|
end: () => {
|
|
115
|
-
|
|
101
|
+
selectedNodes.set([]);
|
|
116
102
|
},
|
|
117
103
|
});
|
|
118
104
|
|
|
@@ -120,7 +106,11 @@ export class DnDPlugin implements IPlugin {
|
|
|
120
106
|
(customType) => customType.type,
|
|
121
107
|
);
|
|
122
108
|
|
|
123
|
-
const [{ isOver }, drop] = useDrop
|
|
109
|
+
const [{ isOver }, drop] = useDrop<
|
|
110
|
+
EditorDragItem,
|
|
111
|
+
void,
|
|
112
|
+
{ isOver: boolean }
|
|
113
|
+
>({
|
|
124
114
|
accept: [
|
|
125
115
|
DnDPlugin.DND_BLOCK_ELEMENT,
|
|
126
116
|
NativeTypes.FILE,
|
|
@@ -151,8 +141,16 @@ export class DnDPlugin implements IPlugin {
|
|
|
151
141
|
setDropPosition('bottom');
|
|
152
142
|
}
|
|
153
143
|
},
|
|
154
|
-
drop(
|
|
144
|
+
drop: (data, monitor) => {
|
|
155
145
|
const itemType = monitor.getItemType();
|
|
146
|
+
const sourceEditor = data.kind === 'inner' ? data.source?.editor : null;
|
|
147
|
+
|
|
148
|
+
const isSameDocument =
|
|
149
|
+
data.kind === 'inner' &&
|
|
150
|
+
data.source?.documentId === this.options.documentId &&
|
|
151
|
+
this.options.documentId !== undefined;
|
|
152
|
+
const isSameEditor =
|
|
153
|
+
data.kind === 'inner' && data.source?.editor === editor;
|
|
156
154
|
|
|
157
155
|
const sourceTo = ReactEditor.findPath(editor, props.element);
|
|
158
156
|
|
|
@@ -169,8 +167,8 @@ export class DnDPlugin implements IPlugin {
|
|
|
169
167
|
const customDropHandler =
|
|
170
168
|
itemType && options.customDropHandlers?.[itemType];
|
|
171
169
|
|
|
172
|
-
if (customDropHandler) {
|
|
173
|
-
customDropHandler.onDrop({ editor,
|
|
170
|
+
if (customDropHandler && data.kind === 'outer') {
|
|
171
|
+
customDropHandler.onDrop({ editor, data, insertAt });
|
|
174
172
|
return;
|
|
175
173
|
}
|
|
176
174
|
|
|
@@ -178,12 +176,17 @@ export class DnDPlugin implements IPlugin {
|
|
|
178
176
|
case NativeTypes.FILE:
|
|
179
177
|
options.onDropFiles(
|
|
180
178
|
editor,
|
|
181
|
-
(
|
|
179
|
+
(data as unknown as { files: File[] }).files,
|
|
182
180
|
insertAt,
|
|
183
181
|
);
|
|
184
182
|
break;
|
|
185
183
|
default: {
|
|
186
|
-
const dragItem =
|
|
184
|
+
const dragItem = data as EditorDragItem;
|
|
185
|
+
|
|
186
|
+
// We don't know what "default" behavior for the outer items is
|
|
187
|
+
if (dragItem.kind === 'outer') {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
187
190
|
|
|
188
191
|
if (!dropPosition || !dragItem.nodes.length) {
|
|
189
192
|
return;
|
|
@@ -191,12 +194,35 @@ export class DnDPlugin implements IPlugin {
|
|
|
191
194
|
|
|
192
195
|
const nodeIds = dragItem.nodes.map((node) => node.nodeId);
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
if (isSameDocument || isSameEditor) {
|
|
198
|
+
if (!isCopying.current) {
|
|
199
|
+
moveNode(editor, sourceTo, nodeIds, dropPosition);
|
|
200
|
+
} else {
|
|
201
|
+
if (!generateId) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
206
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
if (!generateId) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const clonedNodes = cloneWithNewIds(dragItem.nodes, generateId);
|
|
214
|
+
|
|
215
|
+
insertNodes(editor, insertAt, clonedNodes);
|
|
216
|
+
if (!isCopying.current && sourceEditor) {
|
|
217
|
+
removeNodes(sourceEditor, nodeIds);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
selectedNodes.set([]);
|
|
196
221
|
break;
|
|
197
222
|
}
|
|
198
223
|
}
|
|
199
224
|
|
|
225
|
+
isCopying.current = false;
|
|
200
226
|
setDropPosition(null);
|
|
201
227
|
},
|
|
202
228
|
});
|
|
@@ -219,22 +245,32 @@ export class DnDPlugin implements IPlugin {
|
|
|
219
245
|
return props.children;
|
|
220
246
|
}
|
|
221
247
|
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
{options.renderBlock?.({
|
|
251
|
+
props,
|
|
252
|
+
dragRef: drag,
|
|
253
|
+
dropRef: connectDropRef,
|
|
254
|
+
previewRef: preview,
|
|
255
|
+
position: dropPosition,
|
|
256
|
+
onNativeDrop: (event) => {
|
|
257
|
+
isCopying.current = event.altKey;
|
|
258
|
+
},
|
|
259
|
+
onToggleSelected: (event) => {
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
if ($selectedNodes.includes(props.element)) {
|
|
262
|
+
selectedNodes.set(
|
|
263
|
+
[...$selectedNodes].filter(
|
|
264
|
+
(element) => element !== props.element,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
selectedNodes.set([...$selectedNodes, props.element]);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
})}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
238
274
|
};
|
|
239
275
|
}
|
|
240
276
|
|
|
@@ -289,3 +325,52 @@ const moveNode = (
|
|
|
289
325
|
});
|
|
290
326
|
Editor.normalize(editor);
|
|
291
327
|
};
|
|
328
|
+
|
|
329
|
+
const insertNodes = (editor: Editor, path: Path, nodes: NodeWithId[]) => {
|
|
330
|
+
Editor.withoutNormalizing(editor, () => {
|
|
331
|
+
Transforms.insertNodes(editor, nodes, { at: path });
|
|
332
|
+
});
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const removeNodes = (editor: Editor, nodeIds: string[]) => {
|
|
336
|
+
try {
|
|
337
|
+
Transforms.removeNodes(editor, {
|
|
338
|
+
at: [],
|
|
339
|
+
match: (n) => {
|
|
340
|
+
if (!Element.isElement(n)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!Editor.isBlock(editor, n)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const node = n as NodeWithId;
|
|
349
|
+
return typeof node.nodeId === 'string' && nodeIds.includes(node.nodeId);
|
|
350
|
+
},
|
|
351
|
+
mode: 'highest',
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const cloneWithNewIds = (
|
|
359
|
+
nodes: NodeWithId[],
|
|
360
|
+
generateId: () => string,
|
|
361
|
+
): NodeWithId[] => {
|
|
362
|
+
const copy = structuredClone(nodes);
|
|
363
|
+
|
|
364
|
+
const assignId = (node: NodeWithId): void => {
|
|
365
|
+
node.nodeId = generateId();
|
|
366
|
+
node.children.forEach((node) => {
|
|
367
|
+
if ('children' in node) {
|
|
368
|
+
assignId(node);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
copy.forEach(assignId);
|
|
374
|
+
|
|
375
|
+
return copy;
|
|
376
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type MapStore, map } from 'nanostores';
|
|
2
1
|
import type React from 'react';
|
|
3
2
|
import type {
|
|
4
3
|
ConnectDragPreview,
|
|
@@ -7,15 +6,20 @@ import type {
|
|
|
7
6
|
} from 'react-dnd';
|
|
8
7
|
import type { Editor, Path } from 'slate';
|
|
9
8
|
import type { RenderElementProps } from 'slate-react';
|
|
10
|
-
import type { CustomElement } from '../../../types';
|
|
9
|
+
import type { CustomElement, CustomText } from '../../../types';
|
|
10
|
+
import type { NodeIdPlugin } from '../NodeIdPlugin';
|
|
11
11
|
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
export type NodeWithId = Omit<CustomElement, 'children'> & {
|
|
13
|
+
nodeId: string;
|
|
14
|
+
children: (NodeWithId | CustomText)[];
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
nodeId
|
|
17
|
+
export const isNodeWithId = (node: unknown): node is NodeWithId =>
|
|
18
|
+
typeof node === 'object' && node !== null && 'nodeId' in node;
|
|
19
|
+
|
|
20
|
+
export type CustomDropHandler<T = unknown> = {
|
|
21
|
+
type: string | symbol;
|
|
22
|
+
onDrop: (context: CustomDropContext<T>) => DropResult;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
export type Options = {
|
|
@@ -28,8 +32,8 @@ export type Options = {
|
|
|
28
32
|
dropRef: ConnectDropTarget;
|
|
29
33
|
previewRef: ConnectDragPreview;
|
|
30
34
|
position: 'top' | 'bottom' | null;
|
|
31
|
-
|
|
32
|
-
onToggleSelected:
|
|
35
|
+
onNativeDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
36
|
+
onToggleSelected?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
33
37
|
}) => React.ReactNode;
|
|
34
38
|
ignoreNodes?: string[];
|
|
35
39
|
customTypes?: {
|
|
@@ -39,28 +43,26 @@ export type Options = {
|
|
|
39
43
|
};
|
|
40
44
|
};
|
|
41
45
|
customDropHandlers?: {
|
|
42
|
-
[type: string | symbol]:
|
|
43
|
-
type: string | symbol;
|
|
44
|
-
onDrop: <T = unknown>(context: CustomDropContext<T>) => DropResult;
|
|
45
|
-
};
|
|
46
|
+
[type: string | symbol]: CustomDropHandler;
|
|
46
47
|
};
|
|
48
|
+
nodeIdPlugin?: NodeIdPlugin;
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
type CustomDropContext<
|
|
51
|
+
export type CustomDropContext<TItem = unknown> = {
|
|
50
52
|
editor: Editor;
|
|
51
|
-
|
|
53
|
+
data: EditorOuterDragItem<TItem>;
|
|
52
54
|
insertAt: Path;
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
type DropResult = 'handled' | 'ignored';
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export type EditorDragItem = {
|
|
59
|
+
export type EditorInnerDragItem<TItem = unknown> = {
|
|
60
|
+
kind: 'inner';
|
|
61
61
|
nodes: NodeWithId[];
|
|
62
|
-
|
|
62
|
+
item?: TItem;
|
|
63
|
+
items?: TItem[];
|
|
63
64
|
source: {
|
|
65
|
+
editor: Editor;
|
|
64
66
|
/**
|
|
65
67
|
* Different editors have different unique identifiers.
|
|
66
68
|
*/
|
|
@@ -74,3 +76,16 @@ export type EditorDragItem = {
|
|
|
74
76
|
};
|
|
75
77
|
data?: Record<string, unknown>;
|
|
76
78
|
};
|
|
79
|
+
|
|
80
|
+
export type EditorOuterDragItem<TItem = unknown> = {
|
|
81
|
+
kind: 'outer';
|
|
82
|
+
item: TItem;
|
|
83
|
+
items: TItem[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* We're using this type to represent a drag item that is being dragged from the editor.
|
|
88
|
+
*/
|
|
89
|
+
export type EditorDragItem<TItem = unknown> =
|
|
90
|
+
| EditorInnerDragItem<TItem>
|
|
91
|
+
| EditorOuterDragItem<TItem>;
|
|
@@ -124,8 +124,7 @@ const isBlockActive = (editor, type) => {
|
|
|
124
124
|
const [match] = Array.from(
|
|
125
125
|
Editor.nodes(editor, {
|
|
126
126
|
at: Editor.unhangRange(editor, selection),
|
|
127
|
-
match: (node) =>
|
|
128
|
-
!Editor.isEditor(node) && Element.isElement(node) && node.type === type,
|
|
127
|
+
match: (node) => Element.isElement(node) && node.type === type,
|
|
129
128
|
}),
|
|
130
129
|
);
|
|
131
130
|
|
|
@@ -96,7 +96,10 @@ export class ListsPlugin implements IPlugin {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/* Merge adjacent lists */
|
|
99
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
this.isList(editor, node as CustomElement) &&
|
|
101
|
+
Path.hasPrevious(path)
|
|
102
|
+
) {
|
|
100
103
|
const prevPath = Path.previous(path);
|
|
101
104
|
const prevNode = Node.get(editor, prevPath) as CustomElement;
|
|
102
105
|
|
|
@@ -331,10 +334,7 @@ export class ListsPlugin implements IPlugin {
|
|
|
331
334
|
const [match] = Array.from(
|
|
332
335
|
Editor.nodes(editor, {
|
|
333
336
|
at: Editor.unhangRange(editor, selection),
|
|
334
|
-
match: (node) =>
|
|
335
|
-
!Editor.isEditor(node) &&
|
|
336
|
-
Element.isElement(node) &&
|
|
337
|
-
node.type === type,
|
|
337
|
+
match: (node) => Element.isElement(node) && node.type === type,
|
|
338
338
|
}),
|
|
339
339
|
);
|
|
340
340
|
|
|
@@ -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
|
};
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import type { EditorContext } from './types';
|
|
5
|
+
|
|
6
|
+
const Context = createContext<EditorContext | null>(null);
|
|
7
|
+
|
|
8
|
+
export const EditorProvider = ({
|
|
9
|
+
children,
|
|
10
|
+
selectedNodes,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
selectedNodes: StoreValue<string[]>;
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<Context.Provider value={{ selectedNodes }}>{children}</Context.Provider>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const useEditorContext = () => {
|
|
21
|
+
const context = useContext(Context);
|
|
22
|
+
|
|
23
|
+
if (!context) {
|
|
24
|
+
throw new Error('useEditorContext must be used within an EditorProvider');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return context;
|
|
28
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StoreValue } from 'nanostores';
|
|
1
2
|
import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
DecoratedRange,
|
|
@@ -98,3 +99,7 @@ export type Deserialize = (
|
|
|
98
99
|
element: HTMLElement,
|
|
99
100
|
children?: (string | Descendant)[],
|
|
100
101
|
) => CustomElement | CustomText[] | undefined;
|
|
102
|
+
|
|
103
|
+
export type EditorContext = {
|
|
104
|
+
selectedNodes: StoreValue<string[]>;
|
|
105
|
+
};
|