Skip to content

Commit b17a8d8

Browse files
committed
feat: support for react custom inline content
1 parent 11f8633 commit b17a8d8

File tree

12 files changed

+159
-32
lines changed

12 files changed

+159
-32
lines changed

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

Lines changed: 12 additions & 2 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";
@@ -39,7 +39,7 @@ export function serializeInlineContentExternalHTML<
3939
serializer: DOMSerializer,
4040
options?: { document?: Document },
4141
) {
42-
let nodes: any;
42+
let nodes: Node[];
4343

4444
// TODO: reuse function from nodeconversions?
4545
if (!blockContent) {
@@ -84,6 +84,16 @@ export function serializeInlineContentExternalHTML<
8484

8585
if (output) {
8686
fragment.appendChild(output.dom);
87+
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+
}
8797
continue;
8898
}
8999
}

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

Lines changed: 12 additions & 2 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";
@@ -25,7 +25,7 @@ export function serializeInlineContentInternalHTML<
2525
blockType?: string,
2626
options?: { document?: Document },
2727
) {
28-
let nodes: any;
28+
let nodes: Node[];
2929

3030
// TODO: reuse function from nodeconversions?
3131
if (!blockContent) {
@@ -70,6 +70,16 @@ export function serializeInlineContentInternalHTML<
7070

7171
if (output) {
7272
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+
}
7383
continue;
7484
}
7585
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,9 @@ export function addInlineContentKeyboardShortcuts<
7373

7474
// This helper function helps to instantiate a InlineContentSpec with a
7575
// config and implementation that conform to the type of Config
76-
export function createInternalInlineContentSpec<T extends InlineContentConfig>(
77-
config: T,
78-
implementation: InlineContentImplementation<T>,
79-
) {
76+
export function createInternalInlineContentSpec<
77+
const T extends InlineContentConfig,
78+
>(config: T, implementation: InlineContentImplementation<NoInfer<T>>) {
8079
return {
8180
config,
8281
implementation,

packages/react/src/schema/ReactInlineContentSpec.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,7 @@ export type ReactInlineContentImplementation<
5151
S extends StyleSchema,
5252
> = {
5353
render: FC<ReactCustomInlineContentRenderProps<T, S>>;
54-
// TODO?
55-
// toExternalHTML?: FC<{
56-
// block: BlockFromConfig<T, I, S>;
57-
// editor: BlockNoteEditor<BlockSchemaWithBlock<T["type"], T>, I, S>;
58-
// }>;
54+
toExternalHTML?: FC<ReactCustomInlineContentRenderProps<T, S>>;
5955
};
6056

6157
// Function that adds a wrapper with necessary classes and attributes to the
@@ -99,7 +95,7 @@ export function InlineContentWrapper<
9995
// A function to create custom block for API consumers
10096
// we want to hide the tiptap node from API consumers and provide a simpler API surface instead
10197
export function createReactInlineContentSpec<
102-
T extends CustomInlineContentConfig,
98+
const T extends CustomInlineContentConfig,
10399
// I extends InlineContentSchema,
104100
S extends StyleSchema,
105101
>(
@@ -137,16 +133,23 @@ export function createReactInlineContentSpec<
137133
editor.schema.inlineContentSchema,
138134
editor.schema.styleSchema,
139135
) as any as InlineContentFromConfig<T, S>; // TODO: fix cast
140-
const Content = inlineContentImplementation.render;
136+
const Content =
137+
inlineContentImplementation.toExternalHTML ||
138+
inlineContentImplementation.render;
141139
const output = renderToDOMSpec(
142-
(refCB) => (
140+
(ref) => (
143141
<Content
142+
contentRef={(element) => {
143+
ref(element);
144+
if (element) {
145+
element.dataset.editable = "";
146+
}
147+
}}
144148
inlineContent={ic}
145149
updateInlineContent={() => {
146150
// No-op
147151
}}
148152
editor={editor}
149-
contentRef={refCB}
150153
/>
151154
),
152155
editor,
@@ -160,7 +163,6 @@ export function createReactInlineContentSpec<
160163
);
161164
},
162165

163-
// TODO: needed?
164166
addNodeView() {
165167
const editor: BlockNoteEditor<any, any, any> = this.options.editor;
166168
return (props) =>
@@ -180,7 +182,12 @@ export function createReactInlineContentSpec<
180182
propSchema={inlineContentConfig.propSchema}
181183
>
182184
<Content
183-
contentRef={ref}
185+
contentRef={(element) => {
186+
ref(element);
187+
if (element) {
188+
element.dataset.editable = "";
189+
}
190+
}}
184191
editor={editor}
185192
inlineContent={
186193
nodeToCustomInlineContent(
@@ -216,7 +223,39 @@ export function createReactInlineContentSpec<
216223
},
217224
});
218225

219-
return createInternalInlineContentSpec(inlineContentConfig, {
220-
node: node,
221-
} as any);
226+
return createInternalInlineContentSpec(
227+
inlineContentConfig as CustomInlineContentConfig,
228+
{
229+
node,
230+
toExternalHTML(inlineContent, editor) {
231+
const Content =
232+
inlineContentImplementation.toExternalHTML ||
233+
inlineContentImplementation.render;
234+
const output = renderToDOMSpec((ref) => {
235+
return (
236+
<InlineContentWrapper
237+
inlineContentProps={inlineContent.props}
238+
inlineContentType={inlineContentConfig.type}
239+
propSchema={inlineContentConfig.propSchema}
240+
>
241+
<Content
242+
contentRef={(element) => {
243+
ref(element);
244+
if (element) {
245+
element.dataset.editable = "";
246+
}
247+
}}
248+
editor={editor}
249+
inlineContent={inlineContent}
250+
updateInlineContent={() => {
251+
// no-op
252+
}}
253+
/>
254+
</InlineContentWrapper>
255+
);
256+
}, editor);
257+
return output as any;
258+
},
259+
},
260+
);
222261
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="bn-block-content" data-content-type="paragraph">
55
<p class="bn-inline-content">
66
I love
7-
<span data-inline-content-type="tag">
7+
<span class="tag-external" data-external="true" data-inline-content-type="tag">
88
#
99
<span data-editable="">BlockNote</span>
1010
</span>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<p>
22
I love
3-
<span data-inline-content-type="tag">
3+
<span class="tag-external" data-external="true" data-inline-content-type="tag">
44
#
55
<span data-editable="">BlockNote</span>
66
</span>

tests/src/unit/core/testSchema.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ const Tag = createInlineContentSpec(
147147
contentDOM,
148148
};
149149
},
150+
151+
toExternalHTML: () => {
152+
const dom = document.createElement("span");
153+
dom.textContent = "#";
154+
dom.className = "tag-external";
155+
dom.setAttribute("data-external", "true");
156+
dom.setAttribute("data-inline-content-type", "tag");
157+
158+
const contentDOM = document.createElement("span");
159+
dom.appendChild(contentDOM);
160+
161+
return {
162+
dom,
163+
contentDOM,
164+
};
165+
},
150166
},
151167
);
152168

tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/mention/basic.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@
44
<div class="bn-block-content" data-content-type="paragraph">
55
<p class="bn-inline-content">
66
I enjoy working with
7-
<span data-inline-content-type="mention" data-user="Matthew">@Matthew</span>
7+
<span
8+
as="span"
9+
class="bn-inline-content-section"
10+
data-inline-content-type="mention"
11+
data-user="Matthew"
12+
data-node-view-wrapper=""
13+
style="white-space: normal;"
14+
>
15+
<span
16+
data-external="true"
17+
data-inline-content-type="mention"
18+
data-user="Matthew"
19+
>@Matthew</span>
20+
</span>
821
</p>
922
</div>
1023
</div>

tests/src/unit/react/formatConversion/export/__snapshots__/blocknoteHTML/tag/basic.html

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@
44
<div class="bn-block-content" data-content-type="paragraph">
55
<p class="bn-inline-content">
66
I love
7-
<span data-inline-content-type="tag">
8-
#
9-
<span data-editable="">BlockNote</span>
7+
<span
8+
as="span"
9+
class="bn-inline-content-section"
10+
data-inline-content-type="tag"
11+
data-node-view-wrapper=""
12+
style="white-space: normal;"
13+
>
14+
<span>
15+
#
16+
<span data-editable="">BlockNote</span>
17+
</span>
1018
</span>
1119
</p>
1220
</div>
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
<p>
22
I enjoy working with
3-
<span data-inline-content-type="mention" data-user="Matthew">@Matthew</span>
3+
<span
4+
as="span"
5+
class="bn-inline-content-section"
6+
data-inline-content-type="mention"
7+
data-user="Matthew"
8+
data-node-view-wrapper=""
9+
style="white-space: normal;"
10+
>
11+
<span
12+
data-external="true"
13+
data-inline-content-type="mention"
14+
data-user="Matthew"
15+
>@Matthew</span>
16+
</span>
417
</p>

0 commit comments

Comments
 (0)