Skip to content

Commit b582be8

Browse files
committed
Merge branch 'default-blocks' of github.com:TypeCellOS/BlockNote into default-blocks
2 parents caf7ffd + b17a8d8 commit b582be8

File tree

28 files changed

+584
-201
lines changed

28 files changed

+584
-201
lines changed

packages/core/src/api/exporters/html/util/serializeBlocksExternalHTML.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DOMSerializer, Fragment } from "prosemirror-model";
1+
import { DOMSerializer, Fragment, Node } from "prosemirror-model";
22

33
import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
44
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
@@ -13,6 +13,7 @@ import {
1313
inlineContentToNodes,
1414
tableContentToNodes,
1515
} from "../../../nodeConversions/blockToNode.js";
16+
import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js";
1617

1718
function addAttributesAndRemoveClasses(element: HTMLElement) {
1819
// Removes all BlockNote specific class names.
@@ -38,7 +39,7 @@ export function serializeInlineContentExternalHTML<
3839
serializer: DOMSerializer,
3940
options?: { document?: Document },
4041
) {
41-
let nodes: any;
42+
let nodes: Node[];
4243

4344
// TODO: reuse function from nodeconversions?
4445
if (!blockContent) {
@@ -53,16 +54,67 @@ export function serializeInlineContentExternalHTML<
5354
throw new UnreachableCaseError(blockContent.type);
5455
}
5556

56-
// We call the prosemirror serializer here because it handles Marks and Inline Content nodes nicely.
57-
// If we'd want to support custom serialization or externalHTML for Inline Content, we'd have to implement
58-
// a custom serializer here.
59-
const dom = serializer.serializeFragment(Fragment.from(nodes), options);
57+
// Check if any of the nodes are custom inline content with toExternalHTML
58+
const doc = options?.document ?? document;
59+
const fragment = doc.createDocumentFragment();
60+
61+
for (const node of nodes) {
62+
// Check if this is a custom inline content node with toExternalHTML
63+
if (
64+
node.type &&
65+
node.type.name &&
66+
node.type.name in editor.schema.inlineContentSchema
67+
) {
68+
const inlineContentImplementation =
69+
editor.schema.inlineContentSpecs[node.type.name]?.implementation;
70+
71+
if (inlineContentImplementation?.toExternalHTML) {
72+
// Convert the node to inline content format
73+
const inlineContent = nodeToCustomInlineContent(
74+
node,
75+
editor.schema.inlineContentSchema,
76+
editor.schema.styleSchema,
77+
);
78+
79+
// Use the custom toExternalHTML method
80+
const output = inlineContentImplementation.toExternalHTML(
81+
inlineContent as any,
82+
editor as any,
83+
);
84+
85+
if (output) {
86+
fragment.appendChild(output.dom);
6087

61-
if (dom.nodeType === 1 /* Node.ELEMENT_NODE */) {
62-
addAttributesAndRemoveClasses(dom as HTMLElement);
88+
// If contentDOM exists, render the inline content into it
89+
if (output.contentDOM) {
90+
const contentFragment = serializer.serializeFragment(
91+
node.content,
92+
options,
93+
);
94+
output.contentDOM.dataset.editable = "";
95+
output.contentDOM.appendChild(contentFragment);
96+
}
97+
continue;
98+
}
99+
}
100+
}
101+
102+
// Fall back to default serialization for this node
103+
const nodeFragment = serializer.serializeFragment(
104+
Fragment.from([node]),
105+
options,
106+
);
107+
fragment.appendChild(nodeFragment);
108+
}
109+
110+
if (
111+
fragment.childNodes.length === 1 &&
112+
fragment.firstChild?.nodeType === 1 /* Node.ELEMENT_NODE */
113+
) {
114+
addAttributesAndRemoveClasses(fragment.firstChild as HTMLElement);
63115
}
64116

65-
return dom;
117+
return fragment;
66118
}
67119

68120
/**

packages/core/src/api/exporters/html/util/serializeBlocksInternalHTML.ts

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DOMSerializer, Fragment } from "prosemirror-model";
1+
import { DOMSerializer, Fragment, Node } from "prosemirror-model";
22

33
import { PartialBlock } from "../../../../blocks/defaultBlocks.js";
44
import type { BlockNoteEditor } from "../../../../editor/BlockNoteEditor.js";
@@ -13,6 +13,7 @@ import {
1313
tableContentToNodes,
1414
} from "../../../nodeConversions/blockToNode.js";
1515

16+
import { nodeToCustomInlineContent } from "../../../nodeConversions/nodeToBlock.js";
1617
export function serializeInlineContentInternalHTML<
1718
BSchema extends BlockSchema,
1819
I extends InlineContentSchema,
@@ -24,7 +25,7 @@ export function serializeInlineContentInternalHTML<
2425
blockType?: string,
2526
options?: { document?: Document },
2627
) {
27-
let nodes: any;
28+
let nodes: Node[];
2829

2930
// TODO: reuse function from nodeconversions?
3031
if (!blockContent) {
@@ -39,12 +40,60 @@ export function serializeInlineContentInternalHTML<
3940
throw new UnreachableCaseError(blockContent.type);
4041
}
4142

42-
// We call the prosemirror serializer here because it handles Marks and Inline Content nodes nicely.
43-
// If we'd want to support custom serialization or externalHTML for Inline Content, we'd have to implement
44-
// a custom serializer here.
45-
const dom = serializer.serializeFragment(Fragment.from(nodes), options);
43+
// Check if any of the nodes are custom inline content with toExternalHTML
44+
const doc = options?.document ?? document;
45+
const fragment = doc.createDocumentFragment();
46+
47+
for (const node of nodes) {
48+
// Check if this is a custom inline content node with toExternalHTML
49+
if (
50+
node.type &&
51+
node.type.name &&
52+
editor.schema.inlineContentSchema[node.type.name]
53+
) {
54+
const inlineContentImplementation =
55+
editor.inlineContentImplementations[node.type.name]?.implementation;
56+
57+
if (inlineContentImplementation?.toExternalHTML) {
58+
// Convert the node to inline content format
59+
const inlineContent = nodeToCustomInlineContent(
60+
node,
61+
editor.schema.inlineContentSchema,
62+
editor.schema.styleSchema,
63+
);
64+
65+
// Use the custom toExternalHTML method
66+
const output = inlineContentImplementation.toExternalHTML(
67+
inlineContent as any,
68+
editor as any,
69+
);
70+
71+
if (output) {
72+
fragment.appendChild(output.dom);
73+
74+
// If contentDOM exists, render the inline content into it
75+
if (output.contentDOM) {
76+
const contentFragment = serializer.serializeFragment(
77+
node.content,
78+
options,
79+
);
80+
output.contentDOM.dataset.editable = "";
81+
output.contentDOM.appendChild(contentFragment);
82+
}
83+
continue;
84+
}
85+
}
86+
}
4687

47-
return dom;
88+
// Fall back to default serialization for this node
89+
const nodeFragment = serializer.serializeFragment(
90+
Fragment.from([node]),
91+
options,
92+
);
93+
fragment.appendChild(nodeFragment);
94+
}
95+
96+
return fragment;
4897
}
4998

5099
function serializeBlock<

packages/core/src/blocks/Table/block.ts

Lines changed: 52 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,8 @@ import { TableHeader } from "@tiptap/extension-table-header";
44
import { DOMParser, Fragment, Node as PMNode, Schema } from "prosemirror-model";
55
import { TableView } from "prosemirror-tables";
66
import { NodeView } from "prosemirror-view";
7-
import {
8-
BlockSpec,
9-
createBlockSpecFromStronglyTypedTiptapNode,
10-
createStronglyTypedTiptapNode,
11-
} from "../../schema/index.js";
127
import { createBlockNoteExtension } from "../../editor/BlockNoteExtension.js";
8+
import { createBlockSpecFromTiptapNode } from "../../schema/index.js";
139
import { mergeCSSClasses } from "../../util/browser.js";
1410
import { createDefaultBlockDOMOutputSpec } from "../defaultBlockHelpers.js";
1511
import { defaultProps } from "../defaultProps.js";
@@ -19,7 +15,7 @@ export const tablePropSchema = {
1915
textColor: defaultProps.textColor,
2016
};
2117

22-
export const TableNode = createStronglyTypedTiptapNode({
18+
export const TableNode = Node.create({
2319
name: "table",
2420
content: "tableRow+",
2521
group: "blockContent",
@@ -136,7 +132,7 @@ export const TableNode = createStronglyTypedTiptapNode({
136132
},
137133
});
138134

139-
const TableParagraphNode = createStronglyTypedTiptapNode({
135+
const TableParagraphNode = Node.create({
140136
name: "tableParagraph",
141137
group: "tableContent",
142138
content: "inline*",
@@ -246,59 +242,52 @@ function parseTableContent(node: HTMLElement, schema: Schema) {
246242
}
247243

248244
export const createTableBlockSpec = () =>
249-
createBlockSpecFromStronglyTypedTiptapNode(TableNode, tablePropSchema, [
250-
createBlockNoteExtension({
251-
key: "table-extensions",
252-
tiptapExtensions: [
253-
TableExtension,
254-
TableParagraphNode,
255-
TableHeader.extend({
256-
/**
257-
* We allow table headers and cells to have multiple tableContent nodes because
258-
* when merging cells, prosemirror-tables will concat the contents of the cells naively.
259-
* This would cause that content to overflow into other cells when prosemirror tries to enforce the cell structure.
260-
*
261-
* So, we manually fix this up when reading back in the `nodeToBlock` and only ever place a single tableContent back into the cell.
262-
*/
263-
content: "tableContent+",
264-
parseHTML() {
265-
return [
266-
{
267-
tag: "th",
268-
// As `th` elements can contain multiple paragraphs, we need to merge their contents
269-
// into a single one so that ProseMirror can parse everything correctly.
270-
getContent: (node, schema) =>
271-
parseTableContent(node as HTMLElement, schema),
272-
},
273-
];
274-
},
275-
}),
276-
TableCell.extend({
277-
content: "tableContent+",
278-
parseHTML() {
279-
return [
280-
{
281-
tag: "td",
282-
// As `td` elements can contain multiple paragraphs, we need to merge their contents
283-
// into a single one so that ProseMirror can parse everything correctly.
284-
getContent: (node, schema) =>
285-
parseTableContent(node as HTMLElement, schema),
286-
},
287-
];
288-
},
289-
}),
290-
TableRowNode,
291-
],
292-
}),
293-
]) as unknown as BlockSpec<
294-
"table",
295-
{
296-
textColor: {
297-
default: "default";
298-
};
299-
}
300-
> & {
301-
config: {
302-
content: "table";
303-
};
304-
};
245+
createBlockSpecFromTiptapNode(
246+
{ node: TableNode, type: "table", content: "table" },
247+
tablePropSchema,
248+
[
249+
createBlockNoteExtension({
250+
key: "table-extensions",
251+
tiptapExtensions: [
252+
TableExtension,
253+
TableParagraphNode,
254+
TableHeader.extend({
255+
/**
256+
* We allow table headers and cells to have multiple tableContent nodes because
257+
* when merging cells, prosemirror-tables will concat the contents of the cells naively.
258+
* This would cause that content to overflow into other cells when prosemirror tries to enforce the cell structure.
259+
*
260+
* So, we manually fix this up when reading back in the `nodeToBlock` and only ever place a single tableContent back into the cell.
261+
*/
262+
content: "tableContent+",
263+
parseHTML() {
264+
return [
265+
{
266+
tag: "th",
267+
// As `th` elements can contain multiple paragraphs, we need to merge their contents
268+
// into a single one so that ProseMirror can parse everything correctly.
269+
getContent: (node, schema) =>
270+
parseTableContent(node as HTMLElement, schema),
271+
},
272+
];
273+
},
274+
}),
275+
TableCell.extend({
276+
content: "tableContent+",
277+
parseHTML() {
278+
return [
279+
{
280+
tag: "td",
281+
// As `td` elements can contain multiple paragraphs, we need to merge their contents
282+
// into a single one so that ProseMirror can parse everything correctly.
283+
getContent: (node, schema) =>
284+
parseTableContent(node as HTMLElement, schema),
285+
},
286+
];
287+
},
288+
}),
289+
TableRowNode,
290+
],
291+
}),
292+
],
293+
);

packages/core/src/schema/blocks/createSpec.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { Editor } from "@tiptap/core";
1+
import { Editor, Node } from "@tiptap/core";
22
import { DOMParser, Fragment, TagParseRule } from "@tiptap/pm/model";
33
import { NodeView } from "@tiptap/pm/view";
44
import { mergeParagraphs } from "../../blocks/defaultBlockHelpers.js";
55
import { BlockNoteExtension } from "../../editor/BlockNoteExtension.js";
66
import { PropSchema } from "../propTypes.js";
77
import {
8-
createStronglyTypedTiptapNode,
9-
createTypedBlockSpec,
108
getBlockFromPos,
119
propsToAttributes,
1210
wrapInBlockStructure,
1311
} from "./internal.js";
14-
import { BlockConfig, BlockImplementation, BlockSpec } from "./types.js";
12+
import {
13+
BlockConfig,
14+
BlockImplementation,
15+
BlockSpec,
16+
LooseBlockSpec,
17+
} from "./types.js";
1518

1619
// Function that causes events within non-selectable blocks to be handled by the
1720
// browser instead of the editor.
@@ -130,13 +133,10 @@ export function addNodeAndExtensionsToSpec<
130133
blockImplementation: BlockImplementation<TName, TProps, TContent>,
131134
extensions?: BlockNoteExtension<any>[],
132135
priority?: number,
133-
) {
136+
): LooseBlockSpec<TName, TProps, TContent> {
134137
const node =
135-
// Only table already has a node defined, so we just use that node instead of wrapping it from scratch
136-
((blockImplementation as any).node as ReturnType<
137-
typeof createStronglyTypedTiptapNode
138-
>) ||
139-
createStronglyTypedTiptapNode({
138+
((blockImplementation as any).node as Node) ||
139+
Node.create({
140140
name: blockConfig.type,
141141
content: (blockConfig.content === "inline"
142142
? "inline*"
@@ -215,9 +215,9 @@ export function addNodeAndExtensionsToSpec<
215215
);
216216
}
217217

218-
return createTypedBlockSpec(
219-
blockConfig,
220-
{
218+
return {
219+
config: blockConfig,
220+
implementation: {
221221
node,
222222
render(block, editor) {
223223
const blockContentDOMAttributes =
@@ -254,7 +254,7 @@ export function addNodeAndExtensionsToSpec<
254254
},
255255
},
256256
extensions,
257-
);
257+
};
258258
}
259259

260260
/**

0 commit comments

Comments
 (0)