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