From c51767aaea55e009a20d477cd01d547e2916b2b5 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 5 Aug 2025 22:43:25 +0200 Subject: [PATCH 1/5] Made type guards more generic --- .../src/FileReplaceButton.tsx | 4 +- .../fromClipboard/handleFileInsertion.ts | 9 +- packages/core/src/blks/Audio/definition.ts | 2 +- packages/core/src/blks/Image/definition.ts | 4 +- .../src/blks/NumberedListItem/definition.ts | 2 +- packages/core/src/blks/Video/definition.ts | 2 +- packages/core/src/blks/index.ts | 2 +- .../core/src/blocks/defaultBlockTypeGuards.ts | 291 +++++++++--------- packages/core/src/blocks/defaultBlocks.ts | 4 +- packages/core/src/editor/BlockNoteEditor.ts | 4 +- packages/core/src/editor/playground.ts | 4 +- .../getDefaultSlashMenuItems.ts | 52 +++- .../TableHandles/TableHandlesPlugin.ts | 4 +- .../react/src/components/Comments/schema.ts | 18 +- .../FilePanel/DefaultTabs/UploadTab.tsx | 7 +- .../DefaultButtons/FileCaptionButton.tsx | 16 +- .../DefaultButtons/FileDeleteButton.tsx | 13 +- .../DefaultButtons/FileDownloadButton.tsx | 9 +- .../DefaultButtons/FilePreviewButton.tsx | 27 +- .../DefaultButtons/FileRenameButton.tsx | 16 +- .../DefaultButtons/FileReplaceButton.tsx | 6 +- .../DefaultButtons/TextAlignButton.tsx | 20 +- .../DefaultItems/BlockColorsItem.tsx | 36 ++- 23 files changed, 283 insertions(+), 269 deletions(-) diff --git a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx index 48e32a2b01..c0442bb2f7 100644 --- a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx @@ -1,6 +1,6 @@ import { BlockSchema, - checkBlockIsFileBlock, + blockHasProps, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +41,7 @@ export const FileReplaceButton = () => { if ( block === undefined || - !checkBlockIsFileBlock(block, editor) || + !blockHasProps(block, { url: { default: "" } }) || !editor.isEditable ) { return null; diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index d164dcb762..ce49383b86 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -2,7 +2,6 @@ import { Block, PartialBlock } from "../../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../../editor/BlockNoteEditor"; import { BlockSchema, - FileBlockConfig, InlineContentSchema, StyleSchema, } from "../../../schema/index.js"; @@ -106,15 +105,11 @@ export async function handleFileInsertion< event.preventDefault(); - const fileBlockConfigs = Object.values(editor.schema.blockSchema).filter( - (blockConfig) => blockConfig.isFileBlock, - ) as FileBlockConfig[]; - for (let i = 0; i < items.length; i++) { // Gets file block corresponding to MIME type. let fileBlockType = "file"; - for (const fileBlockConfig of fileBlockConfigs) { - for (const mimeType of fileBlockConfig.fileBlockAccept || []) { + for (const fileBlockConfig of Object.values(editor.schema.blockSchema)) { + for (const mimeType of fileBlockConfig.meta?.fileBlockAccept || []) { const isFileExtension = mimeType.startsWith("."); const file = items[i].getAsFile(); diff --git a/packages/core/src/blks/Audio/definition.ts b/packages/core/src/blks/Audio/definition.ts index eb36220e3a..c58b547671 100644 --- a/packages/core/src/blks/Audio/definition.ts +++ b/packages/core/src/blks/Audio/definition.ts @@ -72,7 +72,7 @@ export const definition = createBlockDefinition(config).implementation( }, render: (block, editor) => { const icon = document.createElement("div"); - icon.innerHTML = config.icon ?? FILE_AUDIO_ICON_SVG; + icon.innerHTML = config?.icon ?? FILE_AUDIO_ICON_SVG; const audio = document.createElement("audio"); audio.className = "bn-audio"; diff --git a/packages/core/src/blks/Image/definition.ts b/packages/core/src/blks/Image/definition.ts index af8c34e025..7e5d72400c 100644 --- a/packages/core/src/blks/Image/definition.ts +++ b/packages/core/src/blks/Image/definition.ts @@ -41,7 +41,7 @@ const config = createBlockConfig((_ctx: ImageOptions = {}) => ({ default: undefined, type: "number", }, - }, + } as const, content: "none" as const, meta: { fileBlockAccept: ["image/*"], @@ -78,7 +78,7 @@ export const definition = createBlockDefinition(config).implementation( }, render: (block, editor) => { const icon = document.createElement("div"); - icon.innerHTML = config.icon ?? FILE_IMAGE_ICON_SVG; + icon.innerHTML = config?.icon ?? FILE_IMAGE_ICON_SVG; const imageWrapper = document.createElement("div"); imageWrapper.className = "bn-visual-media-wrapper"; diff --git a/packages/core/src/blks/NumberedListItem/definition.ts b/packages/core/src/blks/NumberedListItem/definition.ts index 6e6f35476a..43962612fa 100644 --- a/packages/core/src/blks/NumberedListItem/definition.ts +++ b/packages/core/src/blks/NumberedListItem/definition.ts @@ -15,7 +15,7 @@ const config = createBlockConfig(() => ({ propSchema: { ...defaultProps, start: { default: undefined, type: "number" }, - }, + } as const, content: "inline", })); diff --git a/packages/core/src/blks/Video/definition.ts b/packages/core/src/blks/Video/definition.ts index fe681a58cd..d8aec2fcc2 100644 --- a/packages/core/src/blks/Video/definition.ts +++ b/packages/core/src/blks/Video/definition.ts @@ -62,7 +62,7 @@ export const definition = createBlockDefinition(config).implementation( }, render: (block, editor) => { const icon = document.createElement("div"); - icon.innerHTML = config.icon ?? FILE_VIDEO_ICON_SVG; + icon.innerHTML = config?.icon ?? FILE_VIDEO_ICON_SVG; const videoWrapper = document.createElement("div"); videoWrapper.className = "bn-visual-media-wrapper"; diff --git a/packages/core/src/blks/index.ts b/packages/core/src/blks/index.ts index a501019f03..97473296ef 100644 --- a/packages/core/src/blks/index.ts +++ b/packages/core/src/blks/index.ts @@ -6,7 +6,7 @@ export * as heading from "./Heading/definition.js"; export * as numberedListItem from "./NumberedListItem/definition.js"; export * as pageBreak from "./PageBreak/definition.js"; export * as paragraph from "./Paragraph/definition.js"; -export * as quoteBlock from "./Quote/definition.js"; +export * as quote from "./Quote/definition.js"; export * as toggleListItem from "./ToggleListItem/definition.js"; export * as file from "./File/definition.js"; diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 4fbdc2e99b..64731a8a06 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -2,196 +2,185 @@ import { CellSelection } from "prosemirror-tables"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; import { BlockConfig, - BlockFromConfig, BlockSchema, - InlineContentConfig, - InlineContentSchema, + PropSchema, + PropSpec, StyleSchema, } from "../schema/index.js"; import { Block, - DefaultBlockSchema, DefaultInlineContentSchema, - defaultBlockSpecs, defaultInlineContentSchema, } from "./defaultBlocks.js"; -import { defaultProps } from "./defaultProps.js"; import { Selection } from "prosemirror-state"; -export function checkDefaultBlockTypeInSchema< - BlockType extends keyof DefaultBlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - blockType: BlockType, - editor: BlockNoteEditor, +export function editorHasBlockWithType( + editor: BlockNoteEditor, + blockType: BType, ): editor is BlockNoteEditor< - { [K in BlockType]: DefaultBlockSchema[BlockType] }, - I, - S + { + [BT in BType]: BlockConfig< + BT, + { + [PN in string]: PropSpec; + } + >; + }, + any, + any > { - return ( - blockType in editor.schema.blockSchema && blockType in defaultBlockSpecs - ); -} + if (!(blockType in editor.schema.blockSpecs)) { + return false; + } -export function checkBlockTypeInSchema< - BlockType extends string, - Config extends BlockConfig, ->( - blockType: BlockType, - blockConfig: Config, - editor: BlockNoteEditor, -): editor is BlockNoteEditor<{ [T in BlockType]: Config }, any, any> { - return ( - blockType in editor.schema.blockSchema && - editor.schema.blockSchema[blockType] === blockConfig - ); + if (editor.schema.blockSpecs[blockType].config.type !== blockType) { + return false; + } + + return true; } -export function checkDefaultInlineContentTypeInSchema< - InlineContentType extends keyof DefaultInlineContentSchema, - B extends BlockSchema, - S extends StyleSchema, +export function editorHasBlockWithTypeAndProps< + BType extends string, + PSchema extends PropSchema, >( - inlineContentType: InlineContentType, - editor: BlockNoteEditor, + editor: BlockNoteEditor, + blockType: BType, + propSchema: PSchema, ): editor is BlockNoteEditor< - B, - { [K in InlineContentType]: DefaultInlineContentSchema[InlineContentType] }, - S + { + [BT in BType]: BlockConfig; + }, + any, + any > { - return ( - inlineContentType in editor.schema.inlineContentSchema && - editor.schema.inlineContentSchema[inlineContentType] === - defaultInlineContentSchema[inlineContentType] - ); -} + if (!editorHasBlockWithType(editor, blockType)) { + return false; + } -export function checkInlineContentTypeInSchema< - InlineContentType extends string, - Config extends InlineContentConfig, ->( - inlineContentType: InlineContentType, - inlineContentConfig: Config, - editor: BlockNoteEditor, -): editor is BlockNoteEditor { - return ( - inlineContentType in editor.schema.inlineContentSchema && - editor.schema.inlineContentSchema[inlineContentType] === inlineContentConfig - ); -} + for (const [propName, propSpec] of Object.entries(propSchema)) { + if (!(propName in editor.schema.blockSpecs[blockType].config.propSchema)) { + return false; + } -export function checkBlockIsDefaultType< - BlockType extends keyof DefaultBlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - blockType: BlockType, - block: Block, - editor: BlockNoteEditor, -): block is BlockFromConfig { - return ( - block.type === blockType && - block.type in editor.schema.blockSchema && - checkDefaultBlockTypeInSchema(block.type, editor) - ); -} + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default !== propSpec.default + ) { + return false; + } -export function checkBlockIsFileBlock< - B extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - block: Block, - editor: BlockNoteEditor, -): block is BlockFromConfig { - return ( - (block.type in editor.schema.blockSchema && - editor.schema.blockSchema[block.type].isFileBlock) || - false - ); + if ( + typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values !== typeof propSpec.values + ) { + return false; + } + + if ( + typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values === "object" && + typeof propSpec.values === "object" + ) { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName].values + .length !== propSpec.values.length + ) { + return false; + } + + for ( + let i = 0; + i < + editor.schema.blockSpecs[blockType].config.propSchema[propName].values + .length; + i++ + ) { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values[i] !== propSpec.values[i] + ) { + return false; + } + } + } + + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default === undefined && + propSpec.default === undefined + ) { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName].type !== + propSpec.type + ) { + return false; + } + } + } + + return true; } -export function checkBlockIsFileBlockWithPreview< - B extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->( - block: Block, - editor: BlockNoteEditor, -): block is BlockFromConfig< - FileBlockConfig & { - propSchema: Required; +export function blockHasType( + block: Block, + editor: BlockNoteEditor, + blockType: BType, +): block is Block< + { + [BT in string]: BlockConfig< + BT, + { + [PN in string]: PropSpec; + } + >; }, - I, - S + any, + any > { - return ( - (block.type in editor.schema.blockSchema && - editor.schema.blockSchema[block.type].isFileBlock && - "showPreview" in editor.schema.blockSchema[block.type].propSchema) || - false - ); -} - -export function checkBlockIsFileBlockWithPlaceholder< - B extends BlockSchema, - I extends InlineContentSchema, - S extends StyleSchema, ->(block: Block, editor: BlockNoteEditor) { - const config = editor.schema.blockSchema[block.type]; - return config.isFileBlock && !block.props.url; + return editorHasBlockWithType(editor, blockType) && block.type === blockType; } -export function checkBlockTypeHasDefaultProp< - Prop extends keyof typeof defaultProps, - I extends InlineContentSchema, - S extends StyleSchema, +export function blockHasTypeAndProps< + BType extends string, + PSchema extends PropSchema, >( - prop: Prop, - blockType: string, - editor: BlockNoteEditor, -): editor is BlockNoteEditor< + block: Block, + editor: BlockNoteEditor, + blockType: BType, + propSchema: PSchema, +): block is Block< { - [BT in string]: { - type: BT; - propSchema: { - [P in Prop]: (typeof defaultProps)[P]; - }; - content: "table" | "inline" | "none"; - }; + [BT in string]: BlockConfig; }, - I, - S + any, + any > { return ( - blockType in editor.schema.blockSchema && - prop in editor.schema.blockSchema[blockType].propSchema && - editor.schema.blockSchema[blockType].propSchema[prop] === defaultProps[prop] + editorHasBlockWithTypeAndProps(editor, blockType, propSchema) && + block.type === blockType ); } -export function checkBlockHasDefaultProp< - Prop extends keyof typeof defaultProps, - I extends InlineContentSchema, +// TODO: Only used in the emoji picker - is it even needed? If so, needs to be +// changed to be like the block type guards. +export function checkDefaultInlineContentTypeInSchema< + InlineContentType extends keyof DefaultInlineContentSchema, + B extends BlockSchema, S extends StyleSchema, >( - prop: Prop, - block: Block, - editor: BlockNoteEditor, -): block is BlockFromConfig< - { - type: string; - propSchema: { - [P in Prop]: (typeof defaultProps)[P]; - }; - content: "table" | "inline" | "none"; - }, - I, + inlineContentType: InlineContentType, + editor: BlockNoteEditor, +): editor is BlockNoteEditor< + B, + { [K in InlineContentType]: DefaultInlineContentSchema[InlineContentType] }, S > { - return checkBlockTypeHasDefaultProp(prop, block.type, editor); + return ( + inlineContentType in editor.schema.inlineContentSchema && + editor.schema.inlineContentSchema[inlineContentType] === + defaultInlineContentSchema[inlineContentType] + ); } export function isTableCellSelection( diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 81e32dd5ce..bc43c0c7a4 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -14,7 +14,7 @@ import { numberedListItem, pageBreak, paragraph, - quoteBlock, + quote, toggleListItem, video, } from "../blks/index.js"; @@ -45,7 +45,7 @@ export const defaultBlockSpecs = { heading: heading.definition(), numberedListItem: numberedListItem.definition(), pageBreak: pageBreak.definition(), - quoteBlock: quoteBlock.definition(), + quote: quote.definition(), toggleListItem: toggleListItem.definition(), file: file.definition(), image: image.definition(), diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 096f15edee..ae989e1e09 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -88,7 +88,7 @@ import { TextCursorPosition } from "./cursorPositionTypes.js"; import { Selection } from "./selectionTypes.js"; import { transformPasted } from "./transformPasted.js"; -import { checkDefaultBlockTypeInSchema } from "../blocks/defaultBlockTypeGuards.js"; +import { editorHasBlockWithType } from "../blocks/defaultBlockTypeGuards.js"; import { BlockNoteSchema } from "./BlockNoteSchema.js"; import { BlockNoteTipTapEditor, @@ -682,7 +682,7 @@ export class BlockNoteEditor< disableExtensions: newOptions.disableExtensions, setIdAttribute: newOptions.setIdAttribute, animations: newOptions.animations ?? true, - tableHandles: checkDefaultBlockTypeInSchema("table", this), + tableHandles: editorHasBlockWithType(this, "table"), dropCursor: this.options.dropCursor ?? dropCursor, placeholders: newOptions.placeholders, tabBehavior: newOptions.tabBehavior, diff --git a/packages/core/src/editor/playground.ts b/packages/core/src/editor/playground.ts index ce358a44c7..86ffa125c4 100644 --- a/packages/core/src/editor/playground.ts +++ b/packages/core/src/editor/playground.ts @@ -9,7 +9,7 @@ import { numberedListItem, pageBreak, paragraph, - quoteBlock, + quote, toggleListItem, video, } from "../blks/index.js"; @@ -38,7 +38,7 @@ const defaultBlockSpecs = { heading: heading.definition, numberedListItem: numberedListItem.definition, pageBreak: pageBreak.definition, - quoteBlock: quoteBlock.definition, + quote: quote.definition, toggleListItem: toggleListItem.definition, file: file.definition, image: image.definition, diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index bcc95c83a7..593deebd1d 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -1,7 +1,14 @@ -import { Block, PartialBlock } from "../../blocks/defaultBlocks.js"; +import { + Block, + defaultBlockSpecs, + PartialBlock, +} from "../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; -import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards.js"; +import { + editorHasBlockWithType, + editorHasBlockWithTypeAndProps, +} from "../../blocks/defaultBlockTypeGuards.js"; import { BlockSchema, InlineContentSchema, @@ -87,7 +94,11 @@ export function getDefaultSlashMenuItems< >(editor: BlockNoteEditor) { const items: DefaultSuggestionItem[] = []; - if (checkDefaultBlockTypeInSchema("heading", editor)) { + if ( + editorHasBlockWithTypeAndProps(editor, "heading", { + level: defaultBlockSpecs["heading"].config.propSchema.level, + }) + ) { items.push( { onItemClick: () => { @@ -125,7 +136,7 @@ export function getDefaultSlashMenuItems< ); } - if (checkDefaultBlockTypeInSchema("quote", editor)) { + if (editorHasBlockWithType(editor, "quote")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -137,7 +148,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("toggleListItem", editor)) { + if (editorHasBlockWithType(editor, "toggleListItem")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -150,7 +161,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("numberedListItem", editor)) { + if (editorHasBlockWithType(editor, "numberedListItem")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -163,7 +174,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("bulletListItem", editor)) { + if (editorHasBlockWithType(editor, "bulletListItem")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -176,7 +187,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("checkListItem", editor)) { + if (editorHasBlockWithType(editor, "checkListItem")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -189,7 +200,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("paragraph", editor)) { + if (editorHasBlockWithType(editor, "paragraph")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -202,7 +213,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("codeBlock", editor)) { + if (editorHasBlockWithType(editor, "codeBlock")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -215,7 +226,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("table", editor)) { + if (editorHasBlockWithType(editor, "table")) { items.push({ onItemClick: () => { insertOrUpdateBlock(editor, { @@ -230,7 +241,7 @@ export function getDefaultSlashMenuItems< cells: ["", "", ""], }, ], - }, + } as any, }); }, badge: undefined, @@ -239,7 +250,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("image", editor)) { + if (editorHasBlockWithType(editor, "image")) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -258,7 +269,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("video", editor)) { + if (editorHasBlockWithType(editor, "video")) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -277,7 +288,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("audio", editor)) { + if (editorHasBlockWithType(editor, "audio")) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -296,7 +307,7 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("file", editor)) { + if (editorHasBlockWithType(editor, "file")) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -315,7 +326,14 @@ export function getDefaultSlashMenuItems< }); } - if (checkDefaultBlockTypeInSchema("heading", editor)) { + if ( + editorHasBlockWithTypeAndProps(editor, "heading", { + level: defaultBlockSpecs["heading"].config.propSchema.level, + isToggleable: { + default: true, + }, + }) + ) { items.push( { onItemClick: () => { diff --git a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts index 388cb47f36..a8e444db71 100644 --- a/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts +++ b/packages/core/src/extensions/TableHandles/TableHandlesPlugin.ts @@ -27,7 +27,7 @@ import { import { nodeToBlock } from "../../api/nodeConversions/nodeToBlock.js"; import { getNodeById } from "../../api/nodeUtil.js"; import { - checkBlockIsDefaultType, + editorHasBlockWithType, isTableCellSelection, } from "../../blocks/defaultBlockTypeGuards.js"; import { DefaultBlockSchema } from "../../blocks/defaultBlocks.js"; @@ -278,7 +278,7 @@ export class TableHandlesView< this.editor.schema.styleSchema, ); - if (checkBlockIsDefaultType("table", block, this.editor)) { + if (editorHasBlockWithType(this.editor, "table")) { this.tablePos = pmNodeInfo.posBeforeNode + 1; tableBlock = block; } diff --git a/packages/react/src/components/Comments/schema.ts b/packages/react/src/components/Comments/schema.ts index 576aa2c3ef..38b65ce88f 100644 --- a/packages/react/src/components/Comments/schema.ts +++ b/packages/react/src/components/Comments/schema.ts @@ -1,20 +1,8 @@ -import { - BlockNoteSchema, - createBlockSpecFromStronglyTypedTiptapNode, - createStronglyTypedTiptapNode, - defaultBlockSpecs, - defaultStyleSpecs, -} from "@blocknote/core"; +import { BlockNoteSchema, defaultStyleSpecs } from "@blocknote/core"; +import { paragraph } from "../../../../core/src/blks"; // this is quite convoluted. we'll clean this up when we make // it easier to extend / customize the default blocks -const paragraph = createBlockSpecFromStronglyTypedTiptapNode( - createStronglyTypedTiptapNode<"paragraph", "inline*">( - defaultBlockSpecs.paragraph.implementation.node.config as any, - ), - // disable default props on paragraph (such as textalignment and colors) - {}, -); // remove textColor, backgroundColor from styleSpecs const { textColor, backgroundColor, ...styleSpecs } = defaultStyleSpecs; @@ -22,7 +10,7 @@ const { textColor, backgroundColor, ...styleSpecs } = defaultStyleSpecs; // the schema to use for comments export const schema = BlockNoteSchema.create({ blockSpecs: { - paragraph, + paragraph: paragraph.definition(), }, styleSpecs, }); diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx index e650875720..f1ca112f0a 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx @@ -75,10 +75,9 @@ export const UploadTab = < ); const config = editor.schema.blockSchema[block.type]; - const accept = - config.isFileBlock && config.fileBlockAccept?.length - ? config.fileBlockAccept.join(",") - : "*/*"; + const accept = config.meta?.fileBlockAccept?.length + ? config.meta.fileBlockAccept.join(",") + : "*/*"; return ( diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx index 204e5bc5dc..48c6b90a31 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx @@ -1,7 +1,6 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlock, - checkBlockIsFileBlockWithPlaceholder, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +40,12 @@ export const FileCaptionButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsFileBlock(block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { + url: { default: "" }, + caption: { default: "" }, + }) + ) { setCurrentEditingCaption(block.props.caption); return block; } @@ -69,11 +73,7 @@ export const FileCaptionButton = () => { [], ); - if ( - !fileBlock || - checkBlockIsFileBlockWithPlaceholder(fileBlock, editor) || - !editor.isEditable - ) { + if (!fileBlock || fileBlock.props.url === "" || !editor.isEditable) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx index 747e5942a0..6e1ab879a6 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx @@ -1,7 +1,6 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlock, - checkBlockIsFileBlockWithPlaceholder, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -33,7 +32,9 @@ export const FileDeleteButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsFileBlock(block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { url: { default: "" } }) + ) { return block; } @@ -45,11 +46,7 @@ export const FileDeleteButton = () => { editor.removeBlocks([fileBlock!]); }, [editor, fileBlock]); - if ( - !fileBlock || - checkBlockIsFileBlockWithPlaceholder(fileBlock, editor) || - !editor.isEditable - ) { + if (!fileBlock || fileBlock.props.url === "" || !editor.isEditable) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx index a80eb5e50e..ce0896cda8 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx @@ -1,7 +1,6 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlock, - checkBlockIsFileBlockWithPlaceholder, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -34,7 +33,9 @@ export const FileDownloadButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsFileBlock(block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { url: { default: "" } }) + ) { return block; } @@ -57,7 +58,7 @@ export const FileDownloadButton = () => { } }, [editor, fileBlock]); - if (!fileBlock || checkBlockIsFileBlockWithPlaceholder(fileBlock, editor)) { + if (!fileBlock || fileBlock.props.url === "") { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx index c52159c3d7..1439d779db 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx @@ -1,7 +1,7 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlockWithPlaceholder, - checkBlockIsFileBlockWithPreview, + editorHasBlockWithTypeAndProps, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -33,7 +33,12 @@ export const FilePreviewButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsFileBlockWithPreview(block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { + url: { default: "" }, + showPreview: { default: true }, + }) + ) { return block; } @@ -41,20 +46,22 @@ export const FilePreviewButton = () => { }, [editor, selectedBlocks]); const onClick = useCallback(() => { - if (fileBlock) { + if ( + fileBlock && + editorHasBlockWithTypeAndProps(editor, fileBlock.type, { + url: { default: "" }, + showPreview: { default: true }, + }) + ) { editor.updateBlock(fileBlock, { props: { - showPreview: !fileBlock.props.showPreview as any, // TODO + showPreview: !fileBlock.props.showPreview, }, }); } }, [editor, fileBlock]); - if ( - !fileBlock || - checkBlockIsFileBlockWithPlaceholder(fileBlock, editor) || - !editor.isEditable - ) { + if (!fileBlock || fileBlock.props.url === "" || !editor.isEditable) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx index 595b9c5271..f1c7c92a4f 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx @@ -1,7 +1,6 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlock, - checkBlockIsFileBlockWithPlaceholder, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +40,12 @@ export const FileRenameButton = () => { const block = selectedBlocks[0]; - if (checkBlockIsFileBlock(block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { + url: { default: "" }, + name: { default: "" }, + }) + ) { setCurrentEditingName(block.props.name); return block; } @@ -69,11 +73,7 @@ export const FileRenameButton = () => { [], ); - if ( - !fileBlock || - checkBlockIsFileBlockWithPlaceholder(fileBlock, editor) || - !editor.isEditable - ) { + if (!fileBlock || fileBlock.props.name === "" || !editor.isEditable) { return null; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx index 7770cd4fd3..22fd3d439b 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx @@ -1,6 +1,6 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockIsFileBlock, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -35,7 +35,9 @@ export const FileReplaceButton = () => { if ( block === undefined || - !checkBlockIsFileBlock(block, editor) || + !blockHasTypeAndProps(block, editor, block.type, { + url: { default: "" }, + }) || !editor.isEditable ) { return null; diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx index 934fc3a32a..bbe14def1d 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx @@ -1,8 +1,9 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockHasDefaultProp, - checkBlockTypeHasDefaultProp, + defaultProps, DefaultProps, + editorHasBlockWithTypeAndProps, InlineContentSchema, mapTableCell, StyleSchema, @@ -46,7 +47,11 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { const textAlignment = useMemo(() => { const block = selectedBlocks[0]; - if (checkBlockHasDefaultProp("textAlignment", block, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { + textAlignment: defaultProps.textAlignment, + }) + ) { return block.props.textAlignment; } if (block.type === "table") { @@ -75,7 +80,14 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { editor.focus(); for (const block of selectedBlocks) { - if (checkBlockTypeHasDefaultProp("textAlignment", block.type, editor)) { + if ( + blockHasTypeAndProps(block, editor, block.type, { + textAlignment: defaultProps.textAlignment, + }) && + editorHasBlockWithTypeAndProps(editor, block.type, { + textAlignment: defaultProps.textAlignment, + }) + ) { editor.updateBlock(block, { props: { textAlignment: textAlignment }, }); diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx index 5e27e1eb21..56acea6fac 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx @@ -1,10 +1,11 @@ import { + blockHasTypeAndProps, BlockSchema, - checkBlockHasDefaultProp, - checkBlockTypeHasDefaultProp, DefaultBlockSchema, DefaultInlineContentSchema, + defaultProps, DefaultStyleSchema, + editorHasBlockWithTypeAndProps, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -29,8 +30,12 @@ export const BlockColorsItem = < const editor = useBlockNoteEditor(); if ( - !checkBlockTypeHasDefaultProp("textColor", props.block.type, editor) && - !checkBlockTypeHasDefaultProp("backgroundColor", props.block.type, editor) + !blockHasTypeAndProps(props.block, editor, props.block.type, { + textColor: defaultProps.textColor, + }) || + !blockHasTypeAndProps(props.block, editor, props.block.type, { + backgroundColor: defaultProps.backgroundColor, + }) ) { return null; } @@ -53,11 +58,12 @@ export const BlockColorsItem = < @@ -69,12 +75,12 @@ export const BlockColorsItem = < : undefined } background={ - checkBlockTypeHasDefaultProp( - "backgroundColor", - props.block.type, - editor, - ) && - checkBlockHasDefaultProp("backgroundColor", props.block, editor) + blockHasTypeAndProps(props.block, editor, props.block.type, { + backgroundColor: defaultProps.backgroundColor, + }) && + editorHasBlockWithTypeAndProps(editor, props.block.type, { + backgroundColor: defaultProps.backgroundColor, + }) ? { color: props.block.props.backgroundColor, setColor: (color) => From 97295ceea74afafb3724cb226cf149eebcef41bd Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 6 Aug 2025 13:24:09 +0200 Subject: [PATCH 2/5] Updated type guard definitions --- .../src/FileReplaceButton.tsx | 4 +- .../core/src/blocks/defaultBlockTypeGuards.ts | 197 +++++++++--------- .../getDefaultSlashMenuItems.ts | 27 +-- .../DefaultButtons/FileCaptionButton.tsx | 19 +- .../DefaultButtons/FileDeleteButton.tsx | 6 +- .../DefaultButtons/FileDownloadButton.tsx | 6 +- .../DefaultButtons/FilePreviewButton.tsx | 15 +- .../DefaultButtons/FileRenameButton.tsx | 19 +- .../DefaultButtons/FileReplaceButton.tsx | 6 +- .../DefaultButtons/TextAlignButton.tsx | 16 +- .../DefaultItems/BlockColorsItem.tsx | 45 ++-- 11 files changed, 189 insertions(+), 171 deletions(-) diff --git a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx index c0442bb2f7..f1c8c84293 100644 --- a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx @@ -1,6 +1,6 @@ import { BlockSchema, - blockHasProps, + blockHasType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,7 +41,7 @@ export const FileReplaceButton = () => { if ( block === undefined || - !blockHasProps(block, { url: { default: "" } }) || + !blockHasType(block, editor, ["url"]) || !editor.isEditable ) { return null; diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 64731a8a06..ffe59f73a6 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -14,42 +14,31 @@ import { } from "./defaultBlocks.js"; import { Selection } from "prosemirror-state"; -export function editorHasBlockWithType( - editor: BlockNoteEditor, - blockType: BType, -): editor is BlockNoteEditor< - { - [BT in BType]: BlockConfig< - BT, - { - [PN in string]: PropSpec; - } - >; - }, - any, - any -> { - if (!(blockType in editor.schema.blockSpecs)) { - return false; - } - - if (editor.schema.blockSpecs[blockType].config.type !== blockType) { - return false; - } - - return true; -} - -export function editorHasBlockWithTypeAndProps< +export function editorHasBlockWithType< BType extends string, - PSchema extends PropSchema, + Props extends + | PropSchema + | Record + | undefined = undefined, >( editor: BlockNoteEditor, blockType: BType, - propSchema: PSchema, + props?: Props, ): editor is BlockNoteEditor< { - [BT in BType]: BlockConfig; + [BT in BType]: Props extends PropSchema + ? BlockConfig + : Props extends Record + ? BlockConfig< + BT, + { + [PN in keyof Props]: { + default: undefined; + type: Props[PN]; + }; + } + > + : BlockConfig; }, any, any @@ -57,113 +46,135 @@ export function editorHasBlockWithTypeAndProps< if (!editorHasBlockWithType(editor, blockType)) { return false; } + if (!props) { + return true; + } - for (const [propName, propSpec] of Object.entries(propSchema)) { + for (const [propName, propSpec] of Object.entries(props)) { if (!(propName in editor.schema.blockSpecs[blockType].config.propSchema)) { return false; } - if ( - editor.schema.blockSpecs[blockType].config.propSchema[propName] - .default !== propSpec.default - ) { - return false; - } - - if ( - typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] - .values !== typeof propSpec.values - ) { - return false; - } + if (typeof propSpec === "string") { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default && + typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default !== propSpec + ) { + return false; + } - if ( - typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] - .values === "object" && - typeof propSpec.values === "object" - ) { if ( - editor.schema.blockSpecs[blockType].config.propSchema[propName].values - .length !== propSpec.values.length + editor.schema.blockSpecs[blockType].config.propSchema[propName].type && + editor.schema.blockSpecs[blockType].config.propSchema[propName].type !== + propSpec + ) { + return false; + } + } else { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default !== propSpec.default ) { return false; } - for ( - let i = 0; - i < - editor.schema.blockSpecs[blockType].config.propSchema[propName].values - .length; - i++ + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .default === undefined && + propSpec.default === undefined ) { if ( editor.schema.blockSpecs[blockType].config.propSchema[propName] - .values[i] !== propSpec.values[i] + .type !== propSpec.type ) { return false; } } - } - if ( - editor.schema.blockSpecs[blockType].config.propSchema[propName] - .default === undefined && - propSpec.default === undefined - ) { if ( - editor.schema.blockSpecs[blockType].config.propSchema[propName].type !== - propSpec.type + typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values !== typeof propSpec.values ) { return false; } + + if ( + typeof editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values === "object" && + typeof propSpec.values === "object" + ) { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName].values + .length !== propSpec.values.length + ) { + return false; + } + + for ( + let i = 0; + i < + editor.schema.blockSpecs[blockType].config.propSchema[propName].values + .length; + i++ + ) { + if ( + editor.schema.blockSpecs[blockType].config.propSchema[propName] + .values[i] !== propSpec.values[i] + ) { + return false; + } + } + } } } return true; } -export function blockHasType( - block: Block, - editor: BlockNoteEditor, - blockType: BType, -): block is Block< - { - [BT in string]: BlockConfig< - BT, - { - [PN in string]: PropSpec; - } - >; - }, - any, - any -> { - return editorHasBlockWithType(editor, blockType) && block.type === blockType; -} - -export function blockHasTypeAndProps< +export function blockHasType< BType extends string, - PSchema extends PropSchema, + Props extends + | PropSchema + | Record + | undefined = undefined, >( block: Block, editor: BlockNoteEditor, blockType: BType, - propSchema: PSchema, + props?: Props, ): block is Block< { - [BT in string]: BlockConfig; + [BT in BType]: Props extends PropSchema + ? BlockConfig + : Props extends Record + ? BlockConfig< + BT, + { + [PN in keyof Props]: PropSpec< + Props[PN] extends "boolean" + ? boolean + : Props[PN] extends "number" + ? number + : Props[PN] extends "string" + ? string + : never + >; + } + > + : BlockConfig; }, any, any > { return ( - editorHasBlockWithTypeAndProps(editor, blockType, propSchema) && - block.type === blockType + editorHasBlockWithType(editor, blockType, props) && block.type === blockType ); } -// TODO: Only used in the emoji picker - is it even needed? If so, needs to be -// changed to be like the block type guards. +// TODO: Only used once in the emoji picker - is it even needed? If so, should +// be changed to be like the block type guards. export function checkDefaultInlineContentTypeInSchema< InlineContentType extends keyof DefaultInlineContentSchema, B extends BlockSchema, diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 593deebd1d..b37d4be1da 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -5,10 +5,7 @@ import { } from "../../blocks/defaultBlocks.js"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; -import { - editorHasBlockWithType, - editorHasBlockWithTypeAndProps, -} from "../../blocks/defaultBlockTypeGuards.js"; +import { editorHasBlockWithType } from "../../blocks/defaultBlockTypeGuards.js"; import { BlockSchema, InlineContentSchema, @@ -94,11 +91,7 @@ export function getDefaultSlashMenuItems< >(editor: BlockNoteEditor) { const items: DefaultSuggestionItem[] = []; - if ( - editorHasBlockWithTypeAndProps(editor, "heading", { - level: defaultBlockSpecs["heading"].config.propSchema.level, - }) - ) { + if (editorHasBlockWithType(editor, "heading", { level: "number" })) { items.push( { onItemClick: () => { @@ -250,7 +243,7 @@ export function getDefaultSlashMenuItems< }); } - if (editorHasBlockWithType(editor, "image")) { + if (editorHasBlockWithType(editor, "image", { url: "string" })) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -269,7 +262,7 @@ export function getDefaultSlashMenuItems< }); } - if (editorHasBlockWithType(editor, "video")) { + if (editorHasBlockWithType(editor, "video", { url: "string" })) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -288,7 +281,7 @@ export function getDefaultSlashMenuItems< }); } - if (editorHasBlockWithType(editor, "audio")) { + if (editorHasBlockWithType(editor, "audio", { url: "string" })) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -307,7 +300,7 @@ export function getDefaultSlashMenuItems< }); } - if (editorHasBlockWithType(editor, "file")) { + if (editorHasBlockWithType(editor, "file", { url: "string" })) { items.push({ onItemClick: () => { const insertedBlock = insertOrUpdateBlock(editor, { @@ -327,11 +320,9 @@ export function getDefaultSlashMenuItems< } if ( - editorHasBlockWithTypeAndProps(editor, "heading", { - level: defaultBlockSpecs["heading"].config.propSchema.level, - isToggleable: { - default: true, - }, + editorHasBlockWithType(editor, "heading", { + level: "number", + isToggleable: "boolean", }) ) { items.push( diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx index 48c6b90a31..38b1fef90c 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx @@ -1,6 +1,7 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, + editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,9 +42,9 @@ export const FileCaptionButton = () => { const block = selectedBlocks[0]; if ( - blockHasTypeAndProps(block, editor, block.type, { - url: { default: "" }, - caption: { default: "" }, + blockHasType(block, editor, block.type, { + url: "string", + caption: "string", }) ) { setCurrentEditingCaption(block.props.caption); @@ -55,11 +56,17 @@ export const FileCaptionButton = () => { const handleEnter = useCallback( (event: KeyboardEvent) => { - if (fileBlock && event.key === "Enter") { + if ( + fileBlock && + editorHasBlockWithType(editor, fileBlock.type, { + caption: "string", + }) && + event.key === "Enter" + ) { event.preventDefault(); editor.updateBlock(fileBlock, { props: { - caption: currentEditingCaption as any, // TODO + caption: currentEditingCaption, }, }); } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx index 6e1ab879a6..2ea096de36 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDeleteButton.tsx @@ -1,5 +1,5 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, InlineContentSchema, StyleSchema, @@ -32,9 +32,7 @@ export const FileDeleteButton = () => { const block = selectedBlocks[0]; - if ( - blockHasTypeAndProps(block, editor, block.type, { url: { default: "" } }) - ) { + if (blockHasType(block, editor, block.type, { url: "string" })) { return block; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx index ce0896cda8..d19e202564 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileDownloadButton.tsx @@ -1,5 +1,5 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, InlineContentSchema, StyleSchema, @@ -33,9 +33,7 @@ export const FileDownloadButton = () => { const block = selectedBlocks[0]; - if ( - blockHasTypeAndProps(block, editor, block.type, { url: { default: "" } }) - ) { + if (blockHasType(block, editor, block.type, { url: "string" })) { return block; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx index 1439d779db..9e8fcb0425 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FilePreviewButton.tsx @@ -1,7 +1,7 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, - editorHasBlockWithTypeAndProps, + editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -34,9 +34,9 @@ export const FilePreviewButton = () => { const block = selectedBlocks[0]; if ( - blockHasTypeAndProps(block, editor, block.type, { - url: { default: "" }, - showPreview: { default: true }, + blockHasType(block, editor, block.type, { + url: "string", + showPreview: "boolean", }) ) { return block; @@ -48,9 +48,8 @@ export const FilePreviewButton = () => { const onClick = useCallback(() => { if ( fileBlock && - editorHasBlockWithTypeAndProps(editor, fileBlock.type, { - url: { default: "" }, - showPreview: { default: true }, + editorHasBlockWithType(editor, fileBlock.type, { + showPreview: "boolean", }) ) { editor.updateBlock(fileBlock, { diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx index f1c7c92a4f..583494917f 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx @@ -1,6 +1,7 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, + editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -41,9 +42,9 @@ export const FileRenameButton = () => { const block = selectedBlocks[0]; if ( - blockHasTypeAndProps(block, editor, block.type, { - url: { default: "" }, - name: { default: "" }, + blockHasType(block, editor, block.type, { + url: "string", + name: "string", }) ) { setCurrentEditingName(block.props.name); @@ -55,11 +56,17 @@ export const FileRenameButton = () => { const handleEnter = useCallback( (event: KeyboardEvent) => { - if (fileBlock && event.key === "Enter") { + if ( + fileBlock && + editorHasBlockWithType(editor, fileBlock.type, { + name: "string", + }) && + event.key === "Enter" + ) { event.preventDefault(); editor.updateBlock(fileBlock, { props: { - name: currentEditingName as any, // TODO + name: currentEditingName, }, }); } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx index 22fd3d439b..ed68593186 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileReplaceButton.tsx @@ -1,5 +1,5 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, InlineContentSchema, StyleSchema, @@ -35,8 +35,8 @@ export const FileReplaceButton = () => { if ( block === undefined || - !blockHasTypeAndProps(block, editor, block.type, { - url: { default: "" }, + !blockHasType(block, editor, block.type, { + url: "string", }) || !editor.isEditable ) { diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx index bbe14def1d..05b38f6a7a 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/TextAlignButton.tsx @@ -1,9 +1,9 @@ import { - blockHasTypeAndProps, + blockHasType, BlockSchema, defaultProps, DefaultProps, - editorHasBlockWithTypeAndProps, + editorHasBlockWithType, InlineContentSchema, mapTableCell, StyleSchema, @@ -48,7 +48,7 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { const block = selectedBlocks[0]; if ( - blockHasTypeAndProps(block, editor, block.type, { + blockHasType(block, editor, block.type, { textAlignment: defaultProps.textAlignment, }) ) { @@ -81,10 +81,10 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { for (const block of selectedBlocks) { if ( - blockHasTypeAndProps(block, editor, block.type, { + blockHasType(block, editor, block.type, { textAlignment: defaultProps.textAlignment, }) && - editorHasBlockWithTypeAndProps(editor, block.type, { + editorHasBlockWithType(editor, block.type, { textAlignment: defaultProps.textAlignment, }) ) { @@ -134,10 +134,12 @@ export const TextAlignButton = (props: { textAlignment: TextAlignment }) => { const show = useMemo(() => { return !!selectedBlocks.find( (block) => - "textAlignment" in block.props || + blockHasType(block, editor, block.type, { + textAlignment: defaultProps.textAlignment, + }) || (block.type === "table" && block.children), ); - }, [selectedBlocks]); + }, [editor, selectedBlocks]); if (!show || !editor.isEditable) { return null; diff --git a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx index 56acea6fac..c8733458d6 100644 --- a/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx +++ b/packages/react/src/components/SideMenu/DragHandleMenu/DefaultItems/BlockColorsItem.tsx @@ -1,11 +1,11 @@ import { - blockHasTypeAndProps, + Block, + blockHasType, BlockSchema, DefaultBlockSchema, DefaultInlineContentSchema, - defaultProps, DefaultStyleSchema, - editorHasBlockWithTypeAndProps, + editorHasBlockWithType, InlineContentSchema, StyleSchema, } from "@blocknote/core"; @@ -29,12 +29,17 @@ export const BlockColorsItem = < const editor = useBlockNoteEditor(); + // We cast the block to a generic one, as the base type causes type errors + // with runtime type checking using `blockHasType`. Runtime type checking is + // more valuable than static checks, so better to do it like this. + const block = props.block as Block; + if ( - !blockHasTypeAndProps(props.block, editor, props.block.type, { - textColor: defaultProps.textColor, + !blockHasType(block, editor, block.type, { + textColor: "string", }) || - !blockHasTypeAndProps(props.block, editor, props.block.type, { - backgroundColor: defaultProps.backgroundColor, + !blockHasType(block, editor, block.type, { + backgroundColor: "string", }) ) { return null; @@ -58,33 +63,33 @@ export const BlockColorsItem = < - editor.updateBlock(props.block, { - type: props.block.type, + editor.updateBlock(block, { + type: block.type, props: { textColor: color }, }), } : undefined } background={ - blockHasTypeAndProps(props.block, editor, props.block.type, { - backgroundColor: defaultProps.backgroundColor, + blockHasType(block, editor, block.type, { + backgroundColor: "string", }) && - editorHasBlockWithTypeAndProps(editor, props.block.type, { - backgroundColor: defaultProps.backgroundColor, + editorHasBlockWithType(editor, block.type, { + backgroundColor: "string", }) ? { - color: props.block.props.backgroundColor, + color: block.props.backgroundColor, setColor: (color) => - editor.updateBlock(props.block, { + editor.updateBlock(block, { props: { backgroundColor: color }, }), } From 1ff7474283abf1529aaddd3ad379cbcb0d89a773 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 6 Aug 2025 13:24:59 +0200 Subject: [PATCH 3/5] Small fix --- .../11-uppy-file-panel/src/FileReplaceButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx index f1c8c84293..dc718437b1 100644 --- a/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx +++ b/examples/03-ui-components/11-uppy-file-panel/src/FileReplaceButton.tsx @@ -41,7 +41,7 @@ export const FileReplaceButton = () => { if ( block === undefined || - !blockHasType(block, editor, ["url"]) || + !blockHasType(block, editor, { url: "string" }) || !editor.isEditable ) { return null; From fc9e4520611e641220c5b24e02bf5ab47da3f94f Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 6 Aug 2025 17:13:30 +0200 Subject: [PATCH 4/5] Removed `checkDefaultInlineContentTypeInSchema` --- .../core/src/blocks/defaultBlockTypeGuards.ts | 35 ++----------------- .../getDefaultEmojiPickerItems.ts | 8 +++-- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index ffe59f73a6..82b35f24b0 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -1,17 +1,7 @@ import { CellSelection } from "prosemirror-tables"; import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js"; -import { - BlockConfig, - BlockSchema, - PropSchema, - PropSpec, - StyleSchema, -} from "../schema/index.js"; -import { - Block, - DefaultInlineContentSchema, - defaultInlineContentSchema, -} from "./defaultBlocks.js"; +import { BlockConfig, PropSchema, PropSpec } from "../schema/index.js"; +import { Block } from "./defaultBlocks.js"; import { Selection } from "prosemirror-state"; export function editorHasBlockWithType< @@ -173,27 +163,6 @@ export function blockHasType< ); } -// TODO: Only used once in the emoji picker - is it even needed? If so, should -// be changed to be like the block type guards. -export function checkDefaultInlineContentTypeInSchema< - InlineContentType extends keyof DefaultInlineContentSchema, - B extends BlockSchema, - S extends StyleSchema, ->( - inlineContentType: InlineContentType, - editor: BlockNoteEditor, -): editor is BlockNoteEditor< - B, - { [K in InlineContentType]: DefaultInlineContentSchema[InlineContentType] }, - S -> { - return ( - inlineContentType in editor.schema.inlineContentSchema && - editor.schema.inlineContentSchema[inlineContentType] === - defaultInlineContentSchema[inlineContentType] - ); -} - export function isTableCellSelection( selection: Selection, ): selection is CellSelection { diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts index 4f02c94625..85ab84ded2 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts @@ -1,6 +1,6 @@ import type { Emoji, EmojiMartData } from "@emoji-mart/data"; -import { checkDefaultInlineContentTypeInSchema } from "../../blocks/defaultBlockTypeGuards.js"; +import { defaultInlineContentSchema } from "../../blocks/defaultBlocks.js"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; import { BlockSchema, @@ -54,7 +54,11 @@ export async function getDefaultEmojiPickerItems< editor: BlockNoteEditor, query: string, ): Promise { - if (!checkDefaultInlineContentTypeInSchema("text", editor)) { + if ( + !("text" in editor.schema.inlineContentSchema) || + editor.schema.inlineContentSchema["text"] !== + defaultInlineContentSchema["text"] + ) { return []; } From a0fc6e2f2185b4c955335669a3d2d99c291f8bb5 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 6 Aug 2025 17:20:58 +0200 Subject: [PATCH 5/5] Small fixes --- packages/core/src/blks/Heading/definition.ts | 2 ++ packages/core/src/blocks/defaultBlockTypeGuards.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/src/blks/Heading/definition.ts b/packages/core/src/blks/Heading/definition.ts index 443c5672ea..0a365b7c51 100644 --- a/packages/core/src/blks/Heading/definition.ts +++ b/packages/core/src/blks/Heading/definition.ts @@ -1,5 +1,6 @@ import { updateBlockTr } from "../../api/blockManipulation/commands/updateBlock/updateBlock.js"; import { getBlockInfoFromTransaction } from "../../api/getBlockInfoFromPos.js"; +import { defaultProps } from "../../blocks/defaultProps.js"; import { createToggleWrapper } from "../../blocks/ToggleWrapper/createToggleWrapper.js"; import { createBlockConfig, @@ -24,6 +25,7 @@ const config = createBlockConfig( }: HeadingOptions = {}) => ({ type: "heading" as const, propSchema: { + ...defaultProps, level: { default: defaultLevel, values: levels }, ...(allowToggleHeadings ? { isToggleable: { default: false } } : {}), }, diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 82b35f24b0..c3db69fdaf 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -33,9 +33,10 @@ export function editorHasBlockWithType< any, any > { - if (!editorHasBlockWithType(editor, blockType)) { + if (!(blockType in editor.schema.blockSpecs)) { return false; } + if (!props) { return true; }