Skip to content

Commit 474897b

Browse files
committed
feat: support internal HTML as different from external HTML for inline content
1 parent eb049e2 commit 474897b

File tree

15 files changed

+133
-56
lines changed

15 files changed

+133
-56
lines changed

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,9 @@ export function serializeInlineContentExternalHTML<
6060

6161
for (const node of nodes) {
6262
// 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-
) {
63+
if (editor.schema.inlineContentSchema[node.type.name]) {
6864
const inlineContentImplementation =
69-
editor.schema.inlineContentSpecs[node.type.name]?.implementation;
65+
editor.schema.inlineContentSpecs[node.type.name].implementation;
7066

7167
if (inlineContentImplementation?.toExternalHTML) {
7268
// Convert the node to inline content format

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ export function serializeInlineContentInternalHTML<
4747
for (const node of nodes) {
4848
// Check if this is a custom inline content node with toExternalHTML
4949
if (
50-
node.type &&
51-
node.type.name &&
50+
node.type.name !== "text" &&
51+
node.type.name !== "link" &&
5252
editor.schema.inlineContentSchema[node.type.name]
5353
) {
5454
const inlineContentImplementation =
55-
editor.inlineContentImplementations[node.type.name]?.implementation;
55+
editor.schema.inlineContentSpecs[node.type.name].implementation;
5656

57-
if (inlineContentImplementation?.toExternalHTML) {
57+
if (inlineContentImplementation) {
5858
// Convert the node to inline content format
5959
const inlineContent = nodeToCustomInlineContent(
6060
node,
@@ -63,8 +63,15 @@ export function serializeInlineContentInternalHTML<
6363
);
6464

6565
// Use the custom toExternalHTML method
66-
const output = inlineContentImplementation.toExternalHTML(
66+
const output = inlineContentImplementation.render.call(
67+
{
68+
renderType: "dom",
69+
props: undefined,
70+
},
6771
inlineContent as any,
72+
() => {
73+
// No-op
74+
},
6875
editor as any,
6976
);
7077

@@ -122,7 +129,14 @@ function serializeBlock<
122129
}
123130

124131
const impl = editor.blockImplementations[block.type as any].implementation;
125-
const ret = impl.render?.call({}, { ...block, props } as any, editor as any);
132+
const ret = impl.render.call(
133+
{
134+
renderType: "dom",
135+
props: undefined,
136+
},
137+
{ ...block, props } as any,
138+
editor as any,
139+
);
126140

127141
if (ret.contentDOM && block.content) {
128142
const ic = serializeInlineContentInternalHTML(

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import {
1919
PartialCustomInlineContentFromConfig,
2020
} from "./types.js";
2121

22-
// TODO: support serialization
23-
2422
export type CustomInlineContentImplementation<
2523
T extends CustomInlineContentConfig,
2624
S extends StyleSchema,
@@ -154,7 +152,8 @@ export function createInlineContentSpec<
154152
renderHTML({ node }) {
155153
const editor = this.options.editor;
156154

157-
const output = inlineContentImplementation.render(
155+
const output = inlineContentImplementation.render.call(
156+
{ renderType: "dom", props: undefined },
158157
nodeToCustomInlineContent(
159158
node,
160159
editor.schema.inlineContentSchema,
@@ -175,10 +174,12 @@ export function createInlineContentSpec<
175174
},
176175

177176
addNodeView() {
178-
return ({ node, getPos }) => {
177+
return (props) => {
178+
const { node, getPos } = props;
179179
const editor = this.options.editor as BlockNoteEditor<any, any, S>;
180180

181-
const output = inlineContentImplementation.render(
181+
const output = inlineContentImplementation.render.call(
182+
{ renderType: "nodeView", props },
182183
nodeToCustomInlineContent(
183184
node,
184185
editor.schema.inlineContentSchema,
@@ -209,6 +210,22 @@ export function createInlineContentSpec<
209210
return createInlineContentSpecFromTipTapNode(
210211
node,
211212
inlineContentConfig.propSchema,
212-
{ toExternalHTML: inlineContentImplementation.toExternalHTML },
213+
{
214+
toExternalHTML: inlineContentImplementation.toExternalHTML,
215+
render(inlineContent, updateInlineContent, editor) {
216+
const output = inlineContentImplementation.render(
217+
inlineContent,
218+
updateInlineContent,
219+
editor,
220+
);
221+
222+
return addInlineContentAttributes(
223+
output,
224+
inlineContentConfig.type,
225+
inlineContent.props,
226+
inlineContentConfig.propSchema,
227+
);
228+
},
229+
},
213230
) as InlineContentSpec<T>;
214231
}

packages/core/src/schema/inlineContent/internal.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { camelToDataKebab } from "../../util/string.js";
44
import { PropSchema, Props } from "../propTypes.js";
55
import {
66
CustomInlineContentConfig,
7-
InlineContentConfig,
87
InlineContentImplementation,
98
InlineContentSchemaFromSpecs,
109
InlineContentSpec,
@@ -91,8 +90,8 @@ export function createInlineContentSpecFromTipTapNode<
9190
>(
9291
node: T,
9392
propSchema: P,
94-
implementation?: Omit<
95-
InlineContentImplementation<InlineContentConfig>,
93+
implementation: Omit<
94+
InlineContentImplementation<CustomInlineContentConfig>,
9695
"node"
9796
>,
9897
) {

packages/core/src/schema/inlineContent/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Node } from "@tiptap/core";
22
import { PropSchema, Props } from "../propTypes.js";
33
import { StyleSchema, Styles } from "../styles/types.js";
44
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
5+
import { ViewMutationRecord } from "prosemirror-view";
56

67
export type CustomInlineContentConfig = {
78
type: string;
@@ -31,6 +32,16 @@ export type InlineContentImplementation<T extends InlineContentConfig> =
3132
contentDOM?: HTMLElement;
3233
}
3334
| undefined;
35+
render: (
36+
inlineContent: any,
37+
updateInlineContent: (update: any) => void,
38+
editor: BlockNoteEditor<any, any, any>,
39+
) => {
40+
dom: HTMLElement | DocumentFragment;
41+
contentDOM?: HTMLElement;
42+
ignoreMutation?: (mutation: ViewMutationRecord) => boolean;
43+
destroy?: () => void;
44+
};
3445
};
3546

3647
export type InlineContentSchemaWithInlineContent<

packages/react/src/schema/ReactInlineContentSpec.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
camelToDataKebab,
66
createInternalInlineContentSpec,
77
CustomInlineContentConfig,
8+
CustomInlineContentImplementation,
89
getInlineContentParseRules,
910
InlineContentFromConfig,
1011
InlineContentSchemaWithInlineContent,
@@ -53,7 +54,7 @@ export type ReactInlineContentImplementation<
5354
> = {
5455
render: FC<ReactCustomInlineContentRenderProps<T, S>>;
5556
toExternalHTML?: FC<ReactCustomInlineContentRenderProps<T, S>>;
56-
};
57+
} & Omit<CustomInlineContentImplementation<T, S>, "render" | "toExternalHTML">;
5758

5859
// Function that adds a wrapper with necessary classes and attributes to the
5960
// component returned from a custom inline content's 'render' function, to
@@ -123,7 +124,10 @@ export function createReactInlineContentSpec<
123124
},
124125

125126
parseHTML() {
126-
return getInlineContentParseRules(inlineContentConfig);
127+
return getInlineContentParseRules(
128+
inlineContentConfig,
129+
inlineContentImplementation.parse,
130+
);
127131
},
128132

129133
renderHTML({ node }) {
@@ -228,6 +232,31 @@ export function createReactInlineContentSpec<
228232
inlineContentConfig as CustomInlineContentConfig,
229233
{
230234
node,
235+
render(inlineContent, updateInlineContent, editor) {
236+
const Content = inlineContentImplementation.render;
237+
const output = renderToDOMSpec((ref) => {
238+
return (
239+
<InlineContentWrapper
240+
inlineContentProps={inlineContent.props}
241+
inlineContentType={inlineContentConfig.type}
242+
propSchema={inlineContentConfig.propSchema}
243+
>
244+
<Content
245+
contentRef={(element) => {
246+
ref(element);
247+
if (element) {
248+
element.dataset.editable = "";
249+
}
250+
}}
251+
editor={editor}
252+
inlineContent={inlineContent}
253+
updateInlineContent={updateInlineContent}
254+
/>
255+
</InlineContentWrapper>
256+
);
257+
}, editor);
258+
return output;
259+
},
231260
toExternalHTML(inlineContent, editor) {
232261
const Content =
233262
inlineContentImplementation.toExternalHTML ||
@@ -255,7 +284,7 @@ export function createReactInlineContentSpec<
255284
</InlineContentWrapper>
256285
);
257286
}, editor);
258-
return output as any;
287+
return output;
259288
},
260289
},
261290
) as any;

tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/inlineContent/mentionWithToExternalHTML.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
<p class="bn-inline-content">
66
I enjoy working with
77
<span
8-
class="mention-external"
9-
data-external="true"
10-
data-inline-content-type="mention"
8+
class="mention-internal"
119
data-user="Matthew"
10+
data-inline-content-type="mention"
1211
>@Matthew</span>
1312
</p>
1413
</div>

tests/src/unit/core/formatConversion/export/__snapshots__/blocknoteHTML/inlineContent/tagWithoutToExternalHTML.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
<div class="bn-block-content" data-content-type="paragraph">
55
<p class="bn-inline-content">
66
I love
7-
<span class="tag-external" data-external="true" data-inline-content-type="tag">
7+
<span data-inline-content-type="tag">
88
#
9-
<span data-editable="">BlockNote</span>
9+
<span data-tag="true" data-editable="">BlockNote</span>
1010
</span>
1111
</p>
1212
</div>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<p>
22
I love
3-
<span class="tag-external" data-external="true" data-inline-content-type="tag">
3+
<span data-inline-content-type="tag">
44
#
5-
<span data-editable="">BlockNote</span>
5+
<span data-tag="true" data-editable="">BlockNote</span>
66
</span>
77
</p>

tests/src/unit/core/testSchema.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const Mention = createInlineContentSpec(
9898
const dom = document.createElement("span");
9999
dom.appendChild(document.createTextNode("@" + ic.props.user));
100100
dom.className = "mention-internal";
101+
dom.setAttribute("data-user", ic.props.user);
101102

102103
return {
103104
dom,
@@ -135,28 +136,20 @@ const Tag = createInlineContentSpec(
135136
content: "styled",
136137
},
137138
{
138-
render: () => {
139-
const dom = document.createElement("span");
140-
dom.textContent = "#";
141-
142-
const contentDOM = document.createElement("span");
143-
dom.appendChild(contentDOM);
144-
145-
return {
146-
dom,
147-
contentDOM,
148-
};
139+
parse: (el) => {
140+
const isTag = el.getAttribute("data-tag");
141+
if (isTag) {
142+
return {};
143+
}
144+
return undefined;
149145
},
150-
151-
toExternalHTML: () => {
146+
render: () => {
152147
const dom = document.createElement("span");
153148
dom.textContent = "#";
154-
dom.className = "tag-external";
155-
dom.setAttribute("data-external", "true");
156-
dom.setAttribute("data-inline-content-type", "tag");
157149

158150
const contentDOM = document.createElement("span");
159151
dom.appendChild(contentDOM);
152+
contentDOM.setAttribute("data-tag", "true");
160153

161154
return {
162155
dom,

0 commit comments

Comments
 (0)