@blocknote/core 0.4.2 → 0.4.4

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/blocknote.js +12248 -12269
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +20 -20
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/package.json +2 -2
  6. package/src/BlockNoteEditor.ts +276 -15
  7. package/src/BlockNoteExtensions.ts +8 -4
  8. package/src/api/formatConversions/formatConversions.ts +4 -4
  9. package/src/extensions/Blocks/api/cursorPositionTypes.ts +2 -0
  10. package/src/extensions/Blocks/nodes/BlockContainer.ts +84 -111
  11. package/src/extensions/SlashMenu/BaseSlashMenuItem.ts +31 -0
  12. package/src/extensions/SlashMenu/SlashMenuExtension.ts +10 -7
  13. package/src/extensions/SlashMenu/{defaultSlashCommands.tsx → defaultSlashMenuItems.tsx} +59 -106
  14. package/src/extensions/SlashMenu/index.ts +3 -7
  15. package/src/index.ts +2 -3
  16. package/src/shared/plugins/suggestion/SuggestionItem.ts +2 -13
  17. package/src/shared/plugins/suggestion/SuggestionPlugin.ts +31 -18
  18. package/types/src/BlockNoteEditor.d.ts +100 -8
  19. package/types/src/BlockNoteExtensions.d.ts +5 -4
  20. package/types/src/api/formatConversions/formatConversions.d.ts +2 -2
  21. package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +2 -0
  22. package/types/src/extensions/SlashMenu/BaseSlashMenuItem.d.ts +20 -0
  23. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +4 -2
  24. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +5 -0
  25. package/types/src/extensions/SlashMenu/index.d.ts +3 -3
  26. package/types/src/index.d.ts +2 -3
  27. package/types/src/shared/plugins/suggestion/SuggestionItem.d.ts +3 -11
  28. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +4 -4
  29. package/src/api/Editor.ts +0 -226
  30. package/src/extensions/SlashMenu/SlashMenuItem.ts +0 -34
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://github.com/yousefed/blocknote",
4
4
  "private": false,
5
5
  "license": "MPL-2.0",
6
- "version": "0.4.2",
6
+ "version": "0.4.4",
7
7
  "files": [
8
8
  "dist",
9
9
  "types",
@@ -106,5 +106,5 @@
106
106
  "access": "public",
107
107
  "registry": "https://registry.npmjs.org/"
108
108
  },
109
- "gitHead": "14086236a83e42c1de7e4d9910e6348218e78a98"
109
+ "gitHead": "9d8f356669e7f644ee437aad0df07b4cc37a0083"
110
110
  }
@@ -1,21 +1,46 @@
1
1
  import { Editor, EditorOptions } from "@tiptap/core";
2
-
2
+ import { Node } from "prosemirror-model";
3
3
  // import "./blocknote.css";
4
- import { Editor as EditorAPI } from "./api/Editor";
4
+ import {
5
+ Block,
6
+ BlockIdentifier,
7
+ PartialBlock,
8
+ } from "./extensions/Blocks/api/blockTypes";
5
9
  import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions";
6
10
  import styles from "./editor.module.css";
7
- import { defaultSlashCommands, SlashCommand } from "./extensions/SlashMenu";
11
+ import {
12
+ defaultSlashMenuItems,
13
+ BaseSlashMenuItem,
14
+ } from "./extensions/SlashMenu";
15
+ import { Editor as TiptapEditor } from "@tiptap/core/dist/packages/core/src/Editor";
16
+ import { nodeToBlock } from "./api/nodeConversions/nodeConversions";
17
+ import { TextCursorPosition } from "./extensions/Blocks/api/cursorPositionTypes";
18
+ import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos";
19
+ import { getNodeById } from "./api/util/nodeUtil";
20
+ import {
21
+ insertBlocks,
22
+ updateBlock,
23
+ removeBlocks,
24
+ replaceBlocks,
25
+ } from "./api/blockManipulation/blockManipulation";
26
+ import {
27
+ blocksToHTML,
28
+ HTMLToBlocks,
29
+ blocksToMarkdown,
30
+ markdownToBlocks,
31
+ } from "./api/formatConversions/formatConversions";
8
32
 
9
33
  export type BlockNoteEditorOptions = {
10
34
  // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them.
11
35
  enableBlockNoteExtensions: boolean;
12
36
  disableHistoryExtension: boolean;
13
37
  uiFactories: UiFactories;
14
- slashCommands: SlashCommand[];
38
+ slashCommands: BaseSlashMenuItem[];
15
39
  parentElement: HTMLElement;
16
40
  editorDOMAttributes: Record<string, string>;
17
- onUpdate: (editor: BlockNoteEditor) => void;
18
- onCreate: (editor: BlockNoteEditor) => void;
41
+ onEditorCreate: (editor: BlockNoteEditor) => void;
42
+ onEditorContentChange: (editor: BlockNoteEditor) => void;
43
+ onTextCursorPositionChange: (editor: BlockNoteEditor) => void;
19
44
 
20
45
  // tiptap options, undocumented
21
46
  _tiptapOptions: any;
@@ -27,8 +52,9 @@ const blockNoteTipTapOptions = {
27
52
  enableCoreExtensions: false,
28
53
  };
29
54
 
30
- export class BlockNoteEditor extends EditorAPI {
31
- public readonly _tiptapEditor: Editor & { contentComponent: any };
55
+ export class BlockNoteEditor {
56
+ public readonly _tiptapEditor: TiptapEditor & { contentComponent: any };
57
+ private blockCache = new WeakMap<Node, Block>();
32
58
 
33
59
  public get domElement() {
34
60
  return this._tiptapEditor.view.dom as HTMLDivElement;
@@ -36,8 +62,9 @@ export class BlockNoteEditor extends EditorAPI {
36
62
 
37
63
  constructor(options: Partial<BlockNoteEditorOptions> = {}) {
38
64
  const blockNoteExtensions = getBlockNoteExtensions({
65
+ editor: this,
39
66
  uiFactories: options.uiFactories || {},
40
- slashCommands: options.slashCommands || defaultSlashCommands,
67
+ slashCommands: options.slashCommands || defaultSlashMenuItems,
41
68
  });
42
69
 
43
70
  let extensions = options.disableHistoryExtension
@@ -47,11 +74,14 @@ export class BlockNoteEditor extends EditorAPI {
47
74
  const tiptapOptions: EditorOptions = {
48
75
  ...blockNoteTipTapOptions,
49
76
  ...options._tiptapOptions,
77
+ onCreate: () => {
78
+ options.onEditorCreate?.(this);
79
+ },
50
80
  onUpdate: () => {
51
- options.onUpdate?.(this);
81
+ options.onEditorContentChange?.(this);
52
82
  },
53
- onCreate: () => {
54
- options.onCreate?.(this);
83
+ onSelectionUpdate: () => {
84
+ options.onTextCursorPositionChange?.(this);
55
85
  },
56
86
  extensions:
57
87
  options.enableBlockNoteExtensions === false
@@ -69,10 +99,241 @@ export class BlockNoteEditor extends EditorAPI {
69
99
  },
70
100
  };
71
101
 
72
- const _tiptapEditor = new Editor(tiptapOptions) as Editor & {
102
+ this._tiptapEditor = new Editor(tiptapOptions) as Editor & {
73
103
  contentComponent: any;
74
104
  };
75
- super(_tiptapEditor);
76
- this._tiptapEditor = _tiptapEditor;
105
+ }
106
+
107
+ /**
108
+ * Gets a snapshot of all top-level (non-nested) blocks in the editor.
109
+ * @returns A snapshot of all top-level (non-nested) blocks in the editor.
110
+ */
111
+ public get topLevelBlocks(): Block[] {
112
+ const blocks: Block[] = [];
113
+
114
+ this._tiptapEditor.state.doc.firstChild!.descendants((node) => {
115
+ blocks.push(nodeToBlock(node, this.blockCache));
116
+
117
+ return false;
118
+ });
119
+
120
+ return blocks;
121
+ }
122
+
123
+ /**
124
+ * Gets a snapshot of an existing block from the editor.
125
+ * @param blockIdentifier The identifier of an existing block that should be retrieved.
126
+ * @returns The block that matches the identifier, or `undefined` if no matching block was found.
127
+ */
128
+ public getBlock(blockIdentifier: BlockIdentifier): Block | undefined {
129
+ const id =
130
+ typeof blockIdentifier === "string"
131
+ ? blockIdentifier
132
+ : blockIdentifier.id;
133
+ let newBlock: Block | undefined = undefined;
134
+
135
+ this._tiptapEditor.state.doc.firstChild!.descendants((node) => {
136
+ if (typeof newBlock !== "undefined") {
137
+ return false;
138
+ }
139
+
140
+ if (node.type.name !== "blockContainer" || node.attrs.id !== id) {
141
+ return true;
142
+ }
143
+
144
+ newBlock = nodeToBlock(node, this.blockCache);
145
+
146
+ return false;
147
+ });
148
+
149
+ return newBlock;
150
+ }
151
+
152
+ /**
153
+ * Traverses all blocks in the editor depth-first, and executes a callback for each.
154
+ * @param callback The callback to execute for each block. Returning `false` stops the traversal.
155
+ * @param reverse Whether the blocks should be traversed in reverse order.
156
+ */
157
+ public forEachBlock(
158
+ callback: (block: Block) => void,
159
+ reverse: boolean = false
160
+ ): void {
161
+ function helper(blocks: Block[]) {
162
+ if (reverse) {
163
+ for (const block of blocks.reverse()) {
164
+ helper(block.children);
165
+ callback(block);
166
+ }
167
+ } else {
168
+ for (const block of blocks) {
169
+ callback(block);
170
+ helper(block.children);
171
+ }
172
+ }
173
+ }
174
+
175
+ helper(this.topLevelBlocks);
176
+ }
177
+
178
+ /**
179
+ * Gets a snapshot of the current text cursor position.
180
+ * @returns A snapshot of the current text cursor position.
181
+ */
182
+ public getTextCursorPosition(): TextCursorPosition {
183
+ const { node, depth, startPos, endPos } = getBlockInfoFromPos(
184
+ this._tiptapEditor.state.doc,
185
+ this._tiptapEditor.state.selection.from
186
+ )!;
187
+
188
+ // Index of the current blockContainer node relative to its parent blockGroup.
189
+ const nodeIndex = this._tiptapEditor.state.doc
190
+ .resolve(endPos)
191
+ .index(depth - 1);
192
+ // Number of the parent blockGroup's child blockContainer nodes.
193
+ const numNodes = this._tiptapEditor.state.doc
194
+ .resolve(endPos + 1)
195
+ .node().childCount;
196
+
197
+ // Gets previous blockContainer node at the same nesting level, if the current node isn't the first child.
198
+ let prevNode: Node | undefined = undefined;
199
+ if (nodeIndex > 0) {
200
+ prevNode = this._tiptapEditor.state.doc.resolve(startPos - 2).node();
201
+ }
202
+
203
+ // Gets next blockContainer node at the same nesting level, if the current node isn't the last child.
204
+ let nextNode: Node | undefined = undefined;
205
+ if (nodeIndex < numNodes - 1) {
206
+ nextNode = this._tiptapEditor.state.doc.resolve(endPos + 2).node();
207
+ }
208
+
209
+ return {
210
+ block: nodeToBlock(node, this.blockCache),
211
+ prevBlock:
212
+ prevNode === undefined
213
+ ? undefined
214
+ : nodeToBlock(prevNode, this.blockCache),
215
+ nextBlock:
216
+ nextNode === undefined
217
+ ? undefined
218
+ : nodeToBlock(nextNode, this.blockCache),
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Sets the text cursor position to the start or end of an existing block. Throws an error if the target block could
224
+ * not be found.
225
+ * @param targetBlock The identifier of an existing block that the text cursor should be moved to.
226
+ * @param placement Whether the text cursor should be placed at the start or end of the block.
227
+ */
228
+ public setTextCursorPosition(
229
+ targetBlock: BlockIdentifier,
230
+ placement: "start" | "end" = "start"
231
+ ) {
232
+ const id = typeof targetBlock === "string" ? targetBlock : targetBlock.id;
233
+
234
+ const { posBeforeNode } = getNodeById(id, this._tiptapEditor.state.doc);
235
+ const { startPos, contentNode } = getBlockInfoFromPos(
236
+ this._tiptapEditor.state.doc,
237
+ posBeforeNode + 2
238
+ )!;
239
+
240
+ if (placement === "start") {
241
+ this._tiptapEditor.commands.setTextSelection(startPos + 1);
242
+ } else {
243
+ this._tiptapEditor.commands.setTextSelection(
244
+ startPos + contentNode.nodeSize - 1
245
+ );
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Inserts new blocks into the editor. If a block's `id` is undefined, BlockNote generates one automatically. Throws an
251
+ * error if the reference block could not be found.
252
+ * @param blocksToInsert An array of partial blocks that should be inserted.
253
+ * @param referenceBlock An identifier for an existing block, at which the new blocks should be inserted.
254
+ * @param placement Whether the blocks should be inserted just before, just after, or nested inside the
255
+ * `referenceBlock`. Inserts the blocks at the start of the existing block's children if "nested" is used.
256
+ */
257
+ public insertBlocks(
258
+ blocksToInsert: PartialBlock[],
259
+ referenceBlock: BlockIdentifier,
260
+ placement: "before" | "after" | "nested" = "before"
261
+ ): void {
262
+ insertBlocks(blocksToInsert, referenceBlock, placement, this._tiptapEditor);
263
+ }
264
+
265
+ /**
266
+ * Updates an existing block in the editor. Since updatedBlock is a PartialBlock object, some fields might not be
267
+ * defined. These undefined fields are kept as-is from the existing block. Throws an error if the block to update could
268
+ * not be found.
269
+ * @param blockToUpdate The block that should be updated.
270
+ * @param update A partial block which defines how the existing block should be changed.
271
+ */
272
+ public updateBlock(blockToUpdate: Block, update: PartialBlock) {
273
+ updateBlock(blockToUpdate, update, this._tiptapEditor);
274
+ }
275
+
276
+ /**
277
+ * Removes existing blocks from the editor. Throws an error if any of the blocks could not be found.
278
+ * @param blocksToRemove An array of identifiers for existing blocks that should be removed.
279
+ */
280
+ public removeBlocks(blocksToRemove: Block[]) {
281
+ removeBlocks(blocksToRemove, this._tiptapEditor);
282
+ }
283
+
284
+ /**
285
+ * Replaces existing blocks in the editor with new blocks. If the blocks that should be removed are not adjacent or
286
+ * are at different nesting levels, `blocksToInsert` will be inserted at the position of the first block in
287
+ * `blocksToRemove`. Throws an error if any of the blocks to remove could not be found.
288
+ * @param blocksToRemove An array of blocks that should be replaced.
289
+ * @param blocksToInsert An array of partial blocks to replace the old ones with.
290
+ */
291
+ public replaceBlocks(
292
+ blocksToRemove: Block[],
293
+ blocksToInsert: PartialBlock[]
294
+ ) {
295
+ replaceBlocks(blocksToRemove, blocksToInsert, this._tiptapEditor);
296
+ }
297
+
298
+ /**
299
+ * Serializes blocks into an HTML string. To better conform to HTML standards, children of blocks which aren't list
300
+ * items are un-nested in the output HTML.
301
+ * @param blocks An array of blocks that should be serialized into HTML.
302
+ * @returns The blocks, serialized as an HTML string.
303
+ */
304
+ public async blocksToHTML(blocks: Block[]): Promise<string> {
305
+ return blocksToHTML(blocks, this._tiptapEditor.schema);
306
+ }
307
+
308
+ /**
309
+ * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and
310
+ * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote
311
+ * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text.
312
+ * @param html The HTML string to parse blocks from.
313
+ * @returns The blocks parsed from the HTML string.
314
+ */
315
+ public async HTMLToBlocks(html: string): Promise<Block[]> {
316
+ return HTMLToBlocks(html, this._tiptapEditor.schema);
317
+ }
318
+
319
+ /**
320
+ * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of
321
+ * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed.
322
+ * @param blocks An array of blocks that should be serialized into Markdown.
323
+ * @returns The blocks, serialized as a Markdown string.
324
+ */
325
+ public async blocksToMarkdown(blocks: Block[]): Promise<string> {
326
+ return blocksToMarkdown(blocks, this._tiptapEditor.schema);
327
+ }
328
+
329
+ /**
330
+ * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on
331
+ * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it
332
+ * as text.
333
+ * @param markdown The Markdown string to parse blocks from.
334
+ * @returns The blocks parsed from the Markdown string.
335
+ */
336
+ public async markdownToBlocks(markdown: string): Promise<Block[]> {
337
+ return markdownToBlocks(markdown, this._tiptapEditor.schema);
77
338
  }
78
339
  }
@@ -1,5 +1,7 @@
1
1
  import { Extensions, extensions } from "@tiptap/core";
2
2
 
3
+ import { BlockNoteEditor } from "./BlockNoteEditor";
4
+
3
5
  import { Bold } from "@tiptap/extension-bold";
4
6
  import { Code } from "@tiptap/extension-code";
5
7
  import { Dropcursor } from "@tiptap/extension-dropcursor";
@@ -22,8 +24,8 @@ import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/Formatt
22
24
  import HyperlinkMark from "./extensions/HyperlinkToolbar/HyperlinkMark";
23
25
  import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes";
24
26
  import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension";
25
- import { SlashCommand, SlashMenuExtension } from "./extensions/SlashMenu";
26
- import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem";
27
+ import { SlashMenuExtension } from "./extensions/SlashMenu";
28
+ import { BaseSlashMenuItem } from "./extensions/SlashMenu";
27
29
  import { TextAlignmentExtension } from "./extensions/TextAlignment/TextAlignmentExtension";
28
30
  import { TextColorExtension } from "./extensions/TextColor/TextColorExtension";
29
31
  import { TextColorMark } from "./extensions/TextColor/TextColorMark";
@@ -34,7 +36,7 @@ import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsM
34
36
  export type UiFactories = Partial<{
35
37
  formattingToolbarFactory: FormattingToolbarFactory;
36
38
  hyperlinkToolbarFactory: HyperlinkToolbarFactory;
37
- slashMenuFactory: SuggestionsMenuFactory<SlashMenuItem>;
39
+ slashMenuFactory: SuggestionsMenuFactory<BaseSlashMenuItem>;
38
40
  blockSideMenuFactory: BlockSideMenuFactory;
39
41
  }>;
40
42
 
@@ -42,8 +44,9 @@ export type UiFactories = Partial<{
42
44
  * Get all the Tiptap extensions BlockNote is configured with by default
43
45
  */
44
46
  export const getBlockNoteExtensions = (opts: {
47
+ editor: BlockNoteEditor;
45
48
  uiFactories: UiFactories;
46
- slashCommands: SlashCommand[];
49
+ slashCommands: BaseSlashMenuItem[];
47
50
  }) => {
48
51
  const ret: Extensions = [
49
52
  extensions.ClipboardTextSerializer,
@@ -123,6 +126,7 @@ export const getBlockNoteExtensions = (opts: {
123
126
  if (opts.uiFactories.slashMenuFactory) {
124
127
  ret.push(
125
128
  SlashMenuExtension.configure({
129
+ editor: opts.editor,
126
130
  commands: opts.slashCommands,
127
131
  slashMenuFactory: opts.uiFactories.slashMenuFactory,
128
132
  })
@@ -38,11 +38,11 @@ export async function blocksToHTML(
38
38
  }
39
39
 
40
40
  export async function HTMLToBlocks(
41
- htmlString: string,
41
+ html: string,
42
42
  schema: Schema
43
43
  ): Promise<Block[]> {
44
44
  const htmlNode = document.createElement("div");
45
- htmlNode.innerHTML = htmlString.trim();
45
+ htmlNode.innerHTML = html.trim();
46
46
 
47
47
  const parser = DOMParser.fromSchema(schema);
48
48
  const parentNode = parser.parse(htmlNode);
@@ -72,7 +72,7 @@ export async function blocksToMarkdown(
72
72
  }
73
73
 
74
74
  export async function markdownToBlocks(
75
- markdownString: string,
75
+ markdown: string,
76
76
  schema: Schema
77
77
  ): Promise<Block[]> {
78
78
  const htmlString = await unified()
@@ -80,7 +80,7 @@ export async function markdownToBlocks(
80
80
  .use(remarkGfm)
81
81
  .use(remarkRehype)
82
82
  .use(rehypeStringify)
83
- .process(markdownString);
83
+ .process(markdown);
84
84
 
85
85
  return HTMLToBlocks(htmlString.value as string, schema);
86
86
  }
@@ -2,4 +2,6 @@ import { Block } from "./blockTypes";
2
2
 
3
3
  export type TextCursorPosition = {
4
4
  block: Block;
5
+ prevBlock: Block | undefined;
6
+ nextBlock: Block | undefined;
5
7
  };
@@ -110,10 +110,10 @@ export const BlockContainer = Node.create<IBlock>({
110
110
 
111
111
  return true;
112
112
  },
113
- // Deletes a block at a given position and sets the selection to where the block was.
113
+ // Deletes a block at a given position.
114
114
  BNDeleteBlock:
115
115
  (posInBlock) =>
116
- ({ state, view, dispatch }) => {
116
+ ({ state, dispatch }) => {
117
117
  const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
118
118
  if (blockInfo === undefined) {
119
119
  return false;
@@ -123,10 +123,89 @@ export const BlockContainer = Node.create<IBlock>({
123
123
 
124
124
  if (dispatch) {
125
125
  state.tr.deleteRange(startPos, endPos);
126
- state.tr.setSelection(
127
- new TextSelection(state.doc.resolve(startPos + 1))
126
+ }
127
+
128
+ return true;
129
+ },
130
+ // Updates a block at a given position.
131
+ BNUpdateBlock:
132
+ (posInBlock, block) =>
133
+ ({ state, dispatch }) => {
134
+ const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
135
+ if (blockInfo === undefined) {
136
+ return false;
137
+ }
138
+
139
+ const { startPos, endPos, node, contentNode } = blockInfo;
140
+
141
+ if (dispatch) {
142
+ // Adds blockGroup node with child blocks if necessary.
143
+ if (block.children !== undefined) {
144
+ const childNodes = [];
145
+
146
+ // Creates ProseMirror nodes for each child block, including their descendants.
147
+ for (const child of block.children) {
148
+ childNodes.push(blockToNode(child, state.schema));
149
+ }
150
+
151
+ // Checks if a blockGroup node already exists.
152
+ if (node.childCount === 2) {
153
+ // Replaces all child nodes in the existing blockGroup with the ones created earlier.
154
+ state.tr.replace(
155
+ startPos + contentNode.nodeSize + 1,
156
+ endPos - 1,
157
+ new Slice(Fragment.from(childNodes), 0, 0)
158
+ );
159
+ } else {
160
+ // Inserts a new blockGroup containing the child nodes created earlier.
161
+ state.tr.insert(
162
+ startPos + contentNode.nodeSize,
163
+ state.schema.nodes["blockGroup"].create({}, childNodes)
164
+ );
165
+ }
166
+ }
167
+
168
+ // Replaces the blockContent node's content if necessary.
169
+ if (block.content !== undefined) {
170
+ let content: PMNode[] = [];
171
+
172
+ // Checks if the provided content is a string or InlineContent[] type.
173
+ if (typeof block.content === "string") {
174
+ // Adds a single text node with no marks to the content.
175
+ content.push(state.schema.text(block.content));
176
+ } else {
177
+ // Adds a text node with the provided styles converted into marks to the content, for each InlineContent
178
+ // object.
179
+ content = inlineContentToNodes(block.content, state.schema);
180
+ }
181
+
182
+ // Replaces the contents of the blockContent node with the previously created text node(s).
183
+ state.tr.replace(
184
+ startPos + 1,
185
+ startPos + contentNode.nodeSize - 1,
186
+ new Slice(Fragment.from(content), 0, 0)
187
+ );
188
+ }
189
+
190
+ // Changes the blockContent node type and adds the provided props as attributes. Also preserves all existing
191
+ // attributes that are compatible with the new type.
192
+ state.tr.setNodeMarkup(
193
+ startPos,
194
+ block.type === undefined
195
+ ? undefined
196
+ : state.schema.nodes[block.type],
197
+ {
198
+ ...contentNode.attrs,
199
+ ...block.props,
200
+ }
128
201
  );
129
- view.focus();
202
+
203
+ // Adds all provided props as attributes to the parent blockContainer node too, and also preserves existing
204
+ // attributes.
205
+ state.tr.setNodeMarkup(startPos - 1, undefined, {
206
+ ...node.attrs,
207
+ ...block.props,
208
+ });
130
209
  }
131
210
 
132
211
  return true;
@@ -283,112 +362,6 @@ export const BlockContainer = Node.create<IBlock>({
283
362
 
284
363
  return true;
285
364
  },
286
- // Updates a block to the given specification.
287
- BNUpdateBlock:
288
- (posInBlock, block) =>
289
- ({ state, dispatch }) => {
290
- const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
291
- if (blockInfo === undefined) {
292
- return false;
293
- }
294
-
295
- const { startPos, endPos, node, contentNode } = blockInfo;
296
-
297
- if (dispatch) {
298
- // Adds blockGroup node with child blocks if necessary.
299
- if (block.children !== undefined) {
300
- const childNodes = [];
301
-
302
- // Creates ProseMirror nodes for each child block, including their descendants.
303
- for (const child of block.children) {
304
- childNodes.push(blockToNode(child, state.schema));
305
- }
306
-
307
- // Checks if a blockGroup node already exists.
308
- if (node.childCount === 2) {
309
- // Replaces all child nodes in the existing blockGroup with the ones created earlier.
310
- state.tr.replace(
311
- startPos + contentNode.nodeSize + 1,
312
- endPos - 1,
313
- new Slice(Fragment.from(childNodes), 0, 0)
314
- );
315
- } else {
316
- // Inserts a new blockGroup containing the child nodes created earlier.
317
- state.tr.insert(
318
- startPos + contentNode.nodeSize,
319
- state.schema.nodes["blockGroup"].create({}, childNodes)
320
- );
321
- }
322
- }
323
-
324
- // Replaces the blockContent node's content if necessary.
325
- if (block.content !== undefined) {
326
- let content: PMNode[] = [];
327
-
328
- // Checks if the provided content is a string or InlineContent[] type.
329
- if (typeof block.content === "string") {
330
- // Adds a single text node with no marks to the content.
331
- content.push(state.schema.text(block.content));
332
- } else {
333
- // Adds a text node with the provided styles converted into marks to the content, for each InlineContent
334
- // object.
335
- content = inlineContentToNodes(block.content, state.schema);
336
- }
337
-
338
- // Replaces the contents of the blockContent node with the previously created text node(s).
339
- state.tr.replace(
340
- startPos + 1,
341
- startPos + contentNode.nodeSize - 1,
342
- new Slice(Fragment.from(content), 0, 0)
343
- );
344
- }
345
-
346
- // Changes the block type and adds the provided props as node attributes. Also preserves all existing node
347
- // attributes that are compatible with the new type.
348
- state.tr.setNodeMarkup(
349
- startPos,
350
- block.type === undefined
351
- ? undefined
352
- : state.schema.nodes[block.type],
353
- {
354
- ...contentNode.attrs,
355
- ...block.props,
356
- }
357
- );
358
- }
359
-
360
- return true;
361
- },
362
- // Updates a block to the given specification if it's empty, otherwise creates a new block from that specification
363
- // below it.
364
- BNCreateOrUpdateBlock:
365
- (posInBlock, block) =>
366
- ({ state, chain }) => {
367
- const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
368
- if (blockInfo === undefined) {
369
- return false;
370
- }
371
-
372
- const { node, startPos, endPos } = blockInfo;
373
-
374
- if (node.textContent.length === 0) {
375
- const oldBlockContentPos = startPos + 1;
376
-
377
- return chain()
378
- .BNUpdateBlock(posInBlock, block)
379
- .setTextSelection(oldBlockContentPos)
380
- .run();
381
- } else {
382
- const newBlockInsertionPos = endPos + 1;
383
- const newBlockContentPos = newBlockInsertionPos + 1;
384
-
385
- return chain()
386
- .BNCreateBlock(newBlockInsertionPos)
387
- .BNUpdateBlock(newBlockContentPos, block)
388
- .setTextSelection(newBlockContentPos)
389
- .run();
390
- }
391
- },
392
365
  };
393
366
  },
394
367
 
@@ -0,0 +1,31 @@
1
+ import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem";
2
+ import { BlockNoteEditor } from "../../BlockNoteEditor";
3
+
4
+ /**
5
+ * A class that defines a slash command (/<command>).
6
+ *
7
+ * (Not to be confused with ProseMirror commands nor TipTap commands.)
8
+ */
9
+ export class BaseSlashMenuItem extends SuggestionItem {
10
+ /**
11
+ * Constructs a new slash-command.
12
+ *
13
+ * @param name The name of the command
14
+ * @param execute The callback for creating a new node
15
+ * @param aliases Aliases for this command
16
+ */
17
+ constructor(
18
+ public readonly name: string,
19
+ public readonly execute: (editor: BlockNoteEditor) => void,
20
+ public readonly aliases: string[] = []
21
+ ) {
22
+ super(name, (query: string): boolean => {
23
+ return (
24
+ this.name.toLowerCase().startsWith(query.toLowerCase()) ||
25
+ this.aliases.filter((alias) =>
26
+ alias.toLowerCase().startsWith(query.toLowerCase())
27
+ ).length !== 0
28
+ );
29
+ });
30
+ }
31
+ }