import { InputRule } from "@tiptap/core"; import { PropSchema, createBlockSpecFromStronglyTypedTiptapNode, createStronglyTypedTiptapNode, } from "../../../schema"; import { createDefaultBlockDOMOutputSpec } from "../../defaultBlockHelpers"; import { defaultProps } from "../../defaultProps"; import { handleEnter } from "../ListItemKeyboardShortcuts"; import { NumberedListIndexingPlugin } from "./NumberedListIndexingPlugin"; export const numberedListItemPropSchema = { ...defaultProps, } satisfies PropSchema; const NumberedListItemBlockContent = createStronglyTypedTiptapNode({ name: "numberedListItem", content: "inline*", group: "blockContent", addAttributes() { return { 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(`^1\\.\\s$`), handler: ({ state, chain, range }) => { chain() .BNUpdateBlock(state.selection.from, { type: "numberedListItem", props: {}, }) // Removes the "1." characters used to set the list. .deleteRange({ from: range.from, to: range.to }); }, }), ]; }, addKeyboardShortcuts() { return { Enter: () => handleEnter(this.editor), "Mod-Shift-8": () => this.editor.commands.BNUpdateBlock(this.editor.state.selection.anchor, { type: "numberedListItem", props: {}, }), }; }, addProseMirrorPlugins() { return [NumberedListIndexingPlugin()]; }, parseHTML() { return [ { tag: "div[data-content-type=" + this.name + "]", // TODO: remove if we can't come up with test case that needs this }, // Case for regular HTML list structure. // (e.g.: when pasting from other apps) { 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") ) { return {}; } return false; }, node: "numberedListItem", }, // Case for BlockNote list structure. // (e.g.: when pasting from blocknote) { tag: "p", getAttrs: (element) => { if (typeof element === "string") { return false; } const parent = element.parentElement; if (parent === null) { return false; } if (parent.getAttribute("data-content-type") === "numberedListItem") { return {}; } return false; }, priority: 300, node: "numberedListItem", }, ]; }, renderHTML({ HTMLAttributes }) { return createDefaultBlockDOMOutputSpec( this.name, // We use a

tag, because for

  • tags we'd need an
      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 );