@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.
Files changed (30) hide show
  1. package/dist/core/createEditable.js +4 -1
  2. package/dist/plugins/CodeBlockPlugin/styles_module.css +1 -0
  3. package/dist/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.js +121 -0
  4. package/dist/plugins/CollapsibleBlocksPlugin/index.js +2 -0
  5. package/dist/plugins/CollapsibleBlocksPlugin/styles.module.js +8 -0
  6. package/dist/plugins/CollapsibleBlocksPlugin/styles_module.css +38 -0
  7. package/dist/plugins/CommandsPlugin/Menu.js +2 -2
  8. package/dist/plugins/DnDPlugin/DnDPlugin.js +60 -19
  9. package/dist/plugins/index.js +2 -1
  10. package/dist/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.d.ts +13 -0
  11. package/dist/src/plugins/CollapsibleBlocksPlugin/index.d.ts +1 -0
  12. package/dist/src/plugins/DnDPlugin/DnDPlugin.d.ts +2 -1
  13. package/dist/src/plugins/DnDPlugin/types.d.ts +1 -0
  14. package/dist/src/plugins/index.d.ts +1 -0
  15. package/dist/src/types.d.ts +5 -0
  16. package/dist/types.d.ts +1 -0
  17. package/package.json +1 -1
  18. package/src/core/createEditable.tsx +5 -1
  19. package/src/plugins/CodeBlockPlugin/styles.module.css +1 -0
  20. package/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.spec.tsx +216 -0
  21. package/src/plugins/CollapsibleBlocksPlugin/CollapsibleBlocksPlugin.tsx +191 -0
  22. package/src/plugins/CollapsibleBlocksPlugin/index.ts +1 -0
  23. package/src/plugins/CollapsibleBlocksPlugin/styles.module.css +37 -0
  24. package/src/plugins/CommandsPlugin/Menu.spec.tsx +87 -0
  25. package/src/plugins/CommandsPlugin/Menu.tsx +2 -2
  26. package/src/plugins/DnDPlugin/DnDPlugin.spec.ts +346 -0
  27. package/src/plugins/DnDPlugin/DnDPlugin.tsx +117 -31
  28. package/src/plugins/DnDPlugin/types.ts +1 -0
  29. package/src/plugins/index.ts +1 -0
  30. 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
- selected.length > 0 &&
65
- selected.find((node) => node.nodeId === currentElement.nodeId)
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 element = dropTargetRef.current;
128
- if (!element) return;
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 insertAt = getDropPath(
160
- editor,
161
- sourceTo,
162
- dropPosition || 'bottom',
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 (!dropPosition || !dragItem.nodes.length) {
184
+ if (!dragItem.nodes.length) {
192
185
  return;
193
186
  }
194
187
 
195
- const nodeIds = dragItem.nodes.map((node) => node.nodeId);
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(editor, sourceTo, nodeIds, dropPosition);
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(sourceEditor, nodeIds);
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 moveNode = (
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 (!Editor.hasPath(editor, dropPath)) {
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 removeNodes = (editor: Editor, nodeIds: string[]) => {
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) => {
@@ -73,6 +73,7 @@ export type EditorInnerDragItem<TItem = unknown> = {
73
73
  */
74
74
  documentId?: string | null;
75
75
  path?: Path;
76
+ sectionPaths?: Path[];
76
77
  };
77
78
  data?: Record<string, unknown>;
78
79
  };
@@ -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