@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
137 lines (127 loc) • 3.75 kB
text/typescript
import { KeyboardShortcutCommand, Node } from "@tiptap/core";
import {
Extension,
ExtensionFactoryInstance,
} from "../../editor/BlockNoteExtension.js";
import { camelToDataKebab } from "../../util/string.js";
import { PropSchema, Props } from "../propTypes.js";
import {
CustomInlineContentConfig,
InlineContentImplementation,
InlineContentSchemaFromSpecs,
InlineContentSpec,
InlineContentSpecs,
} from "./types.js";
// Function that adds necessary classes and attributes to the `dom` element
// returned from a custom inline content's 'render' function, to ensure no data
// is lost on internal copy & paste.
export function addInlineContentAttributes<
IType extends string,
PSchema extends PropSchema,
>(
element: {
dom: HTMLElement;
contentDOM?: HTMLElement;
},
inlineContentType: IType,
inlineContentProps: Props<PSchema>,
propSchema: PSchema,
): {
dom: HTMLElement;
contentDOM?: HTMLElement;
} {
// Sets content type attribute
element.dom.setAttribute("data-inline-content-type", inlineContentType);
// Adds props as HTML attributes in kebab-case with "data-" prefix. Skips props
// set to their default values.
Object.entries(inlineContentProps)
.filter(([prop, value]) => {
const spec = propSchema[prop];
return value !== spec?.default;
})
.map(([prop, value]) => {
return [camelToDataKebab(prop), value];
})
.forEach(([prop, value]) => element.dom.setAttribute(prop, value));
if (element.contentDOM) {
element.contentDOM.setAttribute("data-editable", "");
}
return element;
}
// see https://github.com/TypeCellOS/BlockNote/pull/435
export function addInlineContentKeyboardShortcuts<
T extends CustomInlineContentConfig,
>(
config: T,
): {
[p: string]: KeyboardShortcutCommand;
} {
return {
Backspace: ({ editor }) => {
const resolvedPos = editor.state.selection.$from;
return (
editor.state.selection.empty &&
resolvedPos.node().type.name === config.type &&
resolvedPos.parentOffset === 0
);
},
};
}
// This helper function helps to instantiate a InlineContentSpec with a
// config and implementation that conform to the type of Config
export function createInternalInlineContentSpec<
const T extends CustomInlineContentConfig,
>(
config: T,
implementation: InlineContentImplementation<NoInfer<T>>,
extensions?: (Extension | ExtensionFactoryInstance)[],
): InlineContentSpec<T> {
return {
config,
implementation,
extensions,
} as const;
}
export function createInlineContentSpecFromTipTapNode<
T extends Node,
P extends PropSchema,
>(
node: T,
propSchema: P,
implementation: Omit<
InlineContentImplementation<CustomInlineContentConfig>,
"node"
>,
) {
return createInternalInlineContentSpec(
{
type: node.name as T["name"],
propSchema,
content:
node.config.content === "inline*"
? "styled"
: node.config.content === "text*"
? "plain"
: "none",
},
// Cast needed because `implementation` is typed against the generic
// `CustomInlineContentConfig`, while `createInternalInlineContentSpec`
// expects the implementation for the specific (still-generic) config
// inferred from `node`/`propSchema` above.
{
...implementation,
node,
} as unknown as InlineContentImplementation<{
type: T["name"];
propSchema: P;
content: "styled" | "none" | "plain";
}>,
);
}
export function getInlineContentSchemaFromSpecs<T extends InlineContentSpecs>(
specs: T,
) {
return Object.fromEntries(
Object.entries(specs).map(([key, value]) => [key, value.config]),
) as InlineContentSchemaFromSpecs<T>;
}