@blocknote/core 0.3.0 → 0.4.2

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 (95) hide show
  1. package/dist/blocknote.js +12698 -1341
  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 +39 -15
  9. package/src/BlockNoteExtensions.ts +36 -32
  10. package/src/api/Editor.ts +226 -0
  11. package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +616 -0
  12. package/src/api/blockManipulation/blockManipulation.test.ts +172 -0
  13. package/src/api/blockManipulation/blockManipulation.ts +125 -0
  14. package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +346 -0
  15. package/src/api/formatConversions/formatConversions.test.ts +766 -0
  16. package/src/api/formatConversions/formatConversions.ts +86 -0
  17. package/src/api/formatConversions/removeUnderlinesRehypePlugin.ts +39 -0
  18. package/src/api/formatConversions/simplifyBlocksRehypePlugin.ts +125 -0
  19. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +268 -0
  20. package/src/api/nodeConversions/nodeConversions.test.ts +244 -0
  21. package/src/api/nodeConversions/nodeConversions.ts +279 -0
  22. package/src/api/nodeConversions/testUtil.ts +61 -0
  23. package/src/api/util/nodeUtil.ts +38 -0
  24. package/src/editor.module.css +8 -1
  25. package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +7 -1
  26. package/src/extensions/Blocks/api/blockTypes.ts +90 -0
  27. package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -0
  28. package/src/extensions/Blocks/api/inlineContentTypes.ts +35 -0
  29. package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +4 -4
  30. package/src/extensions/Blocks/nodes/Block.module.css +39 -36
  31. package/src/extensions/Blocks/nodes/BlockContainer.ts +74 -23
  32. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +23 -5
  33. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +28 -6
  34. package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +149 -87
  35. package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -2
  36. package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +3 -3
  37. package/src/extensions/SlashMenu/SlashMenuExtension.ts +7 -12
  38. package/src/extensions/SlashMenu/SlashMenuItem.ts +4 -1
  39. package/src/extensions/SlashMenu/{defaultCommands.tsx → defaultSlashCommands.tsx} +34 -17
  40. package/src/extensions/SlashMenu/index.ts +7 -4
  41. package/src/extensions/UniqueID/UniqueID.ts +1 -1
  42. package/src/index.ts +4 -2
  43. package/src/shared/utils.ts +6 -0
  44. package/types/src/BlockNoteEditor.d.ts +13 -4
  45. package/types/src/BlockNoteEditor.test.d.ts +1 -0
  46. package/types/src/BlockNoteExtensions.d.ts +7 -3
  47. package/types/src/api/Editor.d.ts +93 -0
  48. package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -0
  49. package/types/src/api/blockManipulation/blockManipulation.test.d.ts +1 -0
  50. package/types/src/api/formatConversions/formatConversions.d.ts +6 -0
  51. package/types/src/api/formatConversions/formatConversions.test.d.ts +1 -0
  52. package/types/src/api/formatConversions/removeUnderlinesRehypePlugin.d.ts +6 -0
  53. package/types/src/api/formatConversions/simplifyBlocksRehypePlugin.d.ts +16 -0
  54. package/types/src/api/nodeConversions/nodeConversions.d.ts +15 -0
  55. package/types/src/api/nodeConversions/nodeConversions.test.d.ts +1 -0
  56. package/types/src/api/nodeConversions/testUtil.d.ts +2 -0
  57. package/types/src/api/util/nodeUtil.d.ts +8 -0
  58. package/types/src/extensions/Blocks/api/blockTypes.d.ts +37 -0
  59. package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +4 -0
  60. package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +29 -0
  61. package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
  62. package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +15 -0
  63. package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -2
  64. package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -3
  65. package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -1
  66. package/types/src/extensions/SlashMenu/index.d.ts +3 -3
  67. package/types/src/index.d.ts +4 -2
  68. package/types/src/shared/utils.d.ts +3 -0
  69. package/src/extensions/Blocks/apiTypes.ts +0 -48
  70. package/types/src/EditorElement.d.ts +0 -7
  71. package/types/src/api/Document.d.ts +0 -5
  72. package/types/src/extensions/Blocks/BlockAttributes.d.ts +0 -2
  73. package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +0 -24
  74. package/types/src/extensions/Blocks/apiTypes.d.ts +0 -16
  75. package/types/src/extensions/Blocks/nodes/Block.d.ts +0 -24
  76. package/types/src/extensions/Blocks/nodes/BlockContent/BlockContentTypes.d.ts +0 -4
  77. package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContentTypes.d.ts +0 -4
  78. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContentTypes.d.ts +0 -2
  79. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContentTypes.d.ts +0 -2
  80. package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContentTypes.d.ts +0 -2
  81. package/types/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.d.ts +0 -8
  82. package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.d.ts +0 -8
  83. package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/OrderedListItemIndexPlugin.d.ts +0 -2
  84. package/types/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.d.ts +0 -6
  85. package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +0 -8
  86. package/types/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.d.ts +0 -27
  87. package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +0 -44
  88. package/types/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.d.ts +0 -12
  89. package/types/src/extensions/DraggableBlocks/DragMenuFactoryTypes.d.ts +0 -18
  90. package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +0 -8
  91. package/types/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.d.ts +0 -11
  92. package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +0 -11
  93. package/types/src/extensions/Paragraph/FixedParagraph.d.ts +0 -1
  94. package/types/src/extensions/SlashMenu/defaultCommands.d.ts +0 -8
  95. package/types/src/utils.d.ts +0 -2
@@ -0,0 +1,244 @@
1
+ import { Editor } from "@tiptap/core";
2
+ import { Node } from "prosemirror-model";
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
4
+ import { BlockNoteEditor, PartialBlock } from "../..";
5
+ import UniqueID from "../../extensions/UniqueID/UniqueID";
6
+ import { blockToNode, nodeToBlock } from "./nodeConversions";
7
+ import { partialBlockToBlockForTesting } from "./testUtil";
8
+
9
+ let editor: BlockNoteEditor;
10
+ let tt: Editor;
11
+
12
+ let simpleBlock: PartialBlock;
13
+ let simpleNode: Node;
14
+
15
+ let complexBlock: PartialBlock;
16
+ let complexNode: Node;
17
+
18
+ beforeEach(() => {
19
+ (window as Window & { __TEST_OPTIONS?: {} }).__TEST_OPTIONS = {};
20
+
21
+ editor = new BlockNoteEditor();
22
+ tt = editor._tiptapEditor;
23
+
24
+ simpleBlock = {
25
+ type: "paragraph",
26
+ };
27
+ simpleNode = tt.schema.nodes["blockContainer"].create(
28
+ { id: UniqueID.options.generateID() },
29
+ tt.schema.nodes["paragraph"].create()
30
+ );
31
+
32
+ complexBlock = {
33
+ type: "heading",
34
+ props: {
35
+ backgroundColor: "blue",
36
+ textColor: "yellow",
37
+ textAlignment: "right",
38
+ level: "2",
39
+ },
40
+ content: [
41
+ {
42
+ type: "text",
43
+ text: "Heading ",
44
+ styles: {
45
+ bold: true,
46
+ underline: true,
47
+ },
48
+ },
49
+ {
50
+ type: "text",
51
+ text: "2",
52
+ styles: {
53
+ italic: true,
54
+ strike: true,
55
+ },
56
+ },
57
+ ],
58
+ children: [
59
+ {
60
+ type: "paragraph",
61
+ props: {
62
+ backgroundColor: "red",
63
+ },
64
+ content: "Paragraph",
65
+ children: [],
66
+ },
67
+ {
68
+ type: "bulletListItem",
69
+ },
70
+ ],
71
+ };
72
+ complexNode = tt.schema.nodes["blockContainer"].create(
73
+ {
74
+ id: UniqueID.options.generateID(),
75
+ backgroundColor: "blue",
76
+ textColor: "yellow",
77
+ },
78
+ [
79
+ tt.schema.nodes["heading"].create(
80
+ { textAlignment: "right", level: "2" },
81
+ [
82
+ tt.schema.text("Heading ", [
83
+ tt.schema.mark("bold"),
84
+ tt.schema.mark("underline"),
85
+ ]),
86
+ tt.schema.text("2", [
87
+ tt.schema.mark("italic"),
88
+ tt.schema.mark("strike"),
89
+ ]),
90
+ ]
91
+ ),
92
+ tt.schema.nodes["blockGroup"].create({}, [
93
+ tt.schema.nodes["blockContainer"].create(
94
+ { id: UniqueID.options.generateID(), backgroundColor: "red" },
95
+ [tt.schema.nodes["paragraph"].create({}, tt.schema.text("Paragraph"))]
96
+ ),
97
+ tt.schema.nodes["blockContainer"].create(
98
+ { id: UniqueID.options.generateID() },
99
+ [tt.schema.nodes["bulletListItem"].create()]
100
+ ),
101
+ ]),
102
+ ]
103
+ );
104
+ });
105
+
106
+ afterEach(() => {
107
+ tt.destroy();
108
+ editor = undefined as any;
109
+ tt = undefined as any;
110
+
111
+ delete (window as Window & { __TEST_OPTIONS?: {} }).__TEST_OPTIONS;
112
+ });
113
+
114
+ describe("Simple ProseMirror Node Conversions", () => {
115
+ it("Convert simple block to node", async () => {
116
+ const firstNodeConversion = blockToNode(simpleBlock, tt.schema);
117
+
118
+ expect(firstNodeConversion).toMatchSnapshot();
119
+ });
120
+
121
+ it("Convert simple node to block", async () => {
122
+ const firstBlockConversion = nodeToBlock(simpleNode);
123
+
124
+ expect(firstBlockConversion).toMatchSnapshot();
125
+
126
+ const firstNodeConversion = blockToNode(firstBlockConversion, tt.schema);
127
+
128
+ expect(firstNodeConversion).toStrictEqual(simpleNode);
129
+ });
130
+ });
131
+
132
+ describe("Complex ProseMirror Node Conversions", () => {
133
+ it("Convert complex block to node", async () => {
134
+ const firstNodeConversion = blockToNode(complexBlock, tt.schema);
135
+
136
+ expect(firstNodeConversion).toMatchSnapshot();
137
+ });
138
+
139
+ it("Convert complex node to block", async () => {
140
+ const firstBlockConversion = nodeToBlock(complexNode);
141
+
142
+ expect(firstBlockConversion).toMatchSnapshot();
143
+
144
+ const firstNodeConversion = blockToNode(firstBlockConversion, tt.schema);
145
+
146
+ expect(firstNodeConversion).toStrictEqual(complexNode);
147
+ });
148
+ });
149
+
150
+ describe("links", () => {
151
+ it("Convert a block with link", async () => {
152
+ const block: PartialBlock = {
153
+ id: UniqueID.options.generateID(),
154
+ type: "paragraph",
155
+ content: [
156
+ {
157
+ type: "link",
158
+ href: "https://www.website.com",
159
+ content: "Website",
160
+ },
161
+ ],
162
+ };
163
+ const node = blockToNode(block, tt.schema);
164
+ expect(node).toMatchSnapshot();
165
+ const outputBlock = nodeToBlock(node);
166
+
167
+ // Temporary fix to set props to {}, because at this point
168
+ // we don't have an easy way to access default props at runtime,
169
+ // so partialBlockToBlockForTesting will not set them.
170
+ (outputBlock as any).props = {};
171
+ const fullOriginalBlock = partialBlockToBlockForTesting(block);
172
+
173
+ expect(outputBlock).toStrictEqual(fullOriginalBlock);
174
+ });
175
+
176
+ it("Convert link block with marks", async () => {
177
+ const block: PartialBlock = {
178
+ id: UniqueID.options.generateID(),
179
+ type: "paragraph",
180
+ content: [
181
+ {
182
+ type: "link",
183
+ href: "https://www.website.com",
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: "Web",
188
+ styles: {
189
+ bold: true,
190
+ },
191
+ },
192
+ {
193
+ type: "text",
194
+ text: "site",
195
+ styles: {},
196
+ },
197
+ ],
198
+ },
199
+ ],
200
+ };
201
+ const node = blockToNode(block, tt.schema);
202
+ // expect(node).toMatchSnapshot();
203
+ const outputBlock = nodeToBlock(node);
204
+
205
+ // Temporary fix to set props to {}, because at this point
206
+ // we don't have an easy way to access default props at runtime,
207
+ // so partialBlockToBlockForTesting will not set them.
208
+ (outputBlock as any).props = {};
209
+ const fullOriginalBlock = partialBlockToBlockForTesting(block);
210
+
211
+ expect(outputBlock).toStrictEqual(fullOriginalBlock);
212
+ });
213
+
214
+ it("Convert two adjacent links in a block", async () => {
215
+ const block: PartialBlock = {
216
+ id: UniqueID.options.generateID(),
217
+ type: "paragraph",
218
+ content: [
219
+ {
220
+ type: "link",
221
+ href: "https://www.website.com",
222
+ content: "Website",
223
+ },
224
+ {
225
+ type: "link",
226
+ href: "https://www.website2.com",
227
+ content: "Website2",
228
+ },
229
+ ],
230
+ };
231
+
232
+ const node = blockToNode(block, tt.schema);
233
+ expect(node).toMatchSnapshot();
234
+ const outputBlock = nodeToBlock(node);
235
+
236
+ // Temporary fix to set props to {}, because at this point
237
+ // we don't have an easy way to access default props at runtime,
238
+ // so partialBlockToBlockForTesting will not set them.
239
+ (outputBlock as any).props = {};
240
+ const fullOriginalBlock = partialBlockToBlockForTesting(block);
241
+
242
+ expect(outputBlock).toStrictEqual(fullOriginalBlock);
243
+ });
244
+ });
@@ -0,0 +1,279 @@
1
+ import { Mark } from "@tiptap/pm/model";
2
+ import { Node, Schema } from "prosemirror-model";
3
+ import {
4
+ Block,
5
+ blockProps,
6
+ PartialBlock,
7
+ } from "../../extensions/Blocks/api/blockTypes";
8
+ import {
9
+ ColorStyles,
10
+ InlineContent,
11
+ Link,
12
+ PartialInlineContent,
13
+ PartialLink,
14
+ StyledText,
15
+ Styles,
16
+ ToggledStyles,
17
+ } from "../../extensions/Blocks/api/inlineContentTypes";
18
+ import { getBlockInfoFromPos } from "../../extensions/Blocks/helpers/getBlockInfoFromPos";
19
+ import UniqueID from "../../extensions/UniqueID/UniqueID";
20
+ import { UnreachableCaseError } from "../../shared/utils";
21
+
22
+ const toggleStyles = new Set<ToggledStyles>([
23
+ "bold",
24
+ "italic",
25
+ "underline",
26
+ "strike",
27
+ ]);
28
+ const colorStyles = new Set<ColorStyles>(["textColor", "backgroundColor"]);
29
+
30
+ /**
31
+ * Convert a StyledText inline element to a
32
+ * prosemirror text node with the appropriate marks
33
+ */
34
+ function styledTextToNode(styledText: StyledText, schema: Schema): Node {
35
+ const marks: Mark[] = [];
36
+
37
+ for (const [style, value] of Object.entries(styledText.styles)) {
38
+ if (toggleStyles.has(style as ToggledStyles)) {
39
+ marks.push(schema.mark(style));
40
+ } else if (colorStyles.has(style as ColorStyles)) {
41
+ marks.push(schema.mark(style, { color: value }));
42
+ }
43
+ }
44
+
45
+ return schema.text(styledText.text, marks);
46
+ }
47
+
48
+ /**
49
+ * Converts a Link inline content element to
50
+ * prosemirror text nodes with the appropriate marks
51
+ */
52
+ function linkToNodes(link: PartialLink, schema: Schema): Node[] {
53
+ const linkMark = schema.marks.link.create({
54
+ href: link.href,
55
+ });
56
+
57
+ return styledTextArrayToNodes(link.content, schema).map((node) => {
58
+ return node.mark([...node.marks, linkMark]);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Converts an array of StyledText inline content elements to
64
+ * prosemirror text nodes with the appropriate marks
65
+ */
66
+ function styledTextArrayToNodes(
67
+ content: string | StyledText[],
68
+ schema: Schema
69
+ ): Node[] {
70
+ let nodes: Node[] = [];
71
+
72
+ if (typeof content === "string") {
73
+ nodes.push(schema.text(content));
74
+ return nodes;
75
+ }
76
+
77
+ for (const styledText of content) {
78
+ nodes.push(styledTextToNode(styledText, schema));
79
+ }
80
+ return nodes;
81
+ }
82
+
83
+ /**
84
+ * converts an array of inline content elements to prosemirror nodes
85
+ */
86
+ export function inlineContentToNodes(
87
+ blockContent: PartialInlineContent[],
88
+ schema: Schema
89
+ ): Node[] {
90
+ let nodes: Node[] = [];
91
+
92
+ for (const content of blockContent) {
93
+ if (content.type === "link") {
94
+ nodes.push(...linkToNodes(content, schema));
95
+ } else if (content.type === "text") {
96
+ nodes.push(...styledTextArrayToNodes([content], schema));
97
+ } else {
98
+ throw new UnreachableCaseError(content);
99
+ }
100
+ }
101
+ return nodes;
102
+ }
103
+
104
+ /**
105
+ * Converts a BlockNote block to a TipTap node.
106
+ */
107
+ export function blockToNode(block: PartialBlock, schema: Schema) {
108
+ let id = block.id;
109
+
110
+ if (id === undefined) {
111
+ id = UniqueID.options.generateID();
112
+ }
113
+
114
+ let type = block.type;
115
+
116
+ if (type === undefined) {
117
+ type = "paragraph";
118
+ }
119
+
120
+ let contentNode: Node;
121
+
122
+ if (!block.content) {
123
+ contentNode = schema.nodes[type].create(block.props);
124
+ } else if (typeof block.content === "string") {
125
+ contentNode = schema.nodes[type].create(
126
+ block.props,
127
+ schema.text(block.content)
128
+ );
129
+ } else {
130
+ const nodes = inlineContentToNodes(block.content, schema);
131
+ contentNode = schema.nodes[type].create(block.props, nodes);
132
+ }
133
+
134
+ const children: Node[] = [];
135
+
136
+ if (block.children) {
137
+ for (const child of block.children) {
138
+ children.push(blockToNode(child, schema));
139
+ }
140
+ }
141
+
142
+ const groupNode = schema.nodes["blockGroup"].create({}, children);
143
+
144
+ return schema.nodes["blockContainer"].create(
145
+ {
146
+ id: id,
147
+ ...block.props,
148
+ },
149
+ children.length > 0 ? [contentNode, groupNode] : contentNode
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Converts an internal (prosemirror) content node to a BlockNote InlineContent array.
155
+ */
156
+ function contentNodeToInlineContent(contentNode: Node) {
157
+ const content: InlineContent[] = [];
158
+
159
+ let currentLink: Link | undefined = undefined;
160
+
161
+ // Most of the logic below is for handling links because in ProseMirror links are marks
162
+ // while in BlockNote links are a type of inline content
163
+ contentNode.content.forEach((node) => {
164
+ const styles: Styles = {};
165
+
166
+ let linkMark: Mark | undefined;
167
+ for (const mark of node.marks) {
168
+ if (mark.type.name === "link") {
169
+ linkMark = mark;
170
+ } else if (toggleStyles.has(mark.type.name as ToggledStyles)) {
171
+ styles[mark.type.name as ToggledStyles] = true;
172
+ } else if (colorStyles.has(mark.type.name as ColorStyles)) {
173
+ styles[mark.type.name as ColorStyles] = mark.attrs.color;
174
+ } else {
175
+ throw Error("Mark is of an unrecognized type: " + mark.type.name);
176
+ }
177
+ }
178
+
179
+ if (linkMark && currentLink && linkMark.attrs.href === currentLink.href) {
180
+ // if the node is a link that matches the current link, add it to the current link
181
+ currentLink.content.push({
182
+ type: "text",
183
+ text: node.textContent,
184
+ styles,
185
+ });
186
+ } else if (linkMark) {
187
+ // if the node is a link that doesn't match the current link, create a new link
188
+ currentLink = {
189
+ type: "link",
190
+ href: linkMark.attrs.href,
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: node.textContent,
195
+ styles,
196
+ },
197
+ ],
198
+ };
199
+ content.push(currentLink);
200
+ } else {
201
+ // if the node is not a link, add it to the content
202
+ content.push({
203
+ type: "text",
204
+ text: node.textContent,
205
+ styles,
206
+ });
207
+ currentLink = undefined;
208
+ }
209
+ });
210
+ return content;
211
+ }
212
+
213
+ /**
214
+ * Convert a TipTap node to a BlockNote block.
215
+ */
216
+ export function nodeToBlock(
217
+ node: Node,
218
+ blockCache?: WeakMap<Node, Block>
219
+ ): Block {
220
+ if (node.type.name !== "blockContainer") {
221
+ throw Error(
222
+ "Node must be of type blockContainer, but is of type" +
223
+ node.type.name +
224
+ "."
225
+ );
226
+ }
227
+
228
+ const cachedBlock = blockCache?.get(node);
229
+
230
+ if (cachedBlock) {
231
+ return cachedBlock;
232
+ }
233
+
234
+ const blockInfo = getBlockInfoFromPos(node, 0)!;
235
+
236
+ let id = blockInfo.id;
237
+
238
+ // Only used for blocks converted from other formats.
239
+ if (id === null) {
240
+ id = UniqueID.options.generateID();
241
+ }
242
+
243
+ const props: any = {};
244
+ for (const [attr, value] of Object.entries({
245
+ ...blockInfo.node.attrs,
246
+ ...blockInfo.contentNode.attrs,
247
+ })) {
248
+ if (!(blockInfo.contentType.name in blockProps)) {
249
+ throw Error(
250
+ "Block is of an unrecognized type: " + blockInfo.contentType.name
251
+ );
252
+ }
253
+
254
+ const validAttrs = blockProps[blockInfo.contentType.name as Block["type"]];
255
+
256
+ if (validAttrs.has(attr)) {
257
+ props[attr] = value;
258
+ }
259
+ }
260
+
261
+ const content = contentNodeToInlineContent(blockInfo.contentNode);
262
+
263
+ const children: Block[] = [];
264
+ for (let i = 0; i < blockInfo.numChildBlocks; i++) {
265
+ children.push(nodeToBlock(blockInfo.node.lastChild!.child(i)));
266
+ }
267
+
268
+ const block: Block = {
269
+ id,
270
+ type: blockInfo.contentType.name as Block["type"],
271
+ props,
272
+ content,
273
+ children,
274
+ };
275
+
276
+ blockCache?.set(node, block);
277
+
278
+ return block;
279
+ }
@@ -0,0 +1,61 @@
1
+ import { Block, PartialBlock } from "../../extensions/Blocks/api/blockTypes";
2
+ import {
3
+ InlineContent,
4
+ PartialInlineContent,
5
+ StyledText,
6
+ } from "../../extensions/Blocks/api/inlineContentTypes";
7
+
8
+ function textShorthandToStyledText(
9
+ content: string | StyledText[] = ""
10
+ ): StyledText[] {
11
+ if (typeof content === "string") {
12
+ return [
13
+ {
14
+ type: "text",
15
+ text: content,
16
+ styles: {},
17
+ },
18
+ ];
19
+ }
20
+ return content;
21
+ }
22
+
23
+ function partialContentToInlineContent(
24
+ content: string | PartialInlineContent[] = ""
25
+ ): InlineContent[] {
26
+ if (typeof content === "string") {
27
+ return textShorthandToStyledText(content);
28
+ }
29
+
30
+ return content.map((partialContent) => {
31
+ if (partialContent.type === "link") {
32
+ return {
33
+ ...partialContent,
34
+ content: textShorthandToStyledText(partialContent.content),
35
+ };
36
+ } else {
37
+ return partialContent;
38
+ }
39
+ });
40
+ }
41
+
42
+ export function partialBlockToBlockForTesting(
43
+ partialBlock: PartialBlock
44
+ ): Block {
45
+ const withDefaults = {
46
+ id: "",
47
+ type: "paragraph" as any,
48
+ // because at this point we don't have an easy way to access default props at runtime,
49
+ // partialBlockToBlockForTesting will not set them.
50
+ props: {} as any,
51
+ content: [],
52
+ children: [],
53
+ ...partialBlock,
54
+ };
55
+
56
+ return {
57
+ ...withDefaults,
58
+ content: partialContentToInlineContent(withDefaults.content),
59
+ children: withDefaults.children.map(partialBlockToBlockForTesting),
60
+ };
61
+ }
@@ -0,0 +1,38 @@
1
+ import { Node } from "prosemirror-model";
2
+
3
+ /**
4
+ * Get a TipTap node by id
5
+ */
6
+ export function getNodeById(
7
+ id: string,
8
+ doc: Node
9
+ ): { node: Node; posBeforeNode: number } {
10
+ let targetNode: Node | undefined = undefined;
11
+ let posBeforeNode: number | undefined = undefined;
12
+
13
+ doc.firstChild!.descendants((node, pos) => {
14
+ // Skips traversing nodes after node with target ID has been found.
15
+ if (targetNode) {
16
+ return false;
17
+ }
18
+
19
+ // Keeps traversing nodes if block with target ID has not been found.
20
+ if (node.type.name !== "blockContainer" || node.attrs.id !== id) {
21
+ return true;
22
+ }
23
+
24
+ targetNode = node;
25
+ posBeforeNode = pos + 1;
26
+
27
+ return false;
28
+ });
29
+
30
+ if (targetNode === undefined || posBeforeNode === undefined) {
31
+ throw Error("Could not find block in the editor with matching ID.");
32
+ }
33
+
34
+ return {
35
+ node: targetNode,
36
+ posBeforeNode: posBeforeNode,
37
+ };
38
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  .bnEditor {
4
4
  outline: none;
5
+ margin-left: 50px;
5
6
  }
6
7
 
7
8
  /*
@@ -24,7 +25,8 @@ Tippy popups that are appended to document.body directly
24
25
  box-sizing: inherit;
25
26
  }
26
27
 
27
- .bnEditor, .dragPreview {
28
+ .bnEditor,
29
+ .dragPreview {
28
30
  /* Define a set of colors to be used throughout the app for consistency
29
31
  see https://atlassian.design/foundations/color for more info */
30
32
  --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
@@ -38,3 +40,8 @@ Tippy popups that are appended to document.body directly
38
40
 
39
41
  color: rgb(60, 65, 73);
40
42
  }
43
+
44
+ .dragPreview {
45
+ position: absolute;
46
+ top: -1000px;
47
+ }
@@ -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: {