@use-kona/editor 0.1.22 → 0.1.24

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.
@@ -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, normalizeNode, deleteFragment, deleteBackward, deleteForward } = editorWithPlugins;
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
- const customType = options.customTypes?.[props.element.type];
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: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
43
- item: {
44
- ...customType?.getData?.(props.element) || {},
45
- element: props.element,
46
- nodeIds: currentElement?.nodeId && selected.includes(currentElement?.nodeId) ? selected : [
47
- currentElement?.nodeId
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
- ...customType?.getDndItem?.(props.element) || {},
51
- canDrag: !isReadOnly
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,24 @@ class DnDPlugin {
78
102
  const itemType = monitor.getItemType();
79
103
  const sourceTo = ReactEditor.findPath(editor, props.element);
80
104
  if (!sourceTo) return;
81
- const dropPath = getDropPath(editor, sourceTo);
82
- if (!dropPath) return;
105
+ const insertAt = getDropPath(editor, sourceTo, dropPosition || 'bottom');
106
+ if (!insertAt) return;
107
+ const customDropHandler = itemType && options.customDropHandlers?.[itemType];
108
+ if (customDropHandler) return void customDropHandler.onDrop({
109
+ editor,
110
+ item,
111
+ insertAt
112
+ });
83
113
  switch(itemType){
84
114
  case NativeTypes.FILE:
85
- options.onDropFiles(editor, item.files, dropPath);
115
+ options.onDropFiles(editor, item.files, insertAt);
86
116
  break;
87
117
  default:
88
118
  {
89
119
  const dragItem = item;
90
- if (!dropPosition || !dragItem.nodeIds?.length) return;
91
- moveNode(editor, sourceTo, dragItem.nodeIds);
120
+ if (!dropPosition || !dragItem.nodes.length) return;
121
+ const nodeIds = dragItem.nodes.map((node)=>node.nodeId);
122
+ moveNode(editor, sourceTo, nodeIds, dropPosition);
92
123
  $store.selected.clear();
93
124
  break;
94
125
  }
@@ -120,12 +151,17 @@ class DnDPlugin {
120
151
  });
121
152
  };
122
153
  }
123
- const getDropPath = (editor, targetNodePath)=>{
154
+ const getDropPath = (editor, targetNodePath, position)=>{
124
155
  const target = Editor.node(editor, targetNodePath);
125
- return target[1];
156
+ switch(position){
157
+ case 'top':
158
+ return target[1];
159
+ case 'bottom':
160
+ return Path.next(target[1]);
161
+ }
126
162
  };
127
- const moveNode = (editor, targetNodePath, nodeIds)=>{
128
- const dropPath = getDropPath(editor, targetNodePath);
163
+ const moveNode = (editor, targetNodePath, nodeIds, position)=>{
164
+ const dropPath = getDropPath(editor, targetNodePath, position);
129
165
  if (!dropPath) return;
130
166
  if (!Editor.hasPath(editor, dropPath)) return;
131
167
  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 type React from 'react';
3
- import { type ConnectDragPreview, type ConnectDragSource, type ConnectDropTarget } from 'react-dnd';
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 {};
@@ -0,0 +1,8 @@
1
+ import { Editor } from 'slate';
2
+ import type { NodeWithId } from './types';
3
+ /**
4
+ * Gets a copy of listed notes by their ids
5
+ * @param editor
6
+ * @param nodeIds
7
+ */
8
+ export declare const getNodesByNodeIds: (editor: Editor, nodeIds: string[]) => NodeWithId[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-kona/editor",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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
  }
@@ -24,14 +24,8 @@ export const createEditor = (plugins: IPlugin[]) => () => {
24
24
  return editor;
25
25
  }, baseEditor);
26
26
 
27
- const {
28
- isInline,
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
- type Options = {
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 DnDNode;
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: customType?.type || DnDPlugin.DND_BLOCK_ELEMENT,
104
- item: {
105
- ...(customType?.getData?.(props.element) || {}),
106
- element: props.element,
107
- nodeIds: currentElement?.nodeId && selected.includes(currentElement?.nodeId)
108
- ? selected
109
- : [currentElement?.nodeId],
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,40 @@ export class DnDPlugin implements IPlugin {
154
158
 
155
159
  if (!sourceTo) return;
156
160
 
157
- const dropPath = getDropPath(editor, sourceTo);
161
+ const insertAt = getDropPath(
162
+ editor,
163
+ sourceTo,
164
+ dropPosition || 'bottom',
165
+ );
158
166
 
159
- if (!dropPath) return;
167
+ if (!insertAt) return;
168
+
169
+ const customDropHandler =
170
+ itemType && options.customDropHandlers?.[itemType];
171
+
172
+ if (customDropHandler) {
173
+ customDropHandler.onDrop({ editor, item, insertAt });
174
+ return;
175
+ }
160
176
 
161
177
  switch (itemType) {
162
178
  case NativeTypes.FILE:
163
179
  options.onDropFiles(
164
180
  editor,
165
181
  (item as { files: File[] }).files,
166
- dropPath,
182
+ insertAt,
167
183
  );
168
184
  break;
169
185
  default: {
170
- const dragItem = item as DnDItem;
186
+ const dragItem = item as EditorDragItem;
171
187
 
172
- if (!dropPosition || !dragItem.nodeIds?.length) {
188
+ if (!dropPosition || !dragItem.nodes.length) {
173
189
  return;
174
190
  }
175
191
 
176
- moveNode(editor, sourceTo, dragItem.nodeIds);
192
+ const nodeIds = dragItem.nodes.map((node) => node.nodeId);
193
+
194
+ moveNode(editor, sourceTo, nodeIds, dropPosition);
177
195
  $store.selected.clear();
178
196
  break;
179
197
  }
@@ -223,22 +241,29 @@ export class DnDPlugin implements IPlugin {
223
241
  const getDropPath = (
224
242
  editor: Editor,
225
243
  targetNodePath: Path,
244
+ position: 'top' | 'bottom',
226
245
  ) => {
227
246
  const target = Editor.node(editor, targetNodePath);
228
247
 
229
- return target[1];
248
+ switch (position) {
249
+ case 'top':
250
+ return target[1];
251
+ case 'bottom':
252
+ return Path.next(target[1]);
253
+ }
230
254
  };
231
255
 
232
256
  const moveNode = (
233
257
  editor: Editor,
234
258
  targetNodePath: Path,
235
259
  nodeIds: string[],
260
+ position: 'top' | 'bottom',
236
261
  ) => {
237
- const dropPath = getDropPath(editor, targetNodePath);
262
+ const dropPath = getDropPath(editor, targetNodePath, position);
238
263
 
239
264
  if (!dropPath) return;
240
265
  if (!Editor.hasPath(editor, dropPath)) {
241
- return
266
+ return;
242
267
  }
243
268
 
244
269
  Editor.withoutNormalizing(editor, () => {
@@ -253,7 +278,7 @@ const moveNode = (
253
278
  return false;
254
279
  }
255
280
 
256
- const node = n as DnDNode;
281
+ const node = n as NodeWithId;
257
282
  const shouldMove =
258
283
  typeof node.nodeId === 'string' && nodeIds.includes(node.nodeId);
259
284
  return shouldMove;
@@ -263,5 +288,4 @@ const moveNode = (
263
288
  });
264
289
  });
265
290
  Editor.normalize(editor);
266
-
267
291
  };
@@ -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
+ };