@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.
- package/dist/blocknote.js +12698 -1341
- package/dist/blocknote.js.map +1 -1
- package/dist/blocknote.umd.cjs +50 -1
- package/dist/blocknote.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +16 -5
- package/src/BlockNoteEditor.test.ts +12 -0
- package/src/BlockNoteEditor.ts +39 -15
- package/src/BlockNoteExtensions.ts +36 -32
- package/src/api/Editor.ts +226 -0
- package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +616 -0
- package/src/api/blockManipulation/blockManipulation.test.ts +172 -0
- package/src/api/blockManipulation/blockManipulation.ts +125 -0
- package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +346 -0
- package/src/api/formatConversions/formatConversions.test.ts +766 -0
- package/src/api/formatConversions/formatConversions.ts +86 -0
- package/src/api/formatConversions/removeUnderlinesRehypePlugin.ts +39 -0
- package/src/api/formatConversions/simplifyBlocksRehypePlugin.ts +125 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +268 -0
- package/src/api/nodeConversions/nodeConversions.test.ts +244 -0
- package/src/api/nodeConversions/nodeConversions.ts +279 -0
- package/src/api/nodeConversions/testUtil.ts +61 -0
- package/src/api/util/nodeUtil.ts +38 -0
- package/src/editor.module.css +8 -1
- package/src/extensions/Blocks/PreviousBlockTypePlugin.ts +7 -1
- package/src/extensions/Blocks/api/blockTypes.ts +90 -0
- package/src/extensions/Blocks/api/cursorPositionTypes.ts +5 -0
- package/src/extensions/Blocks/api/inlineContentTypes.ts +35 -0
- package/src/extensions/Blocks/helpers/getBlockInfoFromPos.ts +4 -4
- package/src/extensions/Blocks/nodes/Block.module.css +39 -36
- package/src/extensions/Blocks/nodes/BlockContainer.ts +74 -23
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +23 -5
- package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +28 -6
- package/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +149 -87
- package/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +2 -2
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +3 -3
- package/src/extensions/SlashMenu/SlashMenuExtension.ts +7 -12
- package/src/extensions/SlashMenu/SlashMenuItem.ts +4 -1
- package/src/extensions/SlashMenu/{defaultCommands.tsx → defaultSlashCommands.tsx} +34 -17
- package/src/extensions/SlashMenu/index.ts +7 -4
- package/src/extensions/UniqueID/UniqueID.ts +1 -1
- package/src/index.ts +4 -2
- package/src/shared/utils.ts +6 -0
- package/types/src/BlockNoteEditor.d.ts +13 -4
- package/types/src/BlockNoteEditor.test.d.ts +1 -0
- package/types/src/BlockNoteExtensions.d.ts +7 -3
- package/types/src/api/Editor.d.ts +93 -0
- package/types/src/api/blockManipulation/blockManipulation.d.ts +6 -0
- package/types/src/api/blockManipulation/blockManipulation.test.d.ts +1 -0
- package/types/src/api/formatConversions/formatConversions.d.ts +6 -0
- package/types/src/api/formatConversions/formatConversions.test.d.ts +1 -0
- package/types/src/api/formatConversions/removeUnderlinesRehypePlugin.d.ts +6 -0
- package/types/src/api/formatConversions/simplifyBlocksRehypePlugin.d.ts +16 -0
- package/types/src/api/nodeConversions/nodeConversions.d.ts +15 -0
- package/types/src/api/nodeConversions/nodeConversions.test.d.ts +1 -0
- package/types/src/api/nodeConversions/testUtil.d.ts +2 -0
- package/types/src/api/util/nodeUtil.d.ts +8 -0
- package/types/src/extensions/Blocks/api/blockTypes.d.ts +37 -0
- package/types/src/extensions/Blocks/api/cursorPositionTypes.d.ts +4 -0
- package/types/src/extensions/Blocks/api/inlineContentTypes.d.ts +29 -0
- package/types/src/extensions/Blocks/nodes/BlockContainer.d.ts +3 -3
- package/types/src/extensions/DraggableBlocks/DraggableBlocksPlugin.d.ts +15 -0
- package/types/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.d.ts +2 -2
- package/types/src/extensions/SlashMenu/SlashMenuExtension.d.ts +1 -3
- package/types/src/extensions/SlashMenu/SlashMenuItem.d.ts +4 -1
- package/types/src/extensions/SlashMenu/index.d.ts +3 -3
- package/types/src/index.d.ts +4 -2
- package/types/src/shared/utils.d.ts +3 -0
- package/src/extensions/Blocks/apiTypes.ts +0 -48
- package/types/src/EditorElement.d.ts +0 -7
- package/types/src/api/Document.d.ts +0 -5
- package/types/src/extensions/Blocks/BlockAttributes.d.ts +0 -2
- package/types/src/extensions/Blocks/MultipleNodeSelection.d.ts +0 -24
- package/types/src/extensions/Blocks/apiTypes.d.ts +0 -16
- package/types/src/extensions/Blocks/nodes/Block.d.ts +0 -24
- package/types/src/extensions/Blocks/nodes/BlockContent/BlockContentTypes.d.ts +0 -4
- package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContentTypes.d.ts +0 -4
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContentTypes.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContentTypes.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContentTypes.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.d.ts +0 -8
- package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.d.ts +0 -8
- package/types/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/OrderedListItemIndexPlugin.d.ts +0 -2
- package/types/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.d.ts +0 -6
- package/types/src/extensions/BubbleMenu/BubbleMenuExtension.d.ts +0 -8
- package/types/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.d.ts +0 -27
- package/types/src/extensions/BubbleMenu/BubbleMenuPlugin.d.ts +0 -44
- package/types/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.d.ts +0 -12
- package/types/src/extensions/DraggableBlocks/DragMenuFactoryTypes.d.ts +0 -18
- package/types/src/extensions/Hyperlinks/HyperlinkMark.d.ts +0 -8
- package/types/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.d.ts +0 -11
- package/types/src/extensions/Hyperlinks/HyperlinkMenuPlugin.d.ts +0 -11
- package/types/src/extensions/Paragraph/FixedParagraph.d.ts +0 -1
- package/types/src/extensions/SlashMenu/defaultCommands.d.ts +0 -8
- 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
|
+
}
|
package/src/editor.module.css
CHANGED
|
@@ -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,
|
|
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: {
|