@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
156 lines (138 loc) • 4.35 kB
text/typescript
import { Attributes, Mark } from "@tiptap/core";
import { DOMSerializer } from "@tiptap/pm/model";
import {
StyleConfig,
StyleImplementation,
StylePropSchema,
StyleSchemaFromSpecs,
StyleSpec,
StyleSpecs,
} from "./types.js";
export function stylePropsToAttributes(
propSchema: StylePropSchema,
): Attributes {
if (propSchema === "boolean") {
return {};
}
return {
stringValue: {
default: undefined,
keepOnSplit: true,
parseHTML: (element) => element.getAttribute("data-value"),
renderHTML: (attributes) =>
attributes.stringValue !== undefined
? {
"data-value": attributes.stringValue,
}
: {},
},
};
}
// Function that adds necessary classes and attributes to the `dom` element
// returned from a custom style's 'render' function, to ensure no data is lost
// on internal copy & paste.
export function addStyleAttributes<
SType extends string,
PSchema extends StylePropSchema,
>(
element: {
dom: HTMLElement;
contentDOM?: HTMLElement;
},
styleType: SType,
styleValue: PSchema extends "boolean" ? undefined : string,
propSchema: PSchema,
): {
dom: HTMLElement;
contentDOM?: HTMLElement;
} {
// Sets content type attribute
element.dom.setAttribute("data-style-type", styleType);
// Adds style value as an HTML attribute in kebab-case with "data-" prefix, if
// the style takes a string value.
if (propSchema === "string") {
element.dom.setAttribute("data-value", styleValue as string);
}
if (element.contentDOM) {
element.contentDOM.setAttribute("data-editable", "");
}
return element;
}
// This helper function helps to instantiate a stylespec with a
// config and implementation that conform to the type of Config
export function createInternalStyleSpec<T extends StyleConfig>(
config: T,
implementation: StyleImplementation<T>,
) {
return {
config,
implementation,
} satisfies StyleSpec<T>;
}
export function createStyleSpecFromTipTapMark<
T extends Mark,
P extends StylePropSchema,
>(mark: T, propSchema: P) {
return createInternalStyleSpec(
{
type: mark.name as T["name"],
propSchema,
},
{
mark,
render(value, editor) {
const toDOM = editor.pmSchema.marks[mark.name].spec.toDOM;
if (toDOM === undefined) {
throw new Error(
"This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.",
);
}
const markInstance = editor.pmSchema.mark(mark.name, {
stringValue: value,
});
const renderSpec = DOMSerializer.renderSpec(
document,
toDOM(markInstance, true),
);
if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
throw new Error(
"Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.",
);
}
return renderSpec as {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
},
toExternalHTML(value, editor) {
const toDOM = editor.pmSchema.marks[mark.name].spec.toDOM;
if (toDOM === undefined) {
throw new Error(
"This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`.",
);
}
const markInstance = editor.pmSchema.mark(mark.name, {
stringValue: value,
});
const renderSpec = DOMSerializer.renderSpec(
document,
toDOM(markInstance, true),
);
if (typeof renderSpec !== "object" || !("dom" in renderSpec)) {
throw new Error(
"Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property.",
);
}
return renderSpec as {
dom: HTMLElement;
contentDOM?: HTMLElement;
};
},
},
);
}
export function getStyleSchemaFromSpecs<T extends StyleSpecs>(specs: T) {
return Object.fromEntries(
Object.entries(specs).map(([key, value]) => [key, value.config]),
) as StyleSchemaFromSpecs<T>;
}