diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 0a630c2b01..389844f255 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -19,19 +19,24 @@ import { getNodeById } from "./api/util/nodeUtil"; import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import { - Block, BlockIdentifier, - PartialBlock, + BlockSpec, + BlockTemplate, + PartialBlockTemplate, } from "./extensions/Blocks/api/blockTypes"; +import { + MouseCursorPosition, + TextCursorPosition, +} from "./extensions/Blocks/api/cursorPositionTypes"; +import { + defaultBlocks, + DefaultBlockTypes, +} from "./extensions/Blocks/api/defaultBlocks"; import { ColorStyle, Styles, ToggledStyle, } from "./extensions/Blocks/api/inlineContentTypes"; -import { - MouseCursorPosition, - TextCursorPosition, -} from "./extensions/Blocks/api/cursorPositionTypes"; import { Selection } from "./extensions/Blocks/api/selectionTypes"; import { getBlockInfoFromPos } from "./extensions/Blocks/helpers/getBlockInfoFromPos"; import { @@ -39,7 +44,16 @@ import { defaultSlashMenuItems, } from "./extensions/SlashMenu"; -export type BlockNoteEditorOptions = { +// We need to separate WithChildren, otherwise we get issues with recursive types +// maybe worth a revisit before merging +type WithChildren> = Block & { + children: WithChildren[]; +}; + +export type BlockNoteEditorOptions< + BareBlock extends BlockTemplate, + Block extends BareBlock = WithChildren +> = { // TODO: Figure out if enableBlockNoteExtensions/disableHistoryExtension are needed and document them. enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; @@ -69,16 +83,16 @@ export type BlockNoteEditorOptions = { /** * A callback function that runs when the editor is ready to be used. */ - onEditorReady: (editor: BlockNoteEditor) => void; + onEditorReady: (editor: BlockNoteEditor) => void; /** * A callback function that runs whenever the editor's contents change. */ - onEditorContentChange: (editor: BlockNoteEditor) => void; + onEditorContentChange: (editor: BlockNoteEditor) => void; /** * A callback function that runs whenever the text cursor position changes. */ - onTextCursorPositionChange: (editor: BlockNoteEditor) => void; - initialContent: PartialBlock[]; + onTextCursorPositionChange: (editor: BlockNoteEditor) => void; + initialContent: PartialBlockTemplate[]; /** * Use default BlockNote font and reset the styles of

  • elements etc., that are used in BlockNote. @@ -87,6 +101,10 @@ export type BlockNoteEditorOptions = { */ defaultStyles: boolean; + /** + * A list of block types that should be available in the editor. + */ + blocks: BlockSpec[]; // TODO, type this so that it matches // tiptap options, undocumented _tiptapOptions: any; }; @@ -97,10 +115,15 @@ const blockNoteTipTapOptions = { enableCoreExtensions: false, }; -export class BlockNoteEditor { +// TODO: make type of BareBlock / Block automatically based on options.blocks +export class BlockNoteEditor< + BareBlock extends BlockTemplate = DefaultBlockTypes, + Block extends BareBlock & { children: Block[] } = WithChildren +> { public readonly _tiptapEditor: TiptapEditor & { contentComponent: any }; private blockCache = new WeakMap(); private mousePos = { x: 0, y: 0 }; + private schema = new Map(); public get domElement() { return this._tiptapEditor.view.dom as HTMLDivElement; @@ -110,10 +133,12 @@ export class BlockNoteEditor { this._tiptapEditor.view.focus(); } - constructor(options: Partial = {}) { + constructor(options: Partial> = {}) { + console.log("test"); // apply defaults options = { defaultStyles: true, + blocks: defaultBlocks, ...options, }; @@ -121,8 +146,14 @@ export class BlockNoteEditor { editor: this, uiFactories: options.uiFactories || {}, slashCommands: options.slashCommands || defaultSlashMenuItems, + blocks: options.blocks || [], }); + // add blocks to schema + for (const block of options.blocks || []) { + this.schema.set(block.type, block); + } + let extensions = options.disableHistoryExtension ? blockNoteExtensions.filter((e) => e.name !== "history") : blockNoteExtensions; @@ -192,7 +223,7 @@ export class BlockNoteEditor { const blocks: Block[] = []; this._tiptapEditor.state.doc.firstChild!.descendants((node) => { - blocks.push(nodeToBlock(node, this.blockCache)); + blocks.push(nodeToBlock(node, this.schema, this.blockCache)); return false; }); @@ -221,7 +252,7 @@ export class BlockNoteEditor { return true; } - newBlock = nodeToBlock(node, this.blockCache); + newBlock = nodeToBlock(node, this.schema, this.blockCache); return false; }); @@ -294,7 +325,7 @@ export class BlockNoteEditor { return; } - return { block: nodeToBlock(blockInfo.node, this.blockCache) }; + return { block: nodeToBlock(blockInfo.node, this.schema, this.blockCache) }; } /** @@ -329,15 +360,15 @@ export class BlockNoteEditor { } return { - block: nodeToBlock(node, this.blockCache), + block: nodeToBlock(node, this.schema, this.blockCache), prevBlock: prevNode === undefined ? undefined - : nodeToBlock(prevNode, this.blockCache), + : nodeToBlock(prevNode, this.schema, this.blockCache), nextBlock: nextNode === undefined ? undefined - : nodeToBlock(nextNode, this.blockCache), + : nodeToBlock(nextNode, this.schema, this.blockCache), }; } @@ -389,6 +420,7 @@ export class BlockNoteEditor { blocks.push( nodeToBlock( this._tiptapEditor.state.doc.resolve(pos).node(), + this.schema, this.blockCache ) ); @@ -408,7 +440,7 @@ export class BlockNoteEditor { * `referenceBlock`. Inserts the blocks at the start of the existing block's children if "nested" is used. */ public insertBlocks( - blocksToInsert: PartialBlock[], + blocksToInsert: PartialBlockTemplate[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before" ): void { @@ -422,7 +454,10 @@ export class BlockNoteEditor { * @param blockToUpdate The block that should be updated. * @param update A partial block which defines how the existing block should be changed. */ - public updateBlock(blockToUpdate: BlockIdentifier, update: PartialBlock) { + public updateBlock( + blockToUpdate: BlockIdentifier, + update: PartialBlockTemplate + ) { updateBlock(blockToUpdate, update, this._tiptapEditor); } @@ -443,7 +478,7 @@ export class BlockNoteEditor { */ public replaceBlocks( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[] + blocksToInsert: PartialBlockTemplate[] ) { replaceBlocks(blocksToRemove, blocksToInsert, this._tiptapEditor); } @@ -630,7 +665,7 @@ export class BlockNoteEditor { * @returns The blocks parsed from the HTML string. */ public async HTMLToBlocks(html: string): Promise { - return HTMLToBlocks(html, this._tiptapEditor.schema); + return HTMLToBlocks(html, this.schema, this._tiptapEditor.schema) as any; // TODO: fix type } /** @@ -651,6 +686,35 @@ export class BlockNoteEditor { * @returns The blocks parsed from the Markdown string. */ public async markdownToBlocks(markdown: string): Promise { - return markdownToBlocks(markdown, this._tiptapEditor.schema); + return markdownToBlocks( + markdown, + this.schema, + this._tiptapEditor.schema + ) as any; // TODO: fix type } } + +// Playground: +/* +let x = new BlockNoteEditor(); // default block types are supported + +// this breaks because "level" is not valid on paragraph +x.updateBlock("", { type: "paragraph", content: "hello", props: { level: "3" } }); +x.updateBlock("", { + type: "heading", + content: "hello", + props: { level: "1" }, +}); + + +let y = new BlockNoteEditor(); + +y.updateBlock("", { type: "paragraph", content: "hello", props: { } }); + +// this breaks because "heading" is not a type on this editor +y.updateBlock("", { + type: "heading", + content: "hello", + props: { level: "1" }, +}); +*/ diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index c0a0e3aedf..3c0460c408 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -16,6 +16,7 @@ import { Underline } from "@tiptap/extension-underline"; import { BackgroundColorExtension } from "./extensions/BackgroundColor/BackgroundColorExtension"; import { BackgroundColorMark } from "./extensions/BackgroundColor/BackgroundColorMark"; import { blocks } from "./extensions/Blocks"; +import { BlockSpec } from "./extensions/Blocks/api/blockTypes"; import blockStyles from "./extensions/Blocks/nodes/Block.module.css"; import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; import { DraggableBlocksExtension } from "./extensions/DraggableBlocks/DraggableBlocksExtension"; @@ -46,6 +47,7 @@ export const getBlockNoteExtensions = (opts: { editor: BlockNoteEditor; uiFactories: UiFactories; slashCommands: BaseSlashMenuItem[]; + blocks: BlockSpec[]; }) => { const ret: Extensions = [ extensions.ClipboardTextSerializer, @@ -88,6 +90,7 @@ export const getBlockNoteExtensions = (opts: { // custom blocks: ...blocks, + ...opts.blocks.map((b) => b.node), Dropcursor.configure({ width: 5, color: "#ddeeff" }), History, diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index 13d7f9c441..6e9350400b 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -2,13 +2,14 @@ import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import { BlockIdentifier, - PartialBlock, + BlockTemplate, + PartialBlockTemplate, } from "../../extensions/Blocks/api/blockTypes"; import { blockToNode } from "../nodeConversions/nodeConversions"; import { getNodeById } from "../util/nodeUtil"; -export function insertBlocks( - blocksToInsert: PartialBlock[], +export function insertBlocks>( + blocksToInsert: PartialBlockTemplate[], referenceBlock: BlockIdentifier, placement: "before" | "after" | "nested" = "before", editor: Editor @@ -56,9 +57,9 @@ export function insertBlocks( editor.view.dispatch(editor.state.tr.insert(insertionPos, nodesToInsert)); } -export function updateBlock( +export function updateBlock>( blockToUpdate: BlockIdentifier, - update: PartialBlock, + update: PartialBlockTemplate, editor: Editor ) { const id = @@ -115,9 +116,9 @@ export function removeBlocks( } } -export function replaceBlocks( +export function replaceBlocks>( blocksToRemove: BlockIdentifier[], - blocksToInsert: PartialBlock[], + blocksToInsert: PartialBlockTemplate[], editor: Editor ) { insertBlocks(blocksToInsert, blocksToRemove[0], "before", editor); diff --git a/packages/core/src/api/formatConversions/formatConversions.ts b/packages/core/src/api/formatConversions/formatConversions.ts index 4e7db0d061..f18a83e0c2 100644 --- a/packages/core/src/api/formatConversions/formatConversions.ts +++ b/packages/core/src/api/formatConversions/formatConversions.ts @@ -7,13 +7,17 @@ import remarkParse from "remark-parse"; import remarkRehype from "remark-rehype"; import remarkStringify from "remark-stringify"; import { unified } from "unified"; -import { Block } from "../../extensions/Blocks/api/blockTypes"; +import { + BlockSpec, + BlockTemplate, +} from "../../extensions/Blocks/api/blockTypes"; + import { blockToNode, nodeToBlock } from "../nodeConversions/nodeConversions"; import { removeUnderlines } from "./removeUnderlinesRehypePlugin"; import { simplifyBlocks } from "./simplifyBlocksRehypePlugin"; export async function blocksToHTML( - blocks: Block[], + blocks: BlockTemplate[], schema: Schema ): Promise { const htmlParentElement = document.createElement("div"); @@ -39,25 +43,26 @@ export async function blocksToHTML( export async function HTMLToBlocks( html: string, - schema: Schema -): Promise { + schema: Map, + pmSchema: Schema +): Promise[]> { const htmlNode = document.createElement("div"); htmlNode.innerHTML = html.trim(); - const parser = DOMParser.fromSchema(schema); + const parser = DOMParser.fromSchema(pmSchema); const parentNode = parser.parse(htmlNode); - const blocks: Block[] = []; + const blocks: BlockTemplate[] = []; for (let i = 0; i < parentNode.firstChild!.childCount; i++) { - blocks.push(nodeToBlock(parentNode.firstChild!.child(i))); + blocks.push(nodeToBlock(parentNode.firstChild!.child(i), schema)); } return blocks; } export async function blocksToMarkdown( - blocks: Block[], + blocks: BlockTemplate[], schema: Schema ): Promise { const markdownString = await unified() @@ -73,8 +78,9 @@ export async function blocksToMarkdown( export async function markdownToBlocks( markdown: string, - schema: Schema -): Promise { + schema: Map, + pmSchema: Schema +): Promise[]> { const htmlString = await unified() .use(remarkParse) .use(remarkGfm) @@ -82,5 +88,5 @@ export async function markdownToBlocks( .use(rehypeStringify) .process(markdown); - return HTMLToBlocks(htmlString.value as string, schema); + return HTMLToBlocks(htmlString.value as string, schema, pmSchema); } diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index b3e5900b73..57f7681f8d 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -1,10 +1,11 @@ import { Mark } from "@tiptap/pm/model"; import { Node, Schema } from "prosemirror-model"; import { - Block, - blockProps, - PartialBlock, + BlockSpec, + BlockTemplate, + PartialBlockTemplate, } from "../../extensions/Blocks/api/blockTypes"; + import { ColorStyle, InlineContent, @@ -104,7 +105,10 @@ export function inlineContentToNodes( /** * Converts a BlockNote block to a TipTap node. */ -export function blockToNode(block: PartialBlock, schema: Schema) { +export function blockToNode>( + block: PartialBlockTemplate, + schema: Schema +) { let id = block.id; if (id === undefined) { @@ -213,8 +217,9 @@ function contentNodeToInlineContent(contentNode: Node) { /** * Convert a TipTap node to a BlockNote block. */ -export function nodeToBlock( +export function nodeToBlock>( node: Node, + schema: Map, blockCache?: WeakMap ): Block { if (node.type.name !== "blockContainer") { @@ -245,16 +250,19 @@ export function nodeToBlock( ...blockInfo.node.attrs, ...blockInfo.contentNode.attrs, })) { - if (!(blockInfo.contentType.name in blockProps)) { + const blockSpec = schema.get(blockInfo.contentType.name); + if (!blockSpec) { throw Error( "Block is of an unrecognized type: " + blockInfo.contentType.name ); } - const validAttrs = blockProps[blockInfo.contentType.name as Block["type"]]; + const validAttrs = blockSpec.acceptedProps; - if (validAttrs.has(attr)) { + if (validAttrs.find((prop) => prop.name === attr)) { props[attr] = value; + } else { + console.warn("Block has an unrecognized attribute: " + attr); } } @@ -262,10 +270,13 @@ export function nodeToBlock( const children: Block[] = []; for (let i = 0; i < blockInfo.numChildBlocks; i++) { - children.push(nodeToBlock(blockInfo.node.lastChild!.child(i))); + children.push( + nodeToBlock(blockInfo.node.lastChild!.child(i), schema, blockCache) + ); } - const block: Block = { + // TODO: fix types + const block: any = { id, type: blockInfo.contentType.name as Block["type"], props, diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts new file mode 100644 index 0000000000..9fb2f6a7b4 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -0,0 +1,128 @@ +import { Attribute, Node } from "@tiptap/core"; +import { PropsFromPropSpec, PropSpec } from "./blockTypes"; + +function camelToDataKebab(str: string): string { + return "data-" + str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); +} + +// function dataKebabToCamel(str: string): string { +// const withoutData = str.replace(/^data-/, ""); +// return withoutData.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); +// } + +// A function to create a "BlockSpec" from a tiptap node. +// we use this to create the block specs for the built-in blocks + +// TODO: rename to createBlockSpecFromTiptapNode? +export function createBlockFromTiptapNode< + Type extends string, + Props extends readonly PropSpec[] +>( + blockType: Type, + options: { + props: Props; + }, + node: Node +) { + if (node.name !== blockType) { + throw Error( + "Node must be of type " + blockType + ", but is of type" + node.name + "." + ); + } + + // TODO: how to handle markdown / html conversions + + // the return type gives us runtime access to the block name, props, and tiptap node + // but is also used to generate (derive) the type for the block spec + // so that we can have a strongly typed BlockNoteEditor API + return { + type: blockType, + node, + // TODO: rename to propSpec? + acceptedProps: options.props, + }; +} + +// A function to create custom block for API consumers +// we want to hide the tiptap node from API consumers and provide a simpler API surface instead +export function createCustomBlock< + Type extends string, + Props extends readonly PropSpec[] +>( + blockType: Type, + options: ( + | { + // for blocks with a single inline content element + inlineContent: true; + render: () => { dom: HTMLElement; contentDOM: HTMLElement }; + } + | { + // for custom blocks that don't support content + inlineContent: false; + render: () => { dom: HTMLElement }; + } + ) & { + props: Props; + parseHTML?: (element: HTMLElement) => PropsFromPropSpec; + // todo: possibly add parseDom options / other options we need + } +) { + const node = Node.create({ + name: blockType, + group: "blockContent", + content: options.inlineContent ? "inline*" : "", + + addAttributes() { + const tiptapAttributes: Record = {}; + + Object.values(options.props).forEach((propSpec) => { + tiptapAttributes[propSpec.name] = { + default: propSpec.default, + keepOnSplit: false, + parseHTML: (element) => + element.getAttribute(camelToDataKebab(propSpec.name)), + renderHTML: (attributes) => + attributes[propSpec.name] !== propSpec.default + ? { + [camelToDataKebab(propSpec.name)]: attributes[propSpec.name], + } + : {}, + }; + }); + + return tiptapAttributes; + }, + + parseHTML() { + // TODO: This won't work for content copied outside BlockNote. Given the + // variety of possible custom block types, a one-size-fits-all solution + // probably won't work and we'll need an optional parseHTML option. + return [ + { + tag: "div[data-content-type=" + blockType + "]", + }, + ]; + }, + + // TODO, create node from render / inlineContent / other props from options + renderHTML({ HTMLAttributes }) { + // Create blockContent element + const blockContent = document.createElement("div"); + // Add blockContent HTML attribute + blockContent.setAttribute("data-content-type", blockType); + // Add props as HTML attributes + for (const [attribute, value] of Object.entries(HTMLAttributes)) { + blockContent.setAttribute(attribute, value); + } + // Render content + const rendered = options.render(); + // TODO: Should we always assume contentDOM is always a descendant of dom? + // Add content to blockContent element + blockContent.appendChild(rendered.dom); + + return blockContent; + }, + }); + + return createBlockFromTiptapNode(blockType, { props: options.props }, node); +} diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index e3452bf116..e6bdb7e370 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -1,7 +1,9 @@ /** Define the main block types **/ +import { createBlockFromTiptapNode } from "./block"; import { InlineContent, PartialInlineContent } from "./inlineContentTypes"; +// the type of a block exposed to API consumers export type BlockTemplate< // Type of the block. // Examples might include: "paragraph", "heading", or "bulletListItem". @@ -14,77 +16,80 @@ export type BlockTemplate< type: Type; props: Props; content: InlineContent[]; - children: Block[]; }; -export type DefaultBlockProps = { - backgroundColor: string; - textColor: string; - textAlignment: "left" | "center" | "right" | "justify"; +// information about a blocks props when defining Block types +export type PropSpec = { + name: string; + values?: readonly string[]; + default: string; }; -export type NumberedListItemBlock = BlockTemplate< - "numberedListItem", - DefaultBlockProps ->; - -export type BulletListItemBlock = BlockTemplate< - "bulletListItem", - DefaultBlockProps ->; - -export type HeadingBlock = BlockTemplate< - "heading", - DefaultBlockProps & { - level: "1" | "2" | "3"; - } ->; +// define the default Props +export const defaultBlockProps = [ + { + name: "backgroundColor", + default: "transparent", // TODO think if this makes sense + }, + { + name: "textColor", + default: "black", // TODO + }, + { + name: "textAlignment", + values: ["left", "center", "right", "justify"], + default: "left", + }, +] as const; // TODO: upgrade typescript and use satisfies PropSpec -export type ParagraphBlock = BlockTemplate<"paragraph", DefaultBlockProps>; +export type DefaultBlockPropsType = PropsFromPropSpec; -export type Block = - | ParagraphBlock - | HeadingBlock - | BulletListItemBlock - | NumberedListItemBlock; - -export type BlockIdentifier = string | Block; +export type BlockIdentifier = string | { id: string }; /** Define "Partial Blocks", these are for updating or creating blocks */ -export type PartialBlockTemplate = B extends Block - ? Partial> & { - type?: B["type"]; - props?: Partial; - content?: string | PartialInlineContent[]; - children?: PartialBlock[]; - } - : never; - -export type PartialBlock = PartialBlockTemplate; +export type PartialBlockTemplate> = + B extends BlockTemplate + ? Partial> & { + type?: B["type"]; + props?: Partial; + content?: string | PartialInlineContent[]; + children?: PartialBlockTemplate[]; + } + : never; -export type BlockPropsTemplate = Props extends Block["props"] - ? keyof Props - : never; +// export type BlockPropsTemplate< +// B extends BlockTemplate, +// Props +// > = Props extends PartialBlockTemplate["props"] ? keyof Props : never; -/** - * Expose blockProps. This is currently not very nice, but it's expected this - * will change anyway once we allow for custom blocks - */ +// ExtractElement is a utility typ for PropsFromPropSpec that extracts the element with a specific key K from a union type T +// Example: ExtractElement<{ name: "level"; values: readonly ["warn", "error"]; }, "level"> will result in +// { name: "level"; values: readonly ["warn", "error"]; } +type ExtractElement = T extends { name: K } ? T : never; -export const globalProps: Array = [ - "backgroundColor", - "textColor", - "textAlignment", -]; +// ConfigValue is a utility type for PropsFromPropSpec that gets the value type from an object T. +// If T has a `values` property, it uses the element type of the tuple (indexed by `number`), +// otherwise, it defaults to `string`. +// Example: ConfigValue<{ values: readonly ["warn", "error"] }> will result in "warn" | "error" +type ConfigValue = T extends { values: readonly any[] } + ? T["values"][number] + : string; -export const blockProps: Record> = { - paragraph: new Set([...globalProps]), - heading: new Set([ - ...globalProps, - "level" as const, - ]), - numberedListItem: new Set([ - ...globalProps, - ]), - bulletListItem: new Set([...globalProps]), +// PropsFromPropSpec is a mapped type that iterates over the keys (names) in the PropSpec array and constructs a +// new object type with properties corresponding to the keys in the PropSpec array and their value types. +// Example: With the provided PropSpec array: +// let config = [{ name: "level", values: ["warn", "error"] }, { name: "triggerOn", values: ["startup", "shutdown"] }, { name: "anystring" }] as const; +// PropsFromPropSpec will result in +// { level: "warn" | "error", triggerOn: "startup" | "shutdown", anystring: string } +export type PropsFromPropSpec = { + [K in T[number]["name"]]: ConfigValue>; }; + +// the return type of createBlockFromTiptapNode +export type BlockSpec = ReturnType; + +// create the Block type from registererd block types (BlockSpecs) +export type BlockFromBlockSpec = BlockTemplate< + T["type"], + PropsFromPropSpec +>; diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts new file mode 100644 index 0000000000..b708d79cf2 --- /dev/null +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -0,0 +1,74 @@ +import { HeadingBlockContent } from "../nodes/BlockContent/HeadingBlockContent/HeadingBlockContent"; +import { BulletListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; +import { NumberedListItemBlockContent } from "../nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; +import { ParagraphBlockContent } from "../nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; +import { createBlockFromTiptapNode } from "./block"; +import { BlockFromBlockSpec, defaultBlockProps } from "./blockTypes"; + +// this file defines the default blocks that are available in the editor +// and their types (Block types) + +export const NumberedListItemBlock = createBlockFromTiptapNode( + "numberedListItem", + { + props: defaultBlockProps, + }, + NumberedListItemBlockContent +); + +export type NumberedListItemBlockType = BlockFromBlockSpec< + typeof NumberedListItemBlock +>; + +export const BulletListItemBlock = createBlockFromTiptapNode( + "bulletListItem", + { + props: defaultBlockProps, + }, + BulletListItemBlockContent +); + +export type BulletListItemBlockType = BlockFromBlockSpec< + typeof BulletListItemBlock +>; + +// TODO: rename to HeadingBlockSpec? +export const HeadingBlock = createBlockFromTiptapNode( + "heading", + { + // TODO: rename to propSpec? + props: [ + ...defaultBlockProps, + { + name: "level", + values: ["1", "2", "3"], + default: "1", + }, + ] as const, + }, + HeadingBlockContent +); + +export type HeadingBlockType = BlockFromBlockSpec; + +export const ParagraphBlock = createBlockFromTiptapNode( + "paragraph", + { + props: defaultBlockProps, + }, + ParagraphBlockContent +); + +export type ParagraphBlockType = BlockFromBlockSpec; + +export const defaultBlocks = [ + ParagraphBlock, + NumberedListItemBlock, + BulletListItemBlock, + HeadingBlock, +]; +export type DefaultBlockTypes = + | NumberedListItemBlockType + | BulletListItemBlockType + | HeadingBlockType + | ParagraphBlockType; diff --git a/packages/core/src/extensions/Blocks/index.ts b/packages/core/src/extensions/Blocks/index.ts index ef57f7b250..1f6c9c2de6 100644 --- a/packages/core/src/extensions/Blocks/index.ts +++ b/packages/core/src/extensions/Blocks/index.ts @@ -1,22 +1,11 @@ import { Node } from "@tiptap/core"; import { BlockContainer } from "./nodes/BlockContainer"; import { BlockGroup } from "./nodes/BlockGroup"; -import { ParagraphBlockContent } from "./nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent"; -import { HeadingBlockContent } from "./nodes/BlockContent/HeadingBlockContent/HeadingBlockContent"; -import { BulletListItemBlockContent } from "./nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; -import { NumberedListItemBlockContent } from "./nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; -import { TableContent } from "./nodes/BlockContent/TableContent/TableContent"; -import { TableRow } from "./nodes/BlockContent/TableContent/TableRow"; -import { TableCol } from "./nodes/BlockContent/TableContent/TableCol"; export const blocks: any[] = [ - ParagraphBlockContent, - HeadingBlockContent, - BulletListItemBlockContent, - NumberedListItemBlockContent, - TableContent, - TableRow, - TableCol, + // TableContent, + // TableRow, + // TableCol, BlockContainer, BlockGroup, Node.create({ diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 73ef0f5193..cc3339d392 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -5,7 +5,8 @@ import { blockToNode, inlineContentToNodes, } from "../../../api/nodeConversions/nodeConversions"; -import { PartialBlock } from "../api/blockTypes"; +import { PartialBlockTemplate } from "../api/blockTypes"; + import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"; import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin"; import styles from "./Block.module.css"; @@ -23,10 +24,13 @@ declare module "@tiptap/core" { BNDeleteBlock: (posInBlock: number) => ReturnType; BNMergeBlocks: (posBetweenBlocks: number) => ReturnType; BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType; - BNUpdateBlock: (posInBlock: number, block: PartialBlock) => ReturnType; + BNUpdateBlock: ( + posInBlock: number, + block: PartialBlockTemplate + ) => ReturnType; BNCreateOrUpdateBlock: ( posInBlock: number, - block: PartialBlock + block: PartialBlockTemplate ) => ReturnType; }; } @@ -101,12 +105,13 @@ export const BlockContainer = Node.create({ BNCreateBlock: (pos) => ({ state, dispatch }) => { - const newContent = state.schema.nodes["table"].createAndFill()!; + // const newContent = state.schema.nodes["table"].createAndFill()!; - const newBlock = state.schema.nodes["blockContainer"].createAndFill( - undefined, - newContent - )!; + const newBlock = state.schema.nodes["blockContainer"] + .createAndFill + // undefined, + // newContent + ()!; if (dispatch) { state.tr.insert(pos, newBlock); diff --git a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx index a65d2d2f1e..ddda0ed7f6 100644 --- a/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx +++ b/packages/core/src/extensions/SlashMenu/defaultSlashMenuItems.tsx @@ -1,8 +1,11 @@ -import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; -import { PartialBlock } from "../Blocks/api/blockTypes"; import { BlockNoteEditor } from "../../BlockNoteEditor"; +import { PartialBlockTemplate } from "../Blocks/api/blockTypes"; +import { BaseSlashMenuItem } from "./BaseSlashMenuItem"; -function insertOrUpdateBlock(editor: BlockNoteEditor, block: PartialBlock) { +function insertOrUpdateBlock( + editor: BlockNoteEditor, + block: PartialBlockTemplate +) { const currentBlock = editor.getTextCursorPosition().block; if ( diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx index 81780496e1..7b78d631d4 100644 --- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx @@ -1,3 +1,6 @@ +import { BlockNoteEditor, DefaultBlockPropsType } from "@blocknote/core"; +import { useCallback } from "react"; +import { IconType } from "react-icons"; import { RiAlignCenter, RiAlignJustify, @@ -5,11 +8,8 @@ import { RiAlignRight, } from "react-icons/ri"; import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; -import { BlockNoteEditor, DefaultBlockProps } from "@blocknote/core"; -import { useCallback } from "react"; -import { IconType } from "react-icons"; -const icons: Record = { +const icons: Record = { left: RiAlignLeft, center: RiAlignCenter, right: RiAlignRight, @@ -18,7 +18,7 @@ const icons: Record = { export const TextAlignButton = (props: { editor: BlockNoteEditor; - textAlignment: DefaultBlockProps["textAlignment"]; + textAlignment: DefaultBlockPropsType["textAlignment"]; }) => { const getTextAlignment = useCallback( () => props.editor.getTextCursorPosition().block.props.textAlignment, @@ -26,7 +26,7 @@ export const TextAlignButton = (props: { ); const setTextAlignment = useCallback( - (textAlignment: DefaultBlockProps["textAlignment"]) => { + (textAlignment: DefaultBlockPropsType["textAlignment"]) => { props.editor.focus(); for (const block of props.editor.getSelection().blocks) { props.editor.updateBlock(block, {