@use-kona/editor 0.1.28 → 0.1.30
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 +4 -1
- package/dist/plugins/CodeBlockPlugin/styles_module.css +1 -0
- package/dist/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.js +121 -0
- package/dist/plugins/CollapsibleBlocksPlugin/index.js +2 -0
- package/dist/plugins/CollapsibleBlocksPlugin/styles.module.js +8 -0
- package/dist/plugins/CollapsibleBlocksPlugin/styles_module.css +38 -0
- package/dist/plugins/CommandsPlugin/Menu.js +2 -2
- package/dist/plugins/DnDPlugin/DnDPlugin.js +60 -19
- package/dist/plugins/index.js +2 -1
- package/dist/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.d.ts +13 -0
- package/dist/src/plugins/CollapsibleBlocksPlugin/index.d.ts +1 -0
- package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +2 -1
- package/dist/src/plugins/DnDPlugin/types.d.ts +1 -0
- package/dist/src/plugins/index.d.ts +1 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/createEditable.tsx +5 -1
- package/src/plugins/CodeBlockPlugin/styles.module.css +1 -0
- package/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.spec.tsx +216 -0
- package/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.tsx +191 -0
- package/src/plugins/CollapsibleBlocksPlugin/index.ts +1 -0
- package/src/plugins/CollapsibleBlocksPlugin/styles.module.css +37 -0
- package/src/plugins/CommandsPlugin/Menu.spec.tsx +87 -0
- package/src/plugins/CommandsPlugin/Menu.tsx +2 -2
- package/src/plugins/DnDPlugin/DnDPlugin.spec.ts +346 -0
- package/src/plugins/DnDPlugin/DnDPlugin.tsx +117 -31
- package/src/plugins/DnDPlugin/types.ts +1 -0
- package/src/plugins/index.ts +1 -0
- package/src/types.ts +5 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from '@testing-library/react';
|
|
4
|
+
import { createDragDropManager, type DragDropManager } from 'dnd-core';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { DndProvider } from 'react-dnd';
|
|
7
|
+
import type { Editor } from 'slate';
|
|
8
|
+
import { createEditor } from 'slate';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import { KonaEditor } from '../../editor';
|
|
11
|
+
import type { IPlugin } from '../../types';
|
|
12
|
+
import { CollapsibleBlocksPlugin } from '../CollapsibleBlocksPlugin';
|
|
13
|
+
import { HeadingsPlugin } from '../HeadingsPlugin';
|
|
14
|
+
import { DnDPlugin, moveNode } from './DnDPlugin';
|
|
15
|
+
|
|
16
|
+
Object.assign(globalThis, { React });
|
|
17
|
+
|
|
18
|
+
const listEditor = () => {
|
|
19
|
+
const editor = createEditor();
|
|
20
|
+
editor.children = [
|
|
21
|
+
{
|
|
22
|
+
type: 'ol',
|
|
23
|
+
nodeId: 'list',
|
|
24
|
+
children: [
|
|
25
|
+
{ type: 'li', nodeId: 'first', children: [{ text: 'First' }] },
|
|
26
|
+
{ type: 'li', nodeId: 'second', children: [{ text: 'Second' }] },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
return editor;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sectionEditor = () => {
|
|
34
|
+
const editor = createEditor();
|
|
35
|
+
editor.children = [
|
|
36
|
+
{
|
|
37
|
+
type: 'heading1',
|
|
38
|
+
nodeId: 'section',
|
|
39
|
+
collapsed: true,
|
|
40
|
+
children: [{ text: 'Section' }],
|
|
41
|
+
},
|
|
42
|
+
{ type: 'paragraph', nodeId: 'text', children: [{ text: 'Section text' }] },
|
|
43
|
+
{
|
|
44
|
+
type: 'heading2',
|
|
45
|
+
nodeId: 'nested',
|
|
46
|
+
children: [{ text: 'Nested section' }],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'paragraph',
|
|
50
|
+
nodeId: 'nested-text',
|
|
51
|
+
children: [{ text: 'Nested text' }],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'heading1',
|
|
55
|
+
nodeId: 'next',
|
|
56
|
+
children: [{ text: 'Next section' }],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
return editor;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('moveNode', () => {
|
|
63
|
+
it('moves a list item before another list item', () => {
|
|
64
|
+
const editor = listEditor();
|
|
65
|
+
|
|
66
|
+
moveNode(editor, [0, 0], ['second'], 'top');
|
|
67
|
+
|
|
68
|
+
expect(editor.children[0].children.map((node) => node.nodeId)).toEqual([
|
|
69
|
+
'second',
|
|
70
|
+
'first',
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('allows dropping at the end of a list', () => {
|
|
75
|
+
const editor = listEditor();
|
|
76
|
+
|
|
77
|
+
moveNode(editor, [0, 1], ['first'], 'bottom');
|
|
78
|
+
|
|
79
|
+
expect(editor.children[0].children.map((node) => node.nodeId)).toEqual([
|
|
80
|
+
'second',
|
|
81
|
+
'first',
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('moves a collapsed section with all of its content', () => {
|
|
86
|
+
const editor = sectionEditor();
|
|
87
|
+
|
|
88
|
+
moveNode(editor, [4], [], 'bottom', undefined, [[0], [1], [2], [3]]);
|
|
89
|
+
|
|
90
|
+
expect(editor.children.map((node) => node.children[0].text)).toEqual([
|
|
91
|
+
'Next section',
|
|
92
|
+
'Section',
|
|
93
|
+
'Section text',
|
|
94
|
+
'Nested section',
|
|
95
|
+
'Nested text',
|
|
96
|
+
]);
|
|
97
|
+
expect(editor.children[1]).toMatchObject({ collapsed: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('moves a nested section without crossing the next equal-or-higher boundary', () => {
|
|
101
|
+
const editor = sectionEditor();
|
|
102
|
+
|
|
103
|
+
moveNode(editor, [4], [], 'bottom', undefined, [[2], [3]]);
|
|
104
|
+
|
|
105
|
+
expect(editor.children.map((node) => node.children[0].text)).toEqual([
|
|
106
|
+
'Section',
|
|
107
|
+
'Section text',
|
|
108
|
+
'Next section',
|
|
109
|
+
'Nested section',
|
|
110
|
+
'Nested text',
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not move a section when dropped onto one of its own blocks', () => {
|
|
115
|
+
const editor = sectionEditor();
|
|
116
|
+
|
|
117
|
+
moveNode(editor, [2], [], 'top', undefined, [[0], [1], [2], [3]]);
|
|
118
|
+
|
|
119
|
+
expect(editor.children.map((node) => node.children[0].text)).toEqual([
|
|
120
|
+
'Section',
|
|
121
|
+
'Section text',
|
|
122
|
+
'Nested section',
|
|
123
|
+
'Nested text',
|
|
124
|
+
'Next section',
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const createDndManager = (): DragDropManager =>
|
|
130
|
+
createDragDropManager(() => ({
|
|
131
|
+
connectDragPreview: () => () => {},
|
|
132
|
+
connectDragSource: () => () => {},
|
|
133
|
+
connectDropTarget: () => () => {},
|
|
134
|
+
profile: () => ({}),
|
|
135
|
+
setup: () => {},
|
|
136
|
+
teardown: () => {},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
const getHandlerIds = (
|
|
140
|
+
manager: DragDropManager,
|
|
141
|
+
kind: 'dragSources' | 'dropTargets',
|
|
142
|
+
) => {
|
|
143
|
+
const registry = manager.getRegistry() as unknown as Record<
|
|
144
|
+
'dragSources' | 'dropTargets',
|
|
145
|
+
Map<string, unknown>
|
|
146
|
+
>;
|
|
147
|
+
return [...registry[kind].keys()];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
describe('DnDPlugin', () => {
|
|
151
|
+
it('reorders rendered paragraphs when dropped before another block', async () => {
|
|
152
|
+
const manager = createDndManager();
|
|
153
|
+
const onChange = vi.fn();
|
|
154
|
+
let editor: Editor | undefined;
|
|
155
|
+
|
|
156
|
+
const dndPlugin = new DnDPlugin({
|
|
157
|
+
onDropFiles: () => {},
|
|
158
|
+
renderBlock: ({ dragRef, dropRef, previewRef, props }) =>
|
|
159
|
+
React.createElement(
|
|
160
|
+
'div',
|
|
161
|
+
{
|
|
162
|
+
...props.attributes,
|
|
163
|
+
ref: (element) => dropRef(previewRef(element)),
|
|
164
|
+
},
|
|
165
|
+
React.createElement(
|
|
166
|
+
'button',
|
|
167
|
+
{ contentEditable: false, ref: dragRef, type: 'button' },
|
|
168
|
+
'Drag',
|
|
169
|
+
),
|
|
170
|
+
props.children,
|
|
171
|
+
),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
render(
|
|
175
|
+
React.createElement(
|
|
176
|
+
DndProvider,
|
|
177
|
+
{ manager },
|
|
178
|
+
React.createElement(KonaEditor, {
|
|
179
|
+
initialValue: [
|
|
180
|
+
{ type: 'paragraph', children: [{ text: 'First' }] },
|
|
181
|
+
{ type: 'paragraph', children: [{ text: 'Second' }] },
|
|
182
|
+
],
|
|
183
|
+
plugins: [
|
|
184
|
+
dndPlugin,
|
|
185
|
+
{
|
|
186
|
+
init(value) {
|
|
187
|
+
editor = value;
|
|
188
|
+
return value;
|
|
189
|
+
},
|
|
190
|
+
} satisfies IPlugin,
|
|
191
|
+
],
|
|
192
|
+
onChange,
|
|
193
|
+
}),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
198
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
199
|
+
configurable: true,
|
|
200
|
+
value: () => ({ bottom: 100, top: 0 }),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const sourceIds = getHandlerIds(manager, 'dragSources');
|
|
205
|
+
const targetIds = getHandlerIds(manager, 'dropTargets');
|
|
206
|
+
|
|
207
|
+
expect(sourceIds).toHaveLength(2);
|
|
208
|
+
expect(targetIds).toHaveLength(2);
|
|
209
|
+
|
|
210
|
+
act(() => {
|
|
211
|
+
manager.getActions().beginDrag([sourceIds[1]], {
|
|
212
|
+
clientOffset: { x: 0, y: 50 },
|
|
213
|
+
getSourceClientOffset: () => ({ x: 0, y: 0 }),
|
|
214
|
+
});
|
|
215
|
+
manager.getActions().publishDragSource();
|
|
216
|
+
manager
|
|
217
|
+
.getActions()
|
|
218
|
+
.hover([targetIds[0]], { clientOffset: { x: 0, y: 0 } });
|
|
219
|
+
expect(manager.getMonitor().canDropOnTarget(targetIds[0])).toBe(true);
|
|
220
|
+
manager.getActions().drop();
|
|
221
|
+
manager.getActions().endDrag();
|
|
222
|
+
});
|
|
223
|
+
} finally {
|
|
224
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
225
|
+
configurable: true,
|
|
226
|
+
value: originalRect,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
expect(editor?.children).toEqual([
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
type: 'paragraph',
|
|
233
|
+
children: [{ text: 'Second' }],
|
|
234
|
+
}),
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
type: 'paragraph',
|
|
237
|
+
children: [{ text: 'First' }],
|
|
238
|
+
}),
|
|
239
|
+
]);
|
|
240
|
+
await waitFor(() =>
|
|
241
|
+
expect(onChange).toHaveBeenLastCalledWith(editor?.children),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('moves every block in an expanded collapsible section', async () => {
|
|
246
|
+
const manager = createDndManager();
|
|
247
|
+
const onChange = vi.fn();
|
|
248
|
+
let editor: Editor | undefined;
|
|
249
|
+
|
|
250
|
+
const dndPlugin = new DnDPlugin({
|
|
251
|
+
onDropFiles: () => {},
|
|
252
|
+
renderBlock: ({ dragRef, dropRef, previewRef, props }) =>
|
|
253
|
+
React.createElement(
|
|
254
|
+
'div',
|
|
255
|
+
{
|
|
256
|
+
...props.attributes,
|
|
257
|
+
ref: (element) => dropRef(previewRef(element)),
|
|
258
|
+
},
|
|
259
|
+
React.createElement(
|
|
260
|
+
'button',
|
|
261
|
+
{ contentEditable: false, ref: dragRef, type: 'button' },
|
|
262
|
+
'Drag',
|
|
263
|
+
),
|
|
264
|
+
props.children,
|
|
265
|
+
),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
render(
|
|
269
|
+
React.createElement(
|
|
270
|
+
DndProvider,
|
|
271
|
+
{ manager },
|
|
272
|
+
React.createElement(KonaEditor, {
|
|
273
|
+
initialValue: [
|
|
274
|
+
{
|
|
275
|
+
type: HeadingsPlugin.HeadingLevel1,
|
|
276
|
+
children: [{ text: 'Section' }],
|
|
277
|
+
},
|
|
278
|
+
{ type: 'paragraph', children: [{ text: 'Section text' }] },
|
|
279
|
+
{
|
|
280
|
+
type: HeadingsPlugin.HeadingLevel2,
|
|
281
|
+
children: [{ text: 'Nested section' }],
|
|
282
|
+
},
|
|
283
|
+
{ type: 'paragraph', children: [{ text: 'Nested text' }] },
|
|
284
|
+
{
|
|
285
|
+
type: HeadingsPlugin.HeadingLevel1,
|
|
286
|
+
children: [{ text: 'Next section' }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
plugins: [
|
|
290
|
+
new HeadingsPlugin(),
|
|
291
|
+
new CollapsibleBlocksPlugin([
|
|
292
|
+
HeadingsPlugin.HeadingLevel1,
|
|
293
|
+
HeadingsPlugin.HeadingLevel2,
|
|
294
|
+
]),
|
|
295
|
+
dndPlugin,
|
|
296
|
+
{
|
|
297
|
+
init(value) {
|
|
298
|
+
editor = value;
|
|
299
|
+
return value;
|
|
300
|
+
},
|
|
301
|
+
} satisfies IPlugin,
|
|
302
|
+
],
|
|
303
|
+
onChange,
|
|
304
|
+
}),
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
309
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
310
|
+
configurable: true,
|
|
311
|
+
value: () => ({ bottom: 100, top: 0 }),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const sourceIds = getHandlerIds(manager, 'dragSources');
|
|
316
|
+
const targetIds = getHandlerIds(manager, 'dropTargets');
|
|
317
|
+
|
|
318
|
+
act(() => {
|
|
319
|
+
manager.getActions().beginDrag([sourceIds[0]], {
|
|
320
|
+
clientOffset: { x: 0, y: 50 },
|
|
321
|
+
getSourceClientOffset: () => ({ x: 0, y: 0 }),
|
|
322
|
+
});
|
|
323
|
+
manager.getActions().publishDragSource();
|
|
324
|
+
manager
|
|
325
|
+
.getActions()
|
|
326
|
+
.hover([targetIds[4]], { clientOffset: { x: 0, y: 100 } });
|
|
327
|
+
manager.getActions().drop();
|
|
328
|
+
manager.getActions().endDrag();
|
|
329
|
+
});
|
|
330
|
+
} finally {
|
|
331
|
+
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
|
332
|
+
configurable: true,
|
|
333
|
+
value: originalRect,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
expect(editor?.children.map((node) => node.children[0].text)).toEqual([
|
|
338
|
+
'Next section',
|
|
339
|
+
'Section',
|
|
340
|
+
'Section text',
|
|
341
|
+
'Nested section',
|
|
342
|
+
'Nested text',
|
|
343
|
+
]);
|
|
344
|
+
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
|
345
|
+
});
|
|
346
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { CustomElement } from '../../../types';
|
|
13
13
|
import { useEditorContext } from '../../provider';
|
|
14
14
|
import type { IPlugin } from '../../types';
|
|
15
|
+
import { getCollapsibleSectionPaths } from '../CollapsibleBlocksPlugin/CollapsibleBlocksPlugin';
|
|
15
16
|
import type { EditorDragItem, NodeWithId, Options } from './types';
|
|
16
17
|
import { getNodesByNodeIds } from './utils';
|
|
17
18
|
|
|
@@ -54,15 +55,18 @@ export class DnDPlugin implements IPlugin {
|
|
|
54
55
|
type: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
|
|
55
56
|
item: () => {
|
|
56
57
|
const selected: Array<NodeWithId> = $selectedNodes;
|
|
58
|
+
const path = ReactEditor.findPath(editor, props.element);
|
|
59
|
+
const sectionPaths = getCollapsibleSectionPaths(editor, path);
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
62
|
* We copy nodes that the user is dragging.
|
|
60
63
|
* If a user drags multiple nodes, and the current node is one of them,
|
|
61
64
|
* we copy selected nodes. Otherwise, we copy only the current node.
|
|
62
65
|
*/
|
|
63
|
-
const nodes =
|
|
64
|
-
|
|
65
|
-
selected.
|
|
66
|
+
const nodes = sectionPaths
|
|
67
|
+
? getNodesAtPaths(editor, sectionPaths)
|
|
68
|
+
: selected.length > 0 &&
|
|
69
|
+
selected.find((node) => node.nodeId === currentElement.nodeId)
|
|
66
70
|
? getNodesByNodeIds(
|
|
67
71
|
editor,
|
|
68
72
|
selected.map((node) => node.nodeId),
|
|
@@ -91,6 +95,8 @@ export class DnDPlugin implements IPlugin {
|
|
|
91
95
|
source: {
|
|
92
96
|
editor,
|
|
93
97
|
documentId: options.documentId,
|
|
98
|
+
path,
|
|
99
|
+
sectionPaths: sectionPaths || undefined,
|
|
94
100
|
},
|
|
95
101
|
};
|
|
96
102
|
|
|
@@ -124,22 +130,8 @@ export class DnDPlugin implements IPlugin {
|
|
|
124
130
|
};
|
|
125
131
|
},
|
|
126
132
|
hover(_, monitor) {
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
const hoverBoundingRect = element.getBoundingClientRect();
|
|
131
|
-
const hoverMiddleY =
|
|
132
|
-
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
133
|
-
const clientOffset = monitor.getClientOffset();
|
|
134
|
-
if (!clientOffset) return;
|
|
135
|
-
|
|
136
|
-
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
|
137
|
-
|
|
138
|
-
if (hoverClientY < hoverMiddleY) {
|
|
139
|
-
setDropPosition('top');
|
|
140
|
-
} else {
|
|
141
|
-
setDropPosition('bottom');
|
|
142
|
-
}
|
|
133
|
+
const position = getDropPosition(dropTargetRef.current, monitor);
|
|
134
|
+
if (position) setDropPosition(position);
|
|
143
135
|
},
|
|
144
136
|
drop: (data, monitor) => {
|
|
145
137
|
const itemType = monitor.getItemType();
|
|
@@ -156,11 +148,12 @@ export class DnDPlugin implements IPlugin {
|
|
|
156
148
|
|
|
157
149
|
if (!sourceTo) return;
|
|
158
150
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
151
|
+
const position =
|
|
152
|
+
getDropPosition(dropTargetRef.current, monitor) ||
|
|
153
|
+
dropPosition ||
|
|
154
|
+
'bottom';
|
|
155
|
+
|
|
156
|
+
const insertAt = getDropPath(editor, sourceTo, position);
|
|
164
157
|
|
|
165
158
|
if (!insertAt) return;
|
|
166
159
|
|
|
@@ -188,15 +181,24 @@ export class DnDPlugin implements IPlugin {
|
|
|
188
181
|
return;
|
|
189
182
|
}
|
|
190
183
|
|
|
191
|
-
if (!
|
|
184
|
+
if (!dragItem.nodes.length) {
|
|
192
185
|
return;
|
|
193
186
|
}
|
|
194
187
|
|
|
195
|
-
const nodeIds = dragItem.nodes
|
|
188
|
+
const nodeIds = dragItem.nodes
|
|
189
|
+
.map((node) => node.nodeId)
|
|
190
|
+
.filter((nodeId): nodeId is string => typeof nodeId === 'string');
|
|
196
191
|
|
|
197
192
|
if (isSameDocument || isSameEditor) {
|
|
198
193
|
if (!isCopying.current) {
|
|
199
|
-
moveNode(
|
|
194
|
+
moveNode(
|
|
195
|
+
editor,
|
|
196
|
+
sourceTo,
|
|
197
|
+
nodeIds,
|
|
198
|
+
position,
|
|
199
|
+
dragItem.source.path,
|
|
200
|
+
dragItem.source.sectionPaths,
|
|
201
|
+
);
|
|
200
202
|
} else {
|
|
201
203
|
if (!generateId) {
|
|
202
204
|
return;
|
|
@@ -214,7 +216,11 @@ export class DnDPlugin implements IPlugin {
|
|
|
214
216
|
|
|
215
217
|
insertNodes(editor, insertAt, clonedNodes);
|
|
216
218
|
if (!isCopying.current && sourceEditor) {
|
|
217
|
-
removeNodes(
|
|
219
|
+
removeNodes(
|
|
220
|
+
sourceEditor,
|
|
221
|
+
nodeIds,
|
|
222
|
+
dragItem.source.sectionPaths,
|
|
223
|
+
);
|
|
218
224
|
}
|
|
219
225
|
}
|
|
220
226
|
selectedNodes.set([]);
|
|
@@ -289,20 +295,66 @@ const getDropPath = (
|
|
|
289
295
|
}
|
|
290
296
|
};
|
|
291
297
|
|
|
292
|
-
const
|
|
298
|
+
const getDropPosition = (
|
|
299
|
+
element: HTMLElement | null,
|
|
300
|
+
monitor: { getClientOffset: () => { y: number } | null },
|
|
301
|
+
) => {
|
|
302
|
+
if (!element) return null;
|
|
303
|
+
|
|
304
|
+
const clientOffset = monitor.getClientOffset();
|
|
305
|
+
if (!clientOffset) return null;
|
|
306
|
+
|
|
307
|
+
const rect = element.getBoundingClientRect();
|
|
308
|
+
return clientOffset.y - rect.top < (rect.bottom - rect.top) / 2
|
|
309
|
+
? 'top'
|
|
310
|
+
: 'bottom';
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const moveNode = (
|
|
293
314
|
editor: Editor,
|
|
294
315
|
targetNodePath: Path,
|
|
295
316
|
nodeIds: string[],
|
|
296
317
|
position: 'top' | 'bottom',
|
|
318
|
+
sourcePath?: Path,
|
|
319
|
+
sectionPaths?: Path[],
|
|
297
320
|
) => {
|
|
298
321
|
const dropPath = getDropPath(editor, targetNodePath, position);
|
|
299
322
|
|
|
300
323
|
if (!dropPath) return;
|
|
301
|
-
if (!
|
|
324
|
+
if (!isValidInsertPath(editor, dropPath)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (sectionPaths?.some((path) => Path.equals(path, targetNodePath))) {
|
|
302
329
|
return;
|
|
303
330
|
}
|
|
304
331
|
|
|
305
332
|
Editor.withoutNormalizing(editor, () => {
|
|
333
|
+
if (sectionPaths?.length) {
|
|
334
|
+
const sectionNodes = new Set(
|
|
335
|
+
sectionPaths
|
|
336
|
+
.filter((path) => Editor.hasPath(editor, path))
|
|
337
|
+
.map((path) => Editor.node(editor, path)[0]),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
Transforms.moveNodes(editor, {
|
|
341
|
+
at: [],
|
|
342
|
+
match: (node) => sectionNodes.has(node),
|
|
343
|
+
to: dropPath,
|
|
344
|
+
mode: 'highest',
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
nodeIds.length === 0 &&
|
|
351
|
+
sourcePath &&
|
|
352
|
+
Editor.hasPath(editor, sourcePath)
|
|
353
|
+
) {
|
|
354
|
+
Transforms.moveNodes(editor, { at: sourcePath, to: dropPath });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
306
358
|
Transforms.moveNodes(editor, {
|
|
307
359
|
at: [],
|
|
308
360
|
match: (n) => {
|
|
@@ -326,14 +378,48 @@ const moveNode = (
|
|
|
326
378
|
Editor.normalize(editor);
|
|
327
379
|
};
|
|
328
380
|
|
|
381
|
+
const isValidInsertPath = (editor: Editor, path: Path) => {
|
|
382
|
+
const parentPath = Path.parent(path);
|
|
383
|
+
const index = path[path.length - 1];
|
|
384
|
+
|
|
385
|
+
if (index === undefined || !Editor.hasPath(editor, parentPath)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const [parent] = Editor.node(editor, parentPath);
|
|
390
|
+
return 'children' in parent && index <= parent.children.length;
|
|
391
|
+
};
|
|
392
|
+
|
|
329
393
|
const insertNodes = (editor: Editor, path: Path, nodes: NodeWithId[]) => {
|
|
330
394
|
Editor.withoutNormalizing(editor, () => {
|
|
331
395
|
Transforms.insertNodes(editor, nodes, { at: path });
|
|
332
396
|
});
|
|
333
397
|
};
|
|
334
398
|
|
|
335
|
-
const
|
|
399
|
+
const getNodesAtPaths = (editor: Editor, paths: Path[]) =>
|
|
400
|
+
paths
|
|
401
|
+
.filter((path) => Editor.hasPath(editor, path))
|
|
402
|
+
.map((path) => structuredClone(Editor.node(editor, path)[0] as NodeWithId));
|
|
403
|
+
|
|
404
|
+
const removeNodes = (
|
|
405
|
+
editor: Editor,
|
|
406
|
+
nodeIds: string[],
|
|
407
|
+
sectionPaths?: Path[],
|
|
408
|
+
) => {
|
|
336
409
|
try {
|
|
410
|
+
if (sectionPaths?.length) {
|
|
411
|
+
const paths = sectionPaths
|
|
412
|
+
.filter((path) => Editor.hasPath(editor, path))
|
|
413
|
+
.sort((left, right) => Path.compare(right, left));
|
|
414
|
+
|
|
415
|
+
Editor.withoutNormalizing(editor, () => {
|
|
416
|
+
paths.forEach((path) => {
|
|
417
|
+
Transforms.removeNodes(editor, { at: path });
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
337
423
|
Transforms.removeNodes(editor, {
|
|
338
424
|
at: [],
|
|
339
425
|
match: (n) => {
|
package/src/plugins/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { AttachmentsPlugin } from './AttachmentsPlugin';
|
|
|
2
2
|
export { BasicFormattingPlugin } from './BasicFormattingPlugin';
|
|
3
3
|
export { BreaksPlugin } from './BreaksPlugin';
|
|
4
4
|
export { CodeBlockPlugin, type CodeElement } from './CodeBlockPlugin';
|
|
5
|
+
export { CollapsibleBlocksPlugin } from './CollapsibleBlocksPlugin';
|
|
5
6
|
export {
|
|
6
7
|
type Command,
|
|
7
8
|
type CommandPathEntry,
|
package/src/types.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface IPlugin<
|
|
|
21
21
|
leafs?: Leaf<TEditor, TLeaf>[];
|
|
22
22
|
hotkeys?: Hotkey[];
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Render block wrappers from lower to higher priority. Plugins with the same
|
|
26
|
+
* priority retain the order supplied to KonaEditor.
|
|
27
|
+
*/
|
|
28
|
+
renderBlockPriority?: number;
|
|
24
29
|
renderBlock?: (props: RenderElementProps) => ReactElement | null;
|
|
25
30
|
renderLeaf?: (props: RenderLeafProps) => ReactElement | null;
|
|
26
31
|
|