diff --git a/examples/editor/package.json b/examples/editor/package.json
index 1101a3d4ee..791f589532 100644
--- a/examples/editor/package.json
+++ b/examples/editor/package.json
@@ -12,7 +12,8 @@
"@blocknote/core": "^0.8.1",
"@blocknote/react": "^0.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@types/react": "^18.0.25",
diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx
index 55ce63eb93..c4c11f162b 100644
--- a/examples/editor/src/App.tsx
+++ b/examples/editor/src/App.tsx
@@ -1,8 +1,67 @@
// import logo from './logo.svg'
+import { z } from 'zod';
+import { DefaultBlockSchema, defaultBlockSchema } from '@blocknote/core';
+import { BlockNoteView, useBlockNote, createReactBlockSpec, ReactSlashMenuItem, defaultReactSlashMenuItems } from "@blocknote/react";
import "@blocknote/core/style.css";
-import { BlockNoteView, useBlockNote } from "@blocknote/react";
import styles from "./App.module.css";
+export const AccordionBlock = createReactBlockSpec({
+ type: 'accordion',
+ propSchema: z.object({
+ label: z.string(),
+ autoLayout: z.object({
+ enabled: z.boolean(),
+ }).optional(),
+ }),
+ render: ({ editor, block }) => {
+ return (
+ <>
+
{block.props.label}
+ {
+ block.props.autoLayout?.enabled ?
+ (
+
+ Enabled
+
+ ) :
+ <>>
+ }
+ >
+ );
+ },
+ containsInlineContent: false,
+});
+
+// Creates a slash menu item for inserting an image block.
+export const insertAccordion = new ReactSlashMenuItem<
+ DefaultBlockSchema & { accordion: typeof AccordionBlock }
+>(
+ 'Insert Accordion',
+ (editor) => {
+ editor.insertBlocks(
+ [
+ // Default values are set here
+ {
+ type: 'accordion',
+ props: {
+ label: 'Default',
+ autoLayout: {
+ enabled: true
+ }
+ },
+ },
+ ],
+ editor.getTextCursorPosition().block,
+ 'before'
+ );
+ },
+ ['accordion'],
+ 'Containers',
+ <>+>,
+ 'Used to group content in an accordion.'
+);
+
+
type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: any };
function App() {
@@ -10,6 +69,14 @@ function App() {
onEditorContentChange: (editor) => {
console.log(editor.topLevelBlocks);
},
+ blockSchema: {
+ ...defaultBlockSchema,
+ accordion: AccordionBlock,
+ },
+ slashCommands: [
+ ...defaultReactSlashMenuItems,
+ insertAccordion
+ ],
editorDOMAttributes: {
class: styles.editor,
"data-test": "editor",
diff --git a/package-lock.json b/package-lock.json
index f66c089540..0e7e071cd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,7 +25,8 @@
"@blocknote/core": "^0.8.1",
"@blocknote/react": "^0.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@types/react": "^18.0.25",
@@ -20459,6 +20460,14 @@
"resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
"integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
},
+ "node_modules/zod": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
@@ -20506,7 +20515,8 @@
"uuid": "^8.3.2",
"y-prosemirror": "1.0.20",
"y-protocols": "^1.0.5",
- "yjs": "^13.6.1"
+ "yjs": "^13.6.1",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@types/hast": "^2.3.4",
diff --git a/packages/core/package.json b/packages/core/package.json
index 91c2b7fedc..beb372216d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -82,7 +82,8 @@
"uuid": "^8.3.2",
"y-prosemirror": "1.0.20",
"y-protocols": "^1.0.5",
- "yjs": "^13.6.1"
+ "yjs": "^13.6.1",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@types/hast": "^2.3.4",
diff --git a/packages/core/src/api/formatConversions/formatConversions.test.ts b/packages/core/src/api/formatConversions/formatConversions.test.ts
index 0d8e4054c1..5da57ecec8 100644
--- a/packages/core/src/api/formatConversions/formatConversions.test.ts
+++ b/packages/core/src/api/formatConversions/formatConversions.test.ts
@@ -35,8 +35,8 @@ beforeEach(() => {
id: UniqueID.options.generateID(),
type: "heading",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
level: "1",
},
@@ -53,8 +53,8 @@ beforeEach(() => {
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -70,8 +70,8 @@ beforeEach(() => {
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -87,8 +87,8 @@ beforeEach(() => {
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -116,8 +116,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "heading",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
level: "1",
},
@@ -133,8 +133,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -149,8 +149,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -165,8 +165,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -200,8 +200,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -323,8 +323,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -343,8 +343,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -379,8 +379,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -415,8 +415,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -432,8 +432,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -448,8 +448,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -464,8 +464,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -481,8 +481,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "paragraph",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -498,8 +498,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -515,8 +515,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -532,8 +532,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -548,8 +548,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "numberedListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -567,8 +567,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -586,8 +586,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
@@ -605,8 +605,8 @@ Paragraph
id: UniqueID.options.generateID(),
type: "bulletListItem",
props: {
- backgroundColor: "default",
- textColor: "default",
+ backgroundColor: "transparent",
+ textColor: "black",
textAlignment: "left",
},
content: [
diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts
index dc7f53c226..3b07844d4f 100644
--- a/packages/core/src/api/nodeConversions/nodeConversions.ts
+++ b/packages/core/src/api/nodeConversions/nodeConversions.ts
@@ -4,6 +4,7 @@ import {
Block,
BlockSchema,
PartialBlock,
+ Props,
} from "../../extensions/Blocks/api/blockTypes";
import { defaultProps } from "../../extensions/Blocks/api/defaultBlocks";
@@ -148,6 +149,10 @@ export function blockToNode(
let contentNode: Node;
+ if (!block.props) {
+ throw new Error("Block props are undefined");
+ }
+
if (!block.content) {
contentNode = schema.nodes[type].create(block.props);
} else if (typeof block.content === "string") {
@@ -378,22 +383,24 @@ export function nodeToBlock(
id = UniqueID.options.generateID();
}
- const props: any = {};
+ const blockSpec = blockSchema[blockInfo.contentType.name];
+
+ if (!blockSpec) {
+ throw Error(
+ "Block is of an unrecognized type: " + blockInfo.contentType.name
+ );
+ }
+
+ const props: Props = Object.create(null);
+
for (const [attr, value] of Object.entries({
...blockInfo.node.attrs,
...blockInfo.contentNode.attrs,
})) {
- const blockSpec = blockSchema[blockInfo.contentType.name];
- if (!blockSpec) {
- throw Error(
- "Block is of an unrecognized type: " + blockInfo.contentType.name
- );
- }
-
- const propSchema = blockSpec.propSchema;
-
- if (attr in propSchema) {
- props[attr] = value;
+ if (attr in blockSpec.propSchema.shape) {
+ if (props && typeof props === "object") {
+ props[attr as keyof typeof props] = value as never;
+ }
}
// Block ids are stored as node attributes the same way props are, so we
// need to ensure we don't attempt to read block ids as props.
diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts
index 77604299a9..b27f2dfc7d 100644
--- a/packages/core/src/extensions/Blocks/api/block.ts
+++ b/packages/core/src/extensions/Blocks/api/block.ts
@@ -29,20 +29,25 @@ export function propsToAttributes<
) {
const tiptapAttributes: Record = {};
- Object.entries(blockConfig.propSchema).forEach(([name, spec]) => {
+ Object.entries(blockConfig.propSchema.shape).forEach(([name, spec]) => {
tiptapAttributes[name] = {
- default: spec.default,
+ default: null, // It will be provided by `insertBlocks`
keepOnSplit: true,
// Props are displayed in kebab-case as HTML attributes. If a prop's
// value is the same as its default, we don't display an HTML
// attribute for it.
parseHTML: (element) => element.getAttribute(camelToDataKebab(name)),
- renderHTML: (attributes) =>
- attributes[name] !== spec.default
- ? {
- [camelToDataKebab(name)]: attributes[name],
- }
- : {},
+ renderHTML: (attributes) => {
+ const parsed = spec.safeParse(attributes[name]);
+
+ if (!parsed.success || attributes[camelToDataKebab(name)] !== parsed.data) {
+ return {
+ [camelToDataKebab(name)]: typeof attributes[name] === 'string' ? attributes[name] : null,
+ }
+ }
+
+ return {}
+ },
};
});
@@ -162,7 +167,7 @@ export function createBlockSpec<
// Gets BlockNote editor instance
const editor = this.options.editor! as BlockNoteEditor<
- BSchema & { [k in BType]: BlockSpec }
+ { [k in BType]: BlockSpec }
>;
// Gets position of the node
if (typeof getPos === "boolean") {
diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts
index ee49d9921d..88a65ee1fe 100644
--- a/packages/core/src/extensions/Blocks/api/blockTypes.ts
+++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts
@@ -1,4 +1,5 @@
/** Define the main block types **/
+import { z } from "zod";
import { Node, NodeConfig } from "@tiptap/core";
import { BlockNoteEditor } from "../../../BlockNoteEditor";
import { InlineContent, PartialInlineContent } from "./inlineContentTypes";
@@ -31,28 +32,17 @@ export type TipTapNode<
group: "blockContent";
};
-// Defines a single prop spec, which includes the default value the prop should
-// take and possible values it can take.
-export type PropSpec = {
- values?: readonly string[];
- default: string;
-};
-
// Defines multiple block prop specs. The key of each prop is the name of the
// prop, while the value is a corresponding prop spec. This should be included
// in a block config or schema. From a prop schema, we can derive both the props'
// internal implementation (as TipTap node attributes) and the type information
// for the external API.
-export type PropSchema = Record;
+export type PropSchema = z.SomeZodObject;
// Defines Props objects for use in Block objects in the external API. Converts
-// each prop spec into a union type of its possible values, or a string if no
-// values are specified.
-export type Props = {
- [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly string[]
- ? PSchema[PType]["values"][number]
- : string;
-};
+// each prop spec into a union type of its possible values, or the type of the
+// 'default' property if values are not specified.
+export type Props = z.infer;
// Defines the config for a single block. Meant to be used as an argument to
// `createBlockSpec`, which will create a new block spec from it. This is the
@@ -84,7 +74,7 @@ export type BlockConfig<
* This is typed generically. If you want an editor with your custom schema, you need to
* cast it manually, e.g.: `const e = editor as BlockNoteEditor;`
*/
- editor: BlockNoteEditor }>
+ editor: BlockNoteEditor<{ [k in Type]: BlockSpec }>
// (note) if we want to fix the manual cast, we need to prevent circular references and separate block definition and render implementations
// or allow manually passing , but that's not possible without passing the other generics because Typescript doesn't support partial inferred generics
) => ContainsInlineContent extends true
@@ -161,7 +151,7 @@ type PartialBlocksWithoutChildren = {
[BType in keyof BSchema]: Partial<{
id: string;
type: BType;
- props: Partial>;
+ props: Props;
content: PartialInlineContent[] | string;
}>;
};
diff --git a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
index d60d716cce..7ab1790d58 100644
--- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
+++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts
@@ -1,44 +1,50 @@
+import { z } from "zod";
+
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 { PropSchema, TypesMatch } from "./blockTypes";
+
+export const defaultPropSchema = z.object({
+ backgroundColor: z.enum(["transparent", "red", "orange", "yellow", "blue"]).optional(),
+ textColor: z.enum(["black", "red", "orange", "yellow"]).optional(),
+ textAlignment: z.enum(["left", "center", "right", "justify"]).optional(),
+});
export const defaultProps = {
- backgroundColor: {
- default: "transparent" as const,
- },
- textColor: {
- default: "black" as const, // TODO
- },
- textAlignment: {
- default: "left" as const,
- values: ["left", "center", "right", "justify"] as const,
- },
-} satisfies PropSchema;
+ backgroundColor: "transparent",
+ textColor: "black",
+ textAlignment: "left",
+} satisfies z.infer;
export type DefaultProps = typeof defaultProps;
export const defaultBlockSchema = {
paragraph: {
- propSchema: defaultProps,
+ propSchema: defaultPropSchema,
+ props: defaultProps,
node: ParagraphBlockContent,
},
heading: {
- propSchema: {
+ propSchema: defaultPropSchema.merge(z.object({
+ level: z.enum(["1", "2", "3"]).optional(),
+ })),
+ props: {
...defaultProps,
- level: { default: "1", values: ["1", "2", "3"] as const },
+ level: "1",
},
node: HeadingBlockContent,
},
bulletListItem: {
- propSchema: defaultProps,
+ propSchema: defaultPropSchema,
+ props: defaultProps,
node: BulletListItemBlockContent,
},
numberedListItem: {
- propSchema: defaultProps,
+ propSchema: defaultPropSchema,
+ props: defaultProps,
node: NumberedListItemBlockContent,
},
} as const;
-export type DefaultBlockSchema = TypesMatch;
+export type DefaultBlockSchema = typeof defaultBlockSchema;
\ No newline at end of file
diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
index 3120ba8253..d3fad13b76 100644
--- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
+++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts
@@ -200,7 +200,7 @@ export const BlockContainer = Node.create({
: state.schema.nodes[block.type],
{
...contentNode.attrs,
- ...block.props,
+ ...block.props!,
}
);
@@ -208,7 +208,7 @@ export const BlockContainer = Node.create({
// attributes.
state.tr.setNodeMarkup(startPos - 1, undefined, {
...node.attrs,
- ...block.props,
+ ...block.props!,
});
}
diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
index 3fc435b275..4515fb4e8b 100644
--- a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
+++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx
@@ -29,11 +29,15 @@ export const BlockColorsButton = (
setOpened(true);
}, []);
+ if (!(props.block.props && typeof props.block.props === 'object')) {
+ return <>>;
+ }
+
if (
!("textColor" in props.block.props) ||
!("backgroundColor" in props.block.props)
) {
- return null;
+ return <>>;
}
return (
@@ -56,8 +60,8 @@ export const BlockColorsButton = (
style={{ marginLeft: "5px" }}>
props.editor.updateBlock(props.block, {
props: { textColor: color },
diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
index dde4fe6342..5f4cb1cd85 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx
@@ -1,8 +1,9 @@
import {
BlockNoteEditor,
BlockSchema,
- DefaultProps,
PartialBlock,
+ defaultPropSchema,
+ Props,
} from "@blocknote/core";
import { useCallback, useMemo } from "react";
import { IconType } from "react-icons";
@@ -14,7 +15,7 @@ import {
} from "react-icons/ri";
import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton";
-type TextAlignment = DefaultProps["textAlignment"]["values"][number];
+type TextAlignment = Exclude["textAlignment"], undefined>;
const icons: Record = {
left: RiAlignLeft,
@@ -32,14 +33,14 @@ export const TextAlignButton = (props: {
if (selection) {
for (const block of selection.blocks) {
- if (!("textAlignment" in block.props)) {
+ if (block.props && typeof block.props === 'object' && !("textAlignment" in block.props)) {
return false;
}
}
} else {
const block = props.editor.getTextCursorPosition().block;
- if (!("textAlignment" in block.props)) {
+ if (block.props && typeof block.props === 'object' && !("textAlignment" in block.props)) {
return false;
}
}
@@ -78,7 +79,7 @@ export const TextAlignButton = (props: {
setTextAlignment(props.textAlignment)}
isSelected={
- props.editor.getTextCursorPosition().block.props.textAlignment ===
+ (props.editor.getTextCursorPosition().block.props as any).textAlignment ===
props.textAlignment
}
mainTooltip={
diff --git a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
index b6ce8fe6f5..bf0799896d 100644
--- a/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
+++ b/packages/react/src/FormattingToolbar/components/DefaultDropdowns/BlockTypeDropdown.tsx
@@ -2,6 +2,7 @@ import {
BlockNoteEditor,
BlockSchema,
DefaultBlockSchema,
+ defaultBlockSchema,
} from "@blocknote/core";
import { useEffect, useState } from "react";
import { IconType } from "react-icons";
@@ -25,7 +26,7 @@ const headingIcons: Record = {
const shouldShow = (schema: BlockSchema) => {
const paragraph = "paragraph" in schema;
- const heading = "heading" in schema && "level" in schema.heading.propSchema;
+ const heading = "heading" in schema;
const bulletListItem = "bulletListItem" in schema;
const numberedListItem = "numberedListItem" in schema;
@@ -52,18 +53,24 @@ export const BlockTypeDropdown = (props: {
// the default block schema is being used
let editor = props.editor as any as BlockNoteEditor;
- const headingItems = editor.schema.heading.propSchema.level.values.map(
+ const parsedDefaultProps = defaultBlockSchema.heading.propSchema.safeParse(defaultBlockSchema.heading.props)
+
+ if (!parsedDefaultProps.success) {
+ throw new Error("Default heading values are not valid");
+ }
+
+ const headingItems = (['1', '2', '3'] as const).map(
(level) => ({
onClick: () => {
editor.focus();
editor.updateBlock(block, {
type: "heading",
- props: { level: level },
+ props: { ...parsedDefaultProps.data, level: level },
});
},
text: "Heading " + level,
icon: headingIcons[level],
- isSelected: block.type === "heading" && block.props.level === level,
+ isSelected: block.type === "heading" && (block.props as any).level === level,
})
);
diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
index 85b3650d0b..49772061a5 100644
--- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
+++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx
@@ -22,9 +22,9 @@ export const FormattingToolbar = (props: {
-
-
-
+
+
+
diff --git a/packages/react/src/ReactBlockSpec.tsx b/packages/react/src/ReactBlockSpec.tsx
index 7b8aa7884b..63fca6e704 100644
--- a/packages/react/src/ReactBlockSpec.tsx
+++ b/packages/react/src/ReactBlockSpec.tsx
@@ -88,8 +88,9 @@ export function createReactBlockSpec<
// Add props as HTML attributes in kebab-case with "data-" prefix
const htmlAttributes: Record = {};
+
for (const [attribute, value] of Object.entries(props.node.attrs)) {
- if (attribute in blockConfig.propSchema) {
+ if (attribute in blockConfig.propSchema.shape) {
htmlAttributes[camelToDataKebab(attribute)] = value;
}
}
diff --git a/tests/utils/customblocks/Alert.tsx b/tests/utils/customblocks/Alert.tsx
index 8df5df14f1..81baf48c3f 100644
--- a/tests/utils/customblocks/Alert.tsx
+++ b/tests/utils/customblocks/Alert.tsx
@@ -1,4 +1,6 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultProps, defaultPropSchema } from "@blocknote/core";
import { ReactSlashMenuItem } from "@blocknote/react";
import { RiAlertFill } from "react-icons/ri";
@@ -23,13 +25,15 @@ const values = {
export const Alert = createBlockSpec({
type: "alert" as const,
- propSchema: {
+ propSchema: z.object({
+ textAlignment: defaultPropSchema.shape.textAlignment,
+ textColor: defaultPropSchema.shape.textColor,
+ type: z.enum(["warning", "error", "info", "success"]),
+ }),
+ props: {
textAlignment: defaultProps.textAlignment,
textColor: defaultProps.textColor,
- type: {
- default: "warning",
- values: ["warning", "error", "info", "success"],
- },
+ type: "warning",
} as const,
containsInlineContent: true,
render: (block, editor) => {
diff --git a/tests/utils/customblocks/Button.tsx b/tests/utils/customblocks/Button.tsx
index 5e861e63ae..a5afe611f4 100644
--- a/tests/utils/customblocks/Button.tsx
+++ b/tests/utils/customblocks/Button.tsx
@@ -1,10 +1,15 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultProps, defaultPropSchema } from "@blocknote/core";
import { ReactSlashMenuItem } from "@blocknote/react";
import { RiRadioButtonFill } from "react-icons/ri";
export const Button = createBlockSpec({
type: "button" as const,
- propSchema: {
+ propSchema: z.object({
+ backgroundColor: defaultPropSchema.shape.backgroundColor,
+ }),
+ props: {
backgroundColor: defaultProps.backgroundColor,
} as const,
containsInlineContent: false,
@@ -15,7 +20,7 @@ export const Button = createBlockSpec({
editor.insertBlocks(
[
{
- type: "paragraph",
+ type: "button",
content: "Hello World",
},
],
diff --git a/tests/utils/customblocks/Embed.tsx b/tests/utils/customblocks/Embed.tsx
index 9fbd822012..5437873455 100644
--- a/tests/utils/customblocks/Embed.tsx
+++ b/tests/utils/customblocks/Embed.tsx
@@ -1,13 +1,16 @@
+import { z } from "zod";
+import React from "react";
import { createBlockSpec } from "@blocknote/core";
import { ReactSlashMenuItem } from "@blocknote/react";
import { RiLayout5Fill } from "react-icons/ri";
export const Embed = createBlockSpec({
type: "embed" as const,
- propSchema: {
- src: {
- default: "https://www.youtube.com/embed/wjfuB8Xjhc4",
- },
+ propSchema: z.object({
+ src: z.string().url(),
+ }),
+ props: {
+ src: "https://www.youtube.com/embed/wjfuB8Xjhc4"
} as const,
containsInlineContent: false,
render: (block) => {
diff --git a/tests/utils/customblocks/Image.tsx b/tests/utils/customblocks/Image.tsx
index 9b03dc0770..d7300d4410 100644
--- a/tests/utils/customblocks/Image.tsx
+++ b/tests/utils/customblocks/Image.tsx
@@ -1,14 +1,17 @@
-import { createBlockSpec, defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { createBlockSpec, defaultPropSchema, defaultProps } from "@blocknote/core";
import { ReactSlashMenuItem } from "@blocknote/react";
import { RiImage2Fill } from "react-icons/ri";
export const Image = createBlockSpec({
type: "image" as const,
- propSchema: {
+ propSchema: defaultPropSchema.merge(z.object({
+ src: z.string().url(),
+ })),
+ props: {
...defaultProps,
- src: {
- default: "https://via.placeholder.com/1000",
- },
+ src: "https://via.placeholder.com/1000"
} as const,
containsInlineContent: true,
render: (block) => {
diff --git a/tests/utils/customblocks/ReactAlert.tsx b/tests/utils/customblocks/ReactAlert.tsx
index e89b8586c4..fdc9078a25 100644
--- a/tests/utils/customblocks/ReactAlert.tsx
+++ b/tests/utils/customblocks/ReactAlert.tsx
@@ -1,4 +1,6 @@
-import { defaultProps } from "@blocknote/core";
+import { z } from "zod";
+import React from "react";
+import { defaultProps, defaultPropSchema } from "@blocknote/core";
import {
createReactBlockSpec,
InlineContent,
@@ -28,13 +30,13 @@ const values = {
export const ReactAlert = createReactBlockSpec({
type: "reactAlert" as const,
- propSchema: {
+ propSchema: defaultPropSchema.merge(z.object({
+ type: z.enum(["warning", "error", "info", "success"]),
+ })),
+ props: {
textAlignment: defaultProps.textAlignment,
textColor: defaultProps.textColor,
- type: {
- default: "warning",
- values: ["warning", "error", "info", "success"],
- },
+ type: "warning",
} as const,
containsInlineContent: true,
render: (props) => {
diff --git a/tests/utils/customblocks/ReactImage.tsx b/tests/utils/customblocks/ReactImage.tsx
index f410fbd13a..315a569b41 100644
--- a/tests/utils/customblocks/ReactImage.tsx
+++ b/tests/utils/customblocks/ReactImage.tsx
@@ -1,18 +1,21 @@
+import { z } from "zod";
+import React from "react";
import {
InlineContent,
createReactBlockSpec,
ReactSlashMenuItem,
} from "@blocknote/react";
-import { defaultProps } from "@blocknote/core";
+import { defaultProps, defaultPropSchema } from "@blocknote/core";
import { RiImage2Fill } from "react-icons/ri";
export const ReactImage = createReactBlockSpec({
type: "reactImage" as const,
- propSchema: {
+ propSchema: defaultPropSchema.merge(z.object({
+ src: z.string().url(),
+ })),
+ props: {
...defaultProps,
- src: {
- default: "https://via.placeholder.com/1000",
- },
+ src: "https://via.placeholder.com/1000"
} as const,
containsInlineContent: true,
render: ({ block }) => {
diff --git a/tests/utils/customblocks/Separator.tsx b/tests/utils/customblocks/Separator.tsx
index 39e84067d7..1b37afc8de 100644
--- a/tests/utils/customblocks/Separator.tsx
+++ b/tests/utils/customblocks/Separator.tsx
@@ -1,10 +1,13 @@
+import { z } from "zod";
+import React from "react";
import { createBlockSpec } from "@blocknote/core";
import { ReactSlashMenuItem } from "@blocknote/react";
import { RiSeparator } from "react-icons/ri";
export const Separator = createBlockSpec({
type: "separator" as const,
- propSchema: {} as const,
+ propSchema: z.object({}),
+ props: {} as const,
containsInlineContent: false,
render: () => {
const separator = document.createElement("div");
diff --git a/tests/utils/customblocks/TableOfContents.tsx b/tests/utils/customblocks/TableOfContents.tsx
index e29844d952..f98a97c857 100644
--- a/tests/utils/customblocks/TableOfContents.tsx
+++ b/tests/utils/customblocks/TableOfContents.tsx
@@ -1,3 +1,5 @@
+import { z } from "zod";
+import React from "react";
import {
Block,
BlockSchema,
@@ -43,7 +45,8 @@ function createHeadingElements(block: Block) {
export const TableOfContents = createBlockSpec({
type: "toc" as const,
- propSchema: {} as const,
+ propSchema: z.object({}),
+ props: {} as const,
containsInlineContent: false,
render: (_, editor) => {
const toc = document.createElement("ol");
@@ -51,7 +54,7 @@ export const TableOfContents = createBlockSpec({
editor.onEditorContentChange(() => {
toc.innerHTML = "";
for (const block of editor.topLevelBlocks) {
- if (block.type === "heading") {
+ if ((block.type as any) === "heading") {
toc.appendChild(createHeadingElements(block));
}
}
diff --git a/tests/utils/draghandle.ts b/tests/utils/draghandle.ts
index a576070b85..b3c4714d98 100644
--- a/tests/utils/draghandle.ts
+++ b/tests/utils/draghandle.ts
@@ -23,5 +23,5 @@ export async function getDragHandleYCoord(page: Page, selector: string) {
await moveMouseOverElement(page, element);
await page.waitForSelector(DRAG_HANDLE_SELECTOR);
const boundingBox = await page.locator(DRAG_HANDLE_SELECTOR).boundingBox();
- return boundingBox.y;
+ return boundingBox?.y;
}
diff --git a/tests/utils/mouse.ts b/tests/utils/mouse.ts
index 308b4f39b7..56d1ad9398 100644
--- a/tests/utils/mouse.ts
+++ b/tests/utils/mouse.ts
@@ -3,29 +3,29 @@ import { DRAG_HANDLE_SELECTOR } from "./const";
async function getElementLeftCoords(page: Page, element: Locator) {
const boundingBox = await element.boundingBox();
- const centerY = boundingBox.y + boundingBox.height / 2;
+ const centerY = boundingBox!.y + boundingBox!.height / 2;
- return { x: boundingBox.x + 1, y: centerY };
+ return { x: boundingBox!.x + 1, y: centerY };
}
async function getElementRightCoords(page: Page, element: Locator) {
const boundingBox = await element.boundingBox();
- const centerY = boundingBox.y + boundingBox.height / 2;
+ const centerY = boundingBox!.y + boundingBox!.height / 2;
- return { x: boundingBox.x + boundingBox.width - 1, y: centerY };
+ return { x: boundingBox!.x + boundingBox!.width - 1, y: centerY };
}
async function getElementCenterCoords(page: Page, element: Locator) {
const boundingBox = await element.boundingBox();
- const centerX = boundingBox.x + boundingBox.width / 2;
- const centerY = boundingBox.y + boundingBox.height / 2;
+ const centerX = boundingBox!.x + boundingBox!.width / 2;
+ const centerY = boundingBox!.y + boundingBox!.height / 2;
return { x: centerX, y: centerY };
}
export async function moveMouseOverElement(page: Page, element: Locator) {
const boundingBox = await element.boundingBox();
- const coords = { x: boundingBox.x, y: boundingBox.y };
+ const coords = { x: boundingBox!.x, y: boundingBox!.y };
await page.mouse.move(coords.x, coords.y, { steps: 5 });
}