@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
142 lines (128 loc) • 3.57 kB
text/typescript
import { createBlockConfig, createBlockSpec } from "../../schema/index.js";
import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js";
import { createExtension } from "../../editor/BlockNoteExtension.js";
import {
addDefaultPropsExternalHTML,
defaultProps,
parseDefaultProps,
} from "../defaultProps.js";
import { createToggleWrapper } from "../ToggleWrapper/createToggleWrapper.js";
const HEADING_LEVELS = [1, 2, 3, 4, 5, 6] as const;
export interface HeadingOptions {
defaultLevel?: (typeof HEADING_LEVELS)[number];
levels?: readonly number[];
// TODO should probably use composition instead of this
allowToggleHeadings?: boolean;
}
const createHeadingKeyboardShortcut =
(level: number) =>
({ editor }: { editor: BlockNoteEditor<any, any, any> }) => {
const cursorPosition = editor.getTextCursorPosition();
if (
editor.schema.blockSchema[cursorPosition.block.type].content !== "inline"
) {
return false;
}
editor.updateBlock(cursorPosition.block, {
type: "heading",
props: { level },
});
return true;
};
export type HeadingBlockConfig = ReturnType<typeof createHeadingBlockConfig>;
export const createHeadingBlockConfig = createBlockConfig(
({
defaultLevel = 1,
levels = HEADING_LEVELS,
allowToggleHeadings = true,
}: HeadingOptions = {}) =>
({
type: "heading" as const,
propSchema: {
...defaultProps,
level: { default: defaultLevel, values: levels },
...(allowToggleHeadings
? { isToggleable: { default: false, optional: true } as const }
: {}),
},
content: "inline",
}) as const,
);
export const createHeadingBlockSpec = createBlockSpec(
createHeadingBlockConfig,
({ allowToggleHeadings = true }: HeadingOptions = {}) => ({
meta: {
isolating: false,
},
parse(e) {
let level: number;
switch (e.tagName) {
case "H1":
level = 1;
break;
case "H2":
level = 2;
break;
case "H3":
level = 3;
break;
case "H4":
level = 4;
break;
case "H5":
level = 5;
break;
case "H6":
level = 6;
break;
default:
return undefined;
}
return {
...parseDefaultProps(e),
level,
};
},
render(block, editor) {
const dom = document.createElement(`h${block.props.level}`);
if (allowToggleHeadings) {
const toggleWrapper = createToggleWrapper(block, editor, dom);
return { ...toggleWrapper, contentDOM: dom };
}
return {
dom,
contentDOM: dom,
};
},
toExternalHTML(block) {
const dom = document.createElement(`h${block.props.level}`);
addDefaultPropsExternalHTML(block.props, dom);
return {
dom,
contentDOM: dom,
};
},
}),
({ levels = HEADING_LEVELS }: HeadingOptions = {}) => [
createExtension({
key: "heading-shortcuts",
keyboardShortcuts: Object.fromEntries(
levels.map((level) => [
`Mod-Alt-${level}`,
createHeadingKeyboardShortcut(level),
]) ?? [],
),
inputRules: levels.map((level) => ({
find: new RegExp(`^(#{${level}})\\s$`),
replace({ match }: { match: RegExpMatchArray }) {
return {
type: "heading",
props: {
level: match[1].length,
},
};
},
})),
}),
],
);