@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
172 lines (155 loc) • 5.15 kB
text/typescript
import { InputRule } from "@tiptap/core";
import { updateBlockCommand } from "../../../api/blockManipulation/commands/updateBlock/updateBlock.js";
import { getBlockInfoFromSelection } from "../../../api/getBlockInfoFromPos.js";
import {
PropSchema,
createBlockSpecFromStronglyTypedTiptapNode,
createStronglyTypedTiptapNode,
propsToAttributes,
} from "../../../schema/index.js";
import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers.js";
import { defaultProps } from "../../defaultProps.js";
import { getListItemContent } from "../getListItemContent.js";
import { handleEnter } from "../ListItemKeyboardShortcuts.js";
import { NumberedListIndexingPlugin } from "./NumberedListIndexingPlugin.js";
export const numberedListItemPropSchema = {
...defaultProps,
start: { default: undefined, type: "number" },
} satisfies PropSchema;
const NumberedListItemBlockContent = createStronglyTypedTiptapNode({
name: "numberedListItem",
content: "inline*",
group: "blockContent",
priority: 90,
addAttributes() {
return {
...propsToAttributes(numberedListItemPropSchema),
// the index attribute is only used internally (it's not part of the blocknote schema)
// that's why it's defined explicitly here, and not part of the prop schema
index: {
default: null,
parseHTML: (element) => element.getAttribute("data-index"),
renderHTML: (attributes) => {
return {
"data-index": attributes.index,
};
},
},
};
},
addInputRules() {
return [
// Creates an ordered list when starting with "1.".
new InputRule({
find: new RegExp(`^(\\d+)\\.\\s$`),
handler: ({ state, chain, range, match }) => {
const blockInfo = getBlockInfoFromSelection(state);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockContent.node.type.spec.content !== "inline*" ||
blockInfo.blockNoteType === "numberedListItem"
) {
return;
}
const startIndex = parseInt(match[1]);
chain()
.command(
updateBlockCommand(blockInfo.bnBlock.beforePos, {
type: "numberedListItem",
props:
(startIndex === 1 && {}) ||
({
start: startIndex,
} as any),
}),
)
// Removes the "1." characters used to set the list.
.deleteRange({ from: range.from, to: range.to });
},
}),
];
},
addKeyboardShortcuts() {
return {
Enter: () => handleEnter(this.options.editor),
"Mod-Shift-7": () => {
const blockInfo = getBlockInfoFromSelection(this.editor.state);
if (
!blockInfo.isBlockContainer ||
blockInfo.blockContent.node.type.spec.content !== "inline*"
) {
return true;
}
return this.editor.commands.command(
updateBlockCommand(blockInfo.bnBlock.beforePos, {
type: "numberedListItem",
props: {},
}),
);
},
};
},
addProseMirrorPlugins() {
return [NumberedListIndexingPlugin()];
},
parseHTML() {
return [
// Parse from internal HTML.
{
tag: "div[data-content-type=" + this.name + "]",
contentElement: ".bn-inline-content",
},
// Parse from external HTML.
{
tag: "li",
getAttrs: (element) => {
if (typeof element === "string") {
return false;
}
const parent = element.parentElement;
if (parent === null) {
return false;
}
if (
parent.tagName === "OL" ||
(parent.tagName === "DIV" && parent.parentElement?.tagName === "OL")
) {
const startIndex =
parseInt(parent.getAttribute("start") || "1") || 1;
if (element.previousSibling || startIndex === 1) {
return {};
}
return {
start: startIndex,
};
}
return false;
},
// As `li` elements can contain multiple paragraphs, we need to merge their contents
// into a single one so that ProseMirror can parse everything correctly.
getContent: (node, schema) =>
getListItemContent(node, schema, this.name),
priority: 300,
node: "numberedListItem",
},
];
},
renderHTML({ HTMLAttributes }) {
return createDefaultBlockDOMOutputSpec(
this.name,
// We use a <p> tag, because for <li> tags we'd need an <ol> element to
// put them in to be semantically correct, which we can't have due to the
// schema.
"p",
{
...(this.options.domAttributes?.blockContent || {}),
...HTMLAttributes,
},
this.options.domAttributes?.inlineContent || {},
);
},
});
export const NumberedListItem = createBlockSpecFromStronglyTypedTiptapNode(
NumberedListItemBlockContent,
numberedListItemPropSchema,
);