@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
183 lines (165 loc) • 5.16 kB
text/typescript
import { createExtension } from "../../../editor/BlockNoteExtension.js";
import { createBlockConfig, createBlockSpec } from "../../../schema/index.js";
import {
addDefaultPropsExternalHTML,
defaultProps,
parseDefaultProps,
} from "../../defaultProps.js";
import { handleEnter } from "../../utils/listItemEnterHandler.js";
import { getListItemContent } from "../getListItemContent.js";
export type CheckListItemBlockConfig = ReturnType<
typeof createCheckListItemConfig
>;
export const createCheckListItemConfig = createBlockConfig(
() =>
({
type: "checkListItem" as const,
propSchema: {
...defaultProps,
checked: { default: false, type: "boolean" },
},
content: "inline",
}) as const,
);
export const createCheckListItemBlockSpec = createBlockSpec(
createCheckListItemConfig,
{
meta: {
isolating: false,
},
parse(element) {
if (element.tagName === "input") {
// Ignore if we already parsed an ancestor list item to avoid double-parsing.
if (element.closest("[data-content-type]") || element.closest("li")) {
return undefined;
}
if ((element as HTMLInputElement).type === "checkbox") {
return { checked: (element as HTMLInputElement).checked };
}
return undefined;
}
if (element.tagName !== "LI") {
return undefined;
}
const parent = element.parentElement;
if (parent === null) {
return undefined;
}
if (
parent.tagName === "UL" ||
(parent.tagName === "DIV" && parent.parentElement?.tagName === "UL")
) {
const checkbox =
(element.querySelector("input[type=checkbox]") as HTMLInputElement) ||
null;
if (checkbox === null) {
return undefined;
}
return { ...parseDefaultProps(element), checked: checkbox.checked };
}
return;
},
// As `li` elements can contain multiple paragraphs, we need to merge their contents
// into a single one so that ProseMirror can parse everything correctly.
parseContent: ({ el, schema }) =>
getListItemContent(el, schema, "checkListItem"),
render(block, editor) {
const dom = document.createDocumentFragment();
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = block.props.checked;
if (block.props.checked) {
checkbox.setAttribute("checked", "");
}
checkbox.disabled = !editor.isEditable;
checkbox.addEventListener("change", () => {
if (!editor.isEditable) {
return;
}
editor.updateBlock(block, { props: { checked: !block.props.checked } });
});
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put
// them in to be semantically correct, which we can't have due to the
// schema.
const paragraph = document.createElement("p");
const div = document.createElement("div");
div.contentEditable = "false";
div.appendChild(checkbox);
dom.appendChild(div);
dom.appendChild(paragraph);
return {
dom,
contentDOM: paragraph,
};
},
toExternalHTML(block) {
const dom = document.createElement("li");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = block.props.checked;
if (block.props.checked) {
checkbox.setAttribute("checked", "");
}
// We use a <p> tag, because for <li> tags we'd need a <ul> element to put
// them in to be semantically correct, which we can't have due to the
// schema.
const paragraph = document.createElement("p");
addDefaultPropsExternalHTML(block.props, dom);
dom.appendChild(checkbox);
dom.appendChild(paragraph);
return {
dom,
contentDOM: paragraph,
};
},
runsBefore: ["bulletListItem"],
},
[
createExtension({
key: "check-list-item-shortcuts",
keyboardShortcuts: {
Enter: ({ editor }) => {
return handleEnter(editor, "checkListItem");
},
"Mod-Shift-9": ({ editor }) => {
const cursorPosition = editor.getTextCursorPosition();
if (
editor.schema.blockSchema[cursorPosition.block.type].content !==
"inline"
) {
return false;
}
editor.updateBlock(cursorPosition.block, {
type: "checkListItem",
props: {},
});
return true;
},
},
inputRules: [
{
find: /^\s?\[\s*\]\s$/,
replace() {
return {
type: "checkListItem",
props: {
checked: false,
},
};
},
},
{
find: /^\s?\[[Xx]\]\s$/,
replace() {
return {
type: "checkListItem",
props: {
checked: true,
},
};
},
},
],
}),
],
);