@blocknote/core 0.3.0 → 0.4.0

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 (55) hide show
  1. package/dist/blocknote.js +12508 -1276
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +50 -1
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +16 -5
  7. package/src/BlockNoteEditor.test.ts +12 -0
  8. package/src/BlockNoteEditor.ts +38 -15
  9. package/src/BlockNoteExtensions.ts +25 -21
  10. package/src/api/Editor.ts +142 -0
  11. package/src/api/blockManipulation/blockManipulation.ts +114 -0
  12. package/src/api/formatConversions/formatConversions.ts +86 -0
  13. package/src/api/formatConversions/removeUnderlinesRehypePlugin.ts +39 -0
  14. package/src/api/formatConversions/simplifyBlocksRehypePlugin.ts +125 -0
  15. package/src/api/nodeConversions/nodeConversions.ts +170 -0
  16. package/src/editor.module.css +7 -1
  17. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +7 -1
  18. package/src/extensions/Blocks/api/blockTypes.ts +85 -0
  19. package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -0
  20. package/src/extensions/Blocks/api/inlineContentTypes.ts +44 -0
  21. package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +4 -4
  22. package/src/extensions/Blocks/nodes/BlockContainer.ts +75 -25
  23. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +23 -5
  24. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +28 -6
  25. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -2
  26. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +3 -3
  27. package/src/extensions/SlashMenu/SlashMenuExtension.ts +7 -12
  28. package/src/extensions/SlashMenu/SlashMenuItem.ts +4 -1
  29. package/src/extensions/SlashMenu/{defaultCommands.tsx → defaultSlashCommands.tsx} +34 -17
  30. package/src/extensions/SlashMenu/index.ts +7 -4
  31. package/src/extensions/UniqueID/UniqueID.ts +1 -1
  32. package/src/index.ts +4 -2
  33. package/types/src/BlockNoteEditor.d.ts +13 -4
  34. package/types/src/BlockNoteEditor.test.d.ts +1 -0
  35. package/types/src/BlockNoteExtensions.d.ts +7 -3
  36. package/types/src/api/Editor.d.ts +73 -0
  37. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -0
  38. package/types/src/api/formatConversions/formatConversions.d.ts +6 -0
  39. package/types/src/api/formatConversions/removeUnderlinesRehypePlugin.d.ts +6 -0
  40. package/types/src/api/formatConversions/simplifyBlocksRehypePlugin.d.ts +16 -0
  41. package/types/src/api/nodeConversions/nodeConversions.d.ts +8 -0
  42. package/types/src/api/removeUnderlinesRehypePlugin.d.ts +6 -0
  43. package/types/src/api/simplifyBlocksRehypePlugin.d.ts +16 -0
  44. package/types/src/extensions/Blocks/api/apiTypes.d.ts +18 -0
  45. package/types/src/extensions/Blocks/api/blockTypes.d.ts +36 -0
  46. package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +4 -0
  47. package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +23 -0
  48. package/types/src/extensions/Blocks/api/styleTypes.d.ts +22 -0
  49. package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
  50. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -2
  51. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -3
  52. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -1
  53. package/types/src/extensions/SlashMenu/index.d.ts +3 -3
  54. package/types/src/index.d.ts +4 -2
  55. package/src/extensions/Blocks/apiTypes.ts +0 -48
@@ -0,0 +1,125 @@
1
+ import { Element as HASTElement, Parent as HASTParent } from "hast";
2
+ import { fromDom } from "hast-util-from-dom";
3
+
4
+ type SimplifyBlocksOptions = {
5
+ orderedListItemBlockTypes: Set<string>;
6
+ unorderedListItemBlockTypes: Set<string>;
7
+ };
8
+
9
+ /**
10
+ * Rehype plugin which converts the HTML output string rendered by BlockNote into a simplified structure which better
11
+ * follows HTML standards. It does several things:
12
+ * - Removes all block related div elements, leaving only the actual content inside the block.
13
+ * - Lifts nested blocks to a higher level for all block types that don't represent list items.
14
+ * - Wraps blocks which represent list items in corresponding ul/ol HTML elements and restructures them to comply
15
+ * with HTML list structure.
16
+ * @param options Options for specifying which block types represent ordered and unordered list items.
17
+ */
18
+ export function simplifyBlocks(options: SimplifyBlocksOptions) {
19
+ const listItemBlockTypes = new Set<string>([
20
+ ...options.orderedListItemBlockTypes,
21
+ ...options.unorderedListItemBlockTypes,
22
+ ]);
23
+
24
+ const simplifyBlocksHelper = (tree: HASTParent) => {
25
+ let numChildElements = tree.children.length;
26
+ let activeList: HASTElement | undefined;
27
+
28
+ for (let i = 0; i < numChildElements; i++) {
29
+ const blockOuter = tree.children[i] as HASTElement;
30
+ const blockContainer = blockOuter.children[0] as HASTElement;
31
+ const blockContent = blockContainer.children[0] as HASTElement;
32
+ const blockGroup =
33
+ blockContainer.children.length === 2
34
+ ? (blockContainer.children[1] as HASTElement)
35
+ : null;
36
+
37
+ const isListItemBlock = listItemBlockTypes.has(
38
+ blockContent.properties!["dataContentType"] as string
39
+ );
40
+
41
+ const listItemBlockType = isListItemBlock
42
+ ? options.orderedListItemBlockTypes.has(
43
+ blockContent.properties!["dataContentType"] as string
44
+ )
45
+ ? "ol"
46
+ : "ul"
47
+ : null;
48
+
49
+ // Plugin runs recursively to process nested blocks.
50
+ if (blockGroup !== null) {
51
+ simplifyBlocksHelper(blockGroup);
52
+ }
53
+
54
+ // Checks that there is an active list, but the block can't be added to it as it's of a different type.
55
+ if (activeList && activeList.tagName !== listItemBlockType) {
56
+ // Blocks that were copied into the list are removed and the list is inserted in their place.
57
+ tree.children.splice(
58
+ i - activeList.children.length,
59
+ activeList.children.length,
60
+ activeList
61
+ );
62
+
63
+ // Updates the current index and number of child elements.
64
+ const numElementsRemoved = activeList.children.length - 1;
65
+ i -= numElementsRemoved;
66
+ numChildElements -= numElementsRemoved;
67
+
68
+ activeList = undefined;
69
+ }
70
+
71
+ // Checks if the block represents a list item.
72
+ if (isListItemBlock) {
73
+ // Checks if a list isn't already active. We don't have to check if the block and the list are of the same
74
+ // type as this was already done earlier.
75
+ if (!activeList) {
76
+ // Creates a new list element to represent an active list.
77
+ activeList = fromDom(
78
+ document.createElement(listItemBlockType!)
79
+ ) as HASTElement;
80
+ }
81
+
82
+ // Creates a new list item element to represent the block.
83
+ const listItemElement = fromDom(
84
+ document.createElement("li")
85
+ ) as HASTElement;
86
+
87
+ // Adds only the content inside the block to the active list.
88
+ listItemElement.children.push(blockContent.children[0]);
89
+ // Nested blocks have already been processed in the recursive function call, so the resulting elements are
90
+ // also added to the active list.
91
+ if (blockGroup !== null) {
92
+ listItemElement.children.push(...blockGroup.children);
93
+ }
94
+
95
+ // Adds the list item representing the block to the active list.
96
+ activeList.children.push(listItemElement);
97
+ } else if (blockGroup !== null) {
98
+ // Lifts all children out of the current block, as only list items should allow nesting.
99
+ tree.children.splice(i + 1, 0, ...blockGroup.children);
100
+ // Replaces the block with only the content inside it.
101
+ tree.children[i] = blockContent.children[0];
102
+
103
+ // Updates the current index and number of child elements.
104
+ const numElementsAdded = blockGroup.children.length;
105
+ i += numElementsAdded;
106
+ numChildElements += numElementsAdded;
107
+ } else {
108
+ // Replaces the block with only the content inside it.
109
+ tree.children[i] = blockContent.children[0];
110
+ }
111
+ }
112
+
113
+ // Since the active list is only inserted after encountering a block which can't be added to it, there are cases
114
+ // where it remains un-inserted after processing all blocks, which are handled here.
115
+ if (activeList) {
116
+ tree.children.splice(
117
+ numChildElements - activeList.children.length,
118
+ activeList.children.length,
119
+ activeList
120
+ );
121
+ }
122
+ };
123
+
124
+ return simplifyBlocksHelper;
125
+ }
@@ -0,0 +1,170 @@
1
+ import { Node, Schema } from "prosemirror-model";
2
+ import {
3
+ Block,
4
+ blockProps,
5
+ PartialBlock,
6
+ } from "../../extensions/Blocks/api/blockTypes";
7
+ import {
8
+ InlineContent,
9
+ Style,
10
+ } from "../../extensions/Blocks/api/inlineContentTypes";
11
+ import { getBlockInfoFromPos } from "../../extensions/Blocks/helpers/getBlockInfoFromPos";
12
+ import UniqueID from "../../extensions/UniqueID/UniqueID";
13
+
14
+ export function blockToNode(block: PartialBlock, schema: Schema) {
15
+ let id = block.id;
16
+
17
+ if (id === undefined) {
18
+ id = UniqueID.options.generateID();
19
+ }
20
+
21
+ let content: Node[] = [];
22
+
23
+ if (typeof block.content === "string") {
24
+ content.push(schema.text(block.content));
25
+ } else if (typeof block.content === "object") {
26
+ for (const styledText of block.content) {
27
+ const marks = [];
28
+
29
+ for (const style of styledText.styles) {
30
+ marks.push(schema.mark(style.type, style.props));
31
+ }
32
+
33
+ content.push(schema.text(styledText.text, marks));
34
+ }
35
+ }
36
+
37
+ const contentNode = schema.nodes[block.type].create(block.props, content);
38
+
39
+ const children: Node[] = [];
40
+
41
+ if (block.children) {
42
+ for (const child of block.children) {
43
+ children.push(blockToNode(child, schema));
44
+ }
45
+ }
46
+
47
+ const groupNode = schema.nodes["blockGroup"].create({}, children);
48
+
49
+ return schema.nodes["blockContainer"].create(
50
+ {
51
+ id: id,
52
+ ...block.props,
53
+ },
54
+ children.length > 0 ? [contentNode, groupNode] : contentNode
55
+ );
56
+ }
57
+
58
+ export function getNodeById(
59
+ id: string,
60
+ doc: Node
61
+ ): { node: Node; posBeforeNode: number } {
62
+ let targetNode: Node | undefined = undefined;
63
+ let posBeforeNode: number | undefined = undefined;
64
+
65
+ doc.firstChild!.descendants((node, pos) => {
66
+ // Skips traversing nodes after node with target ID has been found.
67
+ if (targetNode) {
68
+ return false;
69
+ }
70
+
71
+ // Keeps traversing nodes if block with target ID has not been found.
72
+ if (node.type.name !== "blockContainer" || node.attrs.id !== id) {
73
+ return true;
74
+ }
75
+
76
+ targetNode = node;
77
+ posBeforeNode = pos + 1;
78
+
79
+ return false;
80
+ });
81
+
82
+ if (targetNode === undefined || posBeforeNode === undefined) {
83
+ throw Error("Could not find block in the editor with matching ID.");
84
+ }
85
+
86
+ return {
87
+ node: targetNode,
88
+ posBeforeNode: posBeforeNode,
89
+ };
90
+ }
91
+
92
+ export function nodeToBlock(
93
+ node: Node,
94
+ blockCache?: WeakMap<Node, Block>
95
+ ): Block {
96
+ if (node.type.name !== "blockContainer") {
97
+ throw Error(
98
+ "Node must be of type blockContainer, but is of type" +
99
+ node.type.name +
100
+ "."
101
+ );
102
+ }
103
+
104
+ const cachedBlock = blockCache?.get(node);
105
+
106
+ if (cachedBlock) {
107
+ return cachedBlock;
108
+ }
109
+
110
+ const blockInfo = getBlockInfoFromPos(node, 0)!;
111
+
112
+ let id = blockInfo.id;
113
+
114
+ // Only used for blocks converted from other formats.
115
+ if (id === null) {
116
+ id = UniqueID.options.generateID();
117
+ }
118
+
119
+ const props: any = {};
120
+ for (const [attr, value] of Object.entries({
121
+ ...blockInfo.node.attrs,
122
+ ...blockInfo.contentNode.attrs,
123
+ })) {
124
+ if (!(blockInfo.contentType.name in blockProps)) {
125
+ throw Error(
126
+ "Block is of an unrecognized type: " + blockInfo.contentType.name
127
+ );
128
+ }
129
+
130
+ const validAttrs = blockProps[blockInfo.contentType.name as Block["type"]];
131
+
132
+ if (validAttrs.has(attr)) {
133
+ props[attr] = value;
134
+ }
135
+ }
136
+
137
+ const content: InlineContent[] = [];
138
+ blockInfo.contentNode.content.forEach((node) => {
139
+ const styles: Style[] = [];
140
+
141
+ for (const mark of node.marks) {
142
+ styles.push({
143
+ type: mark.type.name,
144
+ props: mark.attrs,
145
+ } as Style);
146
+ }
147
+
148
+ content.push({
149
+ text: node.textContent,
150
+ styles,
151
+ });
152
+ });
153
+
154
+ const children: Block[] = [];
155
+ for (let i = 0; i < blockInfo.numChildBlocks; i++) {
156
+ children.push(nodeToBlock(blockInfo.node.lastChild!.child(i)));
157
+ }
158
+
159
+ const block: Block = {
160
+ id,
161
+ type: blockInfo.contentType.name as Block["type"],
162
+ props,
163
+ content,
164
+ children,
165
+ };
166
+
167
+ blockCache?.set(node, block);
168
+
169
+ return block;
170
+ }
@@ -24,7 +24,8 @@ Tippy popups that are appended to document.body directly
24
24
  box-sizing: inherit;
25
25
  }
26
26
 
27
- .bnEditor, .dragPreview {
27
+ .bnEditor,
28
+ .dragPreview {
28
29
  /* Define a set of colors to be used throughout the app for consistency
29
30
  see https://atlassian.design/foundations/color for more info */
30
31
  --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
@@ -38,3 +39,8 @@ Tippy popups that are appended to document.body directly
38
39
 
39
40
  color: rgb(60, 65, 73);
40
41
  }
42
+
43
+ .dragPreview {
44
+ position: absolute;
45
+ top: -1000px;
46
+ }
@@ -24,6 +24,7 @@ const nodeAttributes: Record<string, string> = {
24
24
  * Solution: When attributes change on a node, this plugin sets a data-* attribute with the "previous" value. This way we can still use CSS transitions. (See block.module.css)
25
25
  */
26
26
  export const PreviousBlockTypePlugin = () => {
27
+ let timeout: any;
27
28
  return new Plugin({
28
29
  key: PLUGIN_KEY,
29
30
  view(_editorView) {
@@ -32,13 +33,18 @@ export const PreviousBlockTypePlugin = () => {
32
33
  if (this.key?.getState(view.state).updatedBlocks.size > 0) {
33
34
  // use setTimeout 0 to clear the decorations so that at least
34
35
  // for one DOM-render the decorations have been applied
35
- setTimeout(() => {
36
+ timeout = setTimeout(() => {
36
37
  view.dispatch(
37
38
  view.state.tr.setMeta(PLUGIN_KEY, { clearUpdate: true })
38
39
  );
39
40
  }, 0);
40
41
  }
41
42
  },
43
+ destroy: () => {
44
+ if (timeout) {
45
+ clearTimeout(timeout);
46
+ }
47
+ },
42
48
  };
43
49
  },
44
50
  state: {
@@ -0,0 +1,85 @@
1
+ /** Define the main block types **/
2
+
3
+ import { InlineContent } from "./inlineContentTypes";
4
+
5
+ export type BlockTemplate<
6
+ // Type of the block.
7
+ // Examples might include: "paragraph", "heading", or "bulletListItem".
8
+ Type extends string,
9
+ // Changeable props which affect the block's behaviour or appearance.
10
+ // An example might be: { textAlignment: "left" | "right" | "center" } for a paragraph block.
11
+ Props extends Record<string, string>
12
+ > = {
13
+ id: string;
14
+ type: Type;
15
+ props: Props;
16
+ content: InlineContent[];
17
+ children: Block[];
18
+ };
19
+
20
+ export type GlobalProps = {
21
+ backgroundColor: string;
22
+ textColor: string;
23
+ textAlignment: "left" | "center" | "right" | "justify";
24
+ };
25
+
26
+ export type NumberedListItemBlock = BlockTemplate<
27
+ "numberedListItem",
28
+ GlobalProps
29
+ >;
30
+
31
+ export type BulletListItemBlock = BlockTemplate<"bulletListItem", GlobalProps>;
32
+
33
+ export type HeadingBlock = BlockTemplate<
34
+ "heading",
35
+ GlobalProps & {
36
+ level: "1" | "2" | "3";
37
+ }
38
+ >;
39
+
40
+ export type ParagraphBlock = BlockTemplate<"paragraph", GlobalProps>;
41
+
42
+ export type Block =
43
+ | ParagraphBlock
44
+ | HeadingBlock
45
+ | BulletListItemBlock
46
+ | NumberedListItemBlock;
47
+
48
+ /** Define "Partial Blocks", these are for updating or creating blocks */
49
+ export type PartialBlockTemplate<B extends Block> = B extends Block
50
+ ? Partial<Omit<B, "props" | "children" | "content" | "type">> & {
51
+ type: B["type"];
52
+ props?: Partial<B["props"]>;
53
+ content?: string | B["content"];
54
+ children?: PartialBlock[];
55
+ }
56
+ : never;
57
+
58
+ export type PartialBlock = PartialBlockTemplate<Block>;
59
+
60
+ export type BlockPropsTemplate<Props> = Props extends Block["props"]
61
+ ? keyof Props
62
+ : never;
63
+
64
+ /**
65
+ * Expose blockProps. This is currently not very nice, but it's expected this
66
+ * will change anyway once we allow for custom blocks
67
+ */
68
+
69
+ export const globalProps: Array<keyof GlobalProps> = [
70
+ "backgroundColor",
71
+ "textColor",
72
+ "textAlignment",
73
+ ];
74
+
75
+ export const blockProps: Record<Block["type"], Set<string>> = {
76
+ paragraph: new Set<keyof ParagraphBlock["props"]>([...globalProps]),
77
+ heading: new Set<keyof HeadingBlock["props"]>([
78
+ ...globalProps,
79
+ "level" as const,
80
+ ]),
81
+ numberedListItem: new Set<keyof NumberedListItemBlock["props"]>([
82
+ ...globalProps,
83
+ ]),
84
+ bulletListItem: new Set<keyof BulletListItemBlock["props"]>([...globalProps]),
85
+ };
@@ -0,0 +1,5 @@
1
+ import { Block } from "./blockTypes";
2
+
3
+ export type TextCursorPosition = {
4
+ block: Block;
5
+ };
@@ -0,0 +1,44 @@
1
+ export type StyleTemplate<
2
+ // Type of the style.
3
+ // Examples might include: "bold", "italic", or "textColor".
4
+ Type extends string,
5
+ // Changeable props which affect the style's appearance.
6
+ // An example might be: { color: string } for a textColor style.
7
+ Props extends Record<string, string>
8
+ > = {
9
+ type: Type;
10
+ props: Props;
11
+ };
12
+
13
+ export type Bold = StyleTemplate<"bold", {}>;
14
+
15
+ export type Italic = StyleTemplate<"italic", {}>;
16
+
17
+ export type Underline = StyleTemplate<"underline", {}>;
18
+
19
+ export type Strikethrough = StyleTemplate<"strike", {}>;
20
+
21
+ export type TextColor = StyleTemplate<"textColor", { color: string }>;
22
+
23
+ export type BackgroundColor = StyleTemplate<
24
+ "backgroundColor",
25
+ { color: string }
26
+ >;
27
+
28
+ export type Link = StyleTemplate<"link", { href: string }>;
29
+
30
+ export type Style =
31
+ | Bold
32
+ | Italic
33
+ | Underline
34
+ | Strikethrough
35
+ | TextColor
36
+ | BackgroundColor
37
+ | Link;
38
+
39
+ export type StyledText = {
40
+ text: string;
41
+ styles: Style[];
42
+ };
43
+
44
+ export type InlineContent = StyledText;
@@ -22,7 +22,7 @@ export function getBlockInfoFromPos(
22
22
  doc: Node,
23
23
  posInBlock: number
24
24
  ): BlockInfo | undefined {
25
- if (posInBlock <= 0 || posInBlock > doc.nodeSize) {
25
+ if (posInBlock < 0 || posInBlock > doc.nodeSize) {
26
26
  return undefined;
27
27
  }
28
28
 
@@ -32,11 +32,11 @@ export function getBlockInfoFromPos(
32
32
  let node = $pos.node(maxDepth);
33
33
  let depth = maxDepth;
34
34
 
35
- while (depth >= 0) {
36
- // If the outermost node is not a block, it means the position does not lie within a block.
37
- if (depth === 0) {
35
+ while (true) {
36
+ if (depth < 0) {
38
37
  return undefined;
39
38
  }
39
+
40
40
  if (node.type.name === "blockContainer") {
41
41
  break;
42
42
  }
@@ -1,7 +1,8 @@
1
1
  import { mergeAttributes, Node } from "@tiptap/core";
2
- import { Fragment, Slice } from "prosemirror-model";
2
+ import { Fragment, Node as PMNode, Slice } from "prosemirror-model";
3
3
  import { TextSelection } from "prosemirror-state";
4
- import { BlockUpdate } from "../apiTypes";
4
+ import { blockToNode } from "../../../api/nodeConversions/nodeConversions";
5
+ import { PartialBlock } from "../api/blockTypes";
5
6
  import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos";
6
7
  import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin";
7
8
  import styles from "./Block.module.css";
@@ -19,13 +20,10 @@ declare module "@tiptap/core" {
19
20
  BNDeleteBlock: (posInBlock: number) => ReturnType;
20
21
  BNMergeBlocks: (posBetweenBlocks: number) => ReturnType;
21
22
  BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType;
22
- BNUpdateBlock: (
23
- posInBlock: number,
24
- blockUpdate: BlockUpdate
25
- ) => ReturnType;
23
+ BNUpdateBlock: (posInBlock: number, block: PartialBlock) => ReturnType;
26
24
  BNCreateOrUpdateBlock: (
27
25
  posInBlock: number,
28
- blockUpdate: BlockUpdate
26
+ block: PartialBlock
29
27
  ) => ReturnType;
30
28
  };
31
29
  }
@@ -197,8 +195,7 @@ export const BlockContainer = Node.create<IBlock>({
197
195
  }
198
196
 
199
197
  // Deletes next block and adds its text content to the nearest previous block.
200
- // TODO: Is there any situation where we need the whole block content, not just text? Implementation for this
201
- // is trickier.
198
+ // TODO: Use slices.
202
199
  if (dispatch) {
203
200
  state.tr.deleteRange(startPos, startPos + contentNode.nodeSize);
204
201
  state.tr.insertText(contentNode.textContent, prevBlockEndPos - 1);
@@ -283,35 +280,88 @@ export const BlockContainer = Node.create<IBlock>({
283
280
 
284
281
  return true;
285
282
  },
286
- // Updates the type and attributes of a block at a given position.
283
+ // Updates a block to the given specification.
287
284
  BNUpdateBlock:
288
- (posInBlock, blockUpdate) =>
285
+ (posInBlock, block) =>
289
286
  ({ state, dispatch }) => {
290
287
  const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
291
288
  if (blockInfo === undefined) {
292
289
  return false;
293
290
  }
294
291
 
295
- const { node, startPos, contentNode } = blockInfo;
292
+ const { startPos, endPos, node, contentNode } = blockInfo;
296
293
 
297
294
  if (dispatch) {
298
- state.tr.setBlockType(
299
- startPos + 1,
300
- startPos + contentNode.nodeSize + 1,
301
- state.schema.node(blockUpdate.type).type,
302
- {
303
- ...node.attrs,
304
- ...blockUpdate.props,
295
+ // Adds blockGroup node with child blocks if necessary.
296
+ if (block.children !== undefined) {
297
+ const childNodes = [];
298
+
299
+ // Creates ProseMirror nodes for each child block, including their descendants.
300
+ for (const child of block.children) {
301
+ childNodes.push(blockToNode(child, state.schema));
305
302
  }
306
- );
303
+
304
+ // Checks if a blockGroup node already exists.
305
+ if (node.childCount === 2) {
306
+ // Replaces all child nodes in the existing blockGroup with the ones created earlier.
307
+ state.tr.replace(
308
+ startPos + contentNode.nodeSize + 1,
309
+ endPos - 1,
310
+ new Slice(Fragment.from(childNodes), 0, 0)
311
+ );
312
+ } else {
313
+ // Inserts a new blockGroup containing the child nodes created earlier.
314
+ state.tr.insert(
315
+ startPos + contentNode.nodeSize,
316
+ state.schema.nodes["blockGroup"].create({}, childNodes)
317
+ );
318
+ }
319
+ }
320
+
321
+ // Replaces the blockContent node's content if necessary.
322
+ if (block.content !== undefined) {
323
+ let content: PMNode[] = [];
324
+
325
+ // Checks if the provided content is a string or InlineContent[] type.
326
+ if (typeof block.content === "string") {
327
+ // Adds a single text node with no marks to the content.
328
+ content.push(state.schema.text(block.content));
329
+ } else {
330
+ // Adds a text node with the provided styles converted into marks to the content, for each InlineContent
331
+ // object.
332
+ for (const styledText of block.content) {
333
+ const marks = [];
334
+
335
+ for (const style of styledText.styles) {
336
+ marks.push(state.schema.mark(style.type, style.props));
337
+ }
338
+
339
+ content.push(state.schema.text(styledText.text, marks));
340
+ }
341
+ }
342
+
343
+ // Replaces the contents of the blockContent node with the previously created text node(s).
344
+ state.tr.replace(
345
+ startPos + 1,
346
+ startPos + contentNode.nodeSize - 1,
347
+ new Slice(Fragment.from(content), 0, 0)
348
+ );
349
+ }
350
+
351
+ // Changes the block type and adds the provided props as node attributes. Also preserves all existing node
352
+ // attributes that are compatible with the new type.
353
+ state.tr.setNodeMarkup(startPos, state.schema.nodes[block.type], {
354
+ ...contentNode.attrs,
355
+ ...block.props,
356
+ });
307
357
  }
308
358
 
309
359
  return true;
310
360
  },
311
- // Changes the block at a given position to a given content type if it's empty, otherwise creates a new block of
312
- // that type below it.
361
+ // Updates a block to the given specification if it's empty, otherwise creates a new block from that specification
362
+ // below it.
313
363
  BNCreateOrUpdateBlock:
314
- (posInBlock, blockUpdate) =>
364
+ (posInBlock, block) =>
315
365
  ({ state, chain }) => {
316
366
  const blockInfo = getBlockInfoFromPos(state.doc, posInBlock);
317
367
  if (blockInfo === undefined) {
@@ -324,7 +374,7 @@ export const BlockContainer = Node.create<IBlock>({
324
374
  const oldBlockContentPos = startPos + 1;
325
375
 
326
376
  return chain()
327
- .BNUpdateBlock(posInBlock, blockUpdate)
377
+ .BNUpdateBlock(posInBlock, block)
328
378
  .setTextSelection(oldBlockContentPos)
329
379
  .run();
330
380
  } else {
@@ -333,7 +383,7 @@ export const BlockContainer = Node.create<IBlock>({
333
383
 
334
384
  return chain()
335
385
  .BNCreateBlock(newBlockInsertionPos)
336
- .BNUpdateBlock(newBlockContentPos, blockUpdate)
386
+ .BNUpdateBlock(newBlockContentPos, block)
337
387
  .setTextSelection(newBlockContentPos)
338
388
  .run();
339
389
  }