Skip to content

Commit 4a23da2

Browse files
feat: custom styles API (#2004)
Co-authored-by: Nick the Sick <[email protected]>
1 parent 66c2591 commit 4a23da2

File tree

17 files changed

+478
-113
lines changed

17 files changed

+478
-113
lines changed

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

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,38 @@ export function serializeInlineContentExternalHTML<
6060

6161
for (const node of nodes) {
6262
// Check if this is a custom inline content node with toExternalHTML
63-
if (editor.schema.inlineContentSchema[node.type.name]) {
63+
if (
64+
node.type.name !== "text" &&
65+
editor.schema.inlineContentSchema[node.type.name]
66+
) {
6467
const inlineContentImplementation =
6568
editor.schema.inlineContentSpecs[node.type.name].implementation;
6669

67-
if (inlineContentImplementation?.toExternalHTML) {
70+
if (inlineContentImplementation) {
6871
// Convert the node to inline content format
6972
const inlineContent = nodeToCustomInlineContent(
7073
node,
7174
editor.schema.inlineContentSchema,
7275
editor.schema.styleSchema,
7376
);
7477

75-
// Use the custom toExternalHTML method
76-
const output = inlineContentImplementation.toExternalHTML(
77-
inlineContent as any,
78-
editor as any,
79-
);
78+
// Use the custom toExternalHTML method or fallback to `render`
79+
const output = inlineContentImplementation.toExternalHTML
80+
? inlineContentImplementation.toExternalHTML(
81+
inlineContent as any,
82+
editor as any,
83+
)
84+
: inlineContentImplementation.render.call(
85+
{
86+
renderType: "dom",
87+
props: undefined,
88+
},
89+
inlineContent as any,
90+
() => {
91+
// No-op
92+
},
93+
editor as any,
94+
);
8095

8196
if (output) {
8297
fragment.appendChild(output.dom);
@@ -93,14 +108,40 @@ export function serializeInlineContentExternalHTML<
93108
continue;
94109
}
95110
}
96-
}
111+
} else if (node.type.name === "text") {
112+
// We serialize text nodes manually as we need to serialize the styles/
113+
// marks using `styleSpec.implementation.render`. When left up to
114+
// ProseMirror, it'll use `toDOM` which is incorrect.
115+
let dom: globalThis.Node | Text = document.createTextNode(
116+
node.textContent,
117+
);
118+
// Reverse the order of marks to maintain the correct priority.
119+
for (const mark of node.marks.toReversed()) {
120+
if (mark.type.name in editor.schema.styleSpecs) {
121+
const newDom = (
122+
editor.schema.styleSpecs[mark.type.name].implementation
123+
.toExternalHTML ??
124+
editor.schema.styleSpecs[mark.type.name].implementation.render
125+
)(mark.attrs["stringValue"], editor);
126+
newDom.contentDOM!.appendChild(dom);
127+
dom = newDom.dom;
128+
} else {
129+
const domOutputSpec = mark.type.spec.toDOM!(mark, true);
130+
const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
131+
newDom.contentDOM!.appendChild(dom);
132+
dom = newDom.dom;
133+
}
134+
}
97135

98-
// Fall back to default serialization for this node
99-
const nodeFragment = serializer.serializeFragment(
100-
Fragment.from([node]),
101-
options,
102-
);
103-
fragment.appendChild(nodeFragment);
136+
fragment.appendChild(dom);
137+
} else {
138+
// Fall back to default serialization for this node
139+
const nodeFragment = serializer.serializeFragment(
140+
Fragment.from([node]),
141+
options,
142+
);
143+
fragment.appendChild(nodeFragment);
144+
}
104145
}
105146

106147
if (

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

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export function serializeInlineContentInternalHTML<
4848
// Check if this is a custom inline content node with toExternalHTML
4949
if (
5050
node.type.name !== "text" &&
51-
node.type.name !== "link" &&
5251
editor.schema.inlineContentSchema[node.type.name]
5352
) {
5453
const inlineContentImplementation =
@@ -90,14 +89,38 @@ export function serializeInlineContentInternalHTML<
9089
continue;
9190
}
9291
}
93-
}
92+
} else if (node.type.name === "text") {
93+
// We serialize text nodes manually as we need to serialize the styles/
94+
// marks using `styleSpec.implementation.render`. When left up to
95+
// ProseMirror, it'll use `toDOM` which is incorrect.
96+
let dom: globalThis.Node | Text = document.createTextNode(
97+
node.textContent,
98+
);
99+
// Reverse the order of marks to maintain the correct priority.
100+
for (const mark of node.marks.toReversed()) {
101+
if (mark.type.name in editor.schema.styleSpecs) {
102+
const newDom = editor.schema.styleSpecs[
103+
mark.type.name
104+
].implementation.render(mark.attrs["stringValue"], editor);
105+
newDom.contentDOM!.appendChild(dom);
106+
dom = newDom.dom;
107+
} else {
108+
const domOutputSpec = mark.type.spec.toDOM!(mark, true);
109+
const newDom = DOMSerializer.renderSpec(document, domOutputSpec);
110+
newDom.contentDOM!.appendChild(dom);
111+
dom = newDom.dom;
112+
}
113+
}
94114

95-
// Fall back to default serialization for this node
96-
const nodeFragment = serializer.serializeFragment(
97-
Fragment.from([node]),
98-
options,
99-
);
100-
fragment.appendChild(nodeFragment);
115+
fragment.appendChild(dom);
116+
} else {
117+
// Fall back to default serialization for this node
118+
const nodeFragment = serializer.serializeFragment(
119+
Fragment.from([node]),
120+
options,
121+
);
122+
fragment.appendChild(nodeFragment);
123+
}
101124
}
102125

103126
return fragment;

packages/core/src/blocks/defaultBlocks.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ import {
1616
createQuoteBlockSpec,
1717
createToggleListItemBlockSpec,
1818
createVideoBlockSpec,
19+
defaultProps,
1920
} from "./index.js";
20-
import { BackgroundColor } from "../extensions/BackgroundColor/BackgroundColorMark.js";
21-
import { TextColor } from "../extensions/TextColor/TextColorMark.js";
2221
import {
2322
BlockNoDefaults,
2423
BlockSchema,
@@ -27,11 +26,13 @@ import {
2726
PartialBlockNoDefaults,
2827
StyleSchema,
2928
StyleSpecs,
29+
createStyleSpec,
3030
createStyleSpecFromTipTapMark,
3131
getInlineContentSchemaFromSpecs,
3232
getStyleSchemaFromSpecs,
3333
} from "../schema/index.js";
3434
import { createTableBlockSpec } from "./Table/block.js";
35+
import { COLORS_DEFAULT } from "../editor/defaultColors.js";
3536

3637
export const defaultBlockSpecs = {
3738
audio: createAudioBlockSpec(),
@@ -56,6 +57,78 @@ export type _DefaultBlockSchema = {
5657
};
5758
export type DefaultBlockSchema = _DefaultBlockSchema;
5859

60+
const TextColor = createStyleSpec(
61+
{
62+
type: "textColor",
63+
propSchema: "string",
64+
},
65+
{
66+
render: () => {
67+
const span = document.createElement("span");
68+
69+
return {
70+
dom: span,
71+
contentDOM: span,
72+
};
73+
},
74+
toExternalHTML: (value) => {
75+
const span = document.createElement("span");
76+
if (value !== defaultProps.textColor.default) {
77+
span.style.color =
78+
value in COLORS_DEFAULT ? COLORS_DEFAULT[value].text : value;
79+
}
80+
81+
return {
82+
dom: span,
83+
contentDOM: span,
84+
};
85+
},
86+
parse: (element) => {
87+
if (element.tagName === "SPAN" && element.style.color) {
88+
return element.style.color;
89+
}
90+
91+
return undefined;
92+
},
93+
},
94+
);
95+
96+
const BackgroundColor = createStyleSpec(
97+
{
98+
type: "backgroundColor",
99+
propSchema: "string",
100+
},
101+
{
102+
render: () => {
103+
const span = document.createElement("span");
104+
105+
return {
106+
dom: span,
107+
contentDOM: span,
108+
};
109+
},
110+
toExternalHTML: (value) => {
111+
const span = document.createElement("span");
112+
if (value !== defaultProps.backgroundColor.default) {
113+
span.style.backgroundColor =
114+
value in COLORS_DEFAULT ? COLORS_DEFAULT[value].background : value;
115+
}
116+
117+
return {
118+
dom: span,
119+
contentDOM: span,
120+
};
121+
},
122+
parse: (element) => {
123+
if (element.tagName === "SPAN" && element.style.backgroundColor) {
124+
return element.style.backgroundColor;
125+
}
126+
127+
return undefined;
128+
},
129+
},
130+
);
131+
59132
export const defaultStyleSpecs = {
60133
bold: createStyleSpecFromTipTapMark(Bold, "boolean"),
61134
italic: createStyleSpecFromTipTapMark(Italic, "boolean"),

packages/core/src/blocks/defaultProps.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,10 @@ export const getBackgroundColorAttribute = (
9292
default: defaultProps.backgroundColor.default,
9393
parseHTML: (element) => {
9494
if (element.hasAttribute("data-background-color")) {
95-
return element.getAttribute("data-background-color");
95+
return element.getAttribute("data-background-color")!;
9696
}
9797

9898
if (element.style.backgroundColor) {
99-
// Check if `element.style.backgroundColor` matches the string:
100-
// `var(--blocknote-background-<color>)`. If it does, return the color
101-
// name only. Otherwise, return `element.style.backgroundColor`.
102-
const match = element.style.backgroundColor.match(
103-
/var\(--blocknote-background-(.+)\)/,
104-
);
105-
if (match) {
106-
return match[1];
107-
}
108-
10999
return element.style.backgroundColor;
110100
}
111101

@@ -128,18 +118,10 @@ export const getTextColorAttribute = (
128118
default: defaultProps.textColor.default,
129119
parseHTML: (element) => {
130120
if (element.hasAttribute("data-text-color")) {
131-
return element.getAttribute("data-text-color");
121+
return element.getAttribute("data-text-color")!;
132122
}
133123

134124
if (element.style.color) {
135-
// Check if `element.style.color` matches the string:
136-
// `var(--blocknote-text-<color>)`. If it does, return the color name
137-
// only. Otherwise, return `element.style.color`.
138-
const match = element.style.color.match(/var\(--blocknote-text-(.+)\)/);
139-
if (match) {
140-
return match[1];
141-
}
142-
143125
return element.style.color;
144126
}
145127

@@ -149,6 +131,7 @@ export const getTextColorAttribute = (
149131
if (attributes[attributeName] === defaultProps.textColor.default) {
150132
return {};
151133
}
134+
152135
return {
153136
"data-text-color": attributes[attributeName],
154137
};
@@ -174,6 +157,7 @@ export const getTextAlignmentAttribute = (
174157
if (attributes[attributeName] === defaultProps.textAlignment.default) {
175158
return {};
176159
}
160+
177161
return {
178162
"data-text-alignment": attributes[attributeName],
179163
};

0 commit comments

Comments
 (0)