From df515ebeb6932f2b914919a5b41673fa49c77cbc Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Thu, 29 Jun 2023 09:52:39 +0200 Subject: [PATCH 1/8] fix: enable more types of props in propSchema definition --- packages/core/src/extensions/Blocks/api/blockTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index ee49d9921..a49477ef7 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -34,8 +34,8 @@ export type TipTapNode< // 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; + values?: readonly unknown[]; + default: unknown; }; // Defines multiple block prop specs. The key of each prop is the name of the From babd1d49dd433f0ed9f996dbdf0a18f8e18304d2 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Thu, 29 Jun 2023 13:31:07 +0200 Subject: [PATCH 2/8] fix: enable autocomplete for props --- packages/core/src/extensions/Blocks/api/blockTypes.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index a49477ef7..f7934572b 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -46,12 +46,12 @@ export type PropSpec = { export type PropSchema = Record; // 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. +// 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 = { - [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly string[] + [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly unknown[] ? PSchema[PType]["values"][number] - : string; + : PSchema[PType]["default"]; }; // Defines the config for a single block. Meant to be used as an argument to @@ -84,7 +84,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 From 0fd0b9bbf62c0c44f3b336b33c3ed852c433d68a Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Thu, 29 Jun 2023 13:35:40 +0200 Subject: [PATCH 3/8] fix: update default props, test valid values, and editor type (addNodeView in createBlockSpec) --- .../formatConversions.test.ts | 96 +++++++++---------- .../core/src/extensions/Blocks/api/block.ts | 2 +- .../extensions/Blocks/api/defaultBlocks.ts | 2 + .../DefaultButtons/BlockColorsButton.tsx | 4 +- 4 files changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/core/src/api/formatConversions/formatConversions.test.ts b/packages/core/src/api/formatConversions/formatConversions.test.ts index 0d8e4054c..5da57ecec 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/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 77604299a..322dcb042 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -162,7 +162,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/defaultBlocks.ts b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts index d60d716cc..b6e941b15 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -7,9 +7,11 @@ import { PropSchema, TypesMatch } from "./blockTypes"; export const defaultProps = { backgroundColor: { default: "transparent" as const, + values: ["transparent", "red", "orange", "yellow", "blue"] as const, }, textColor: { default: "black" as const, // TODO + values: ["black", "red", "orange", "yellow"] as const, }, textAlignment: { default: "left" as const, diff --git a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx index 3fc435b27..bbf12c68d 100644 --- a/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx +++ b/packages/react/src/BlockSideMenu/components/DefaultButtons/BlockColorsButton.tsx @@ -56,8 +56,8 @@ export const BlockColorsButton = ( style={{ marginLeft: "5px" }}> props.editor.updateBlock(props.block, { props: { textColor: color }, From 22bf92ed9330ee8d423664e6e3587be6c44a90b0 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Wed, 5 Jul 2023 13:42:18 +0200 Subject: [PATCH 4/8] fix: render attributes only when the `default` value of the prop is a string --- .../core/src/extensions/Blocks/api/block.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 322dcb042..f8f7b7c6d 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -30,20 +30,22 @@ export function propsToAttributes< const tiptapAttributes: Record = {}; Object.entries(blockConfig.propSchema).forEach(([name, spec]) => { - tiptapAttributes[name] = { - default: spec.default, - 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], - } - : {}, - }; + if (typeof spec.default === 'string') { + tiptapAttributes[name] = { + default: spec.default, + 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[camelToDataKebab(name)] !== spec.default + ? { + [camelToDataKebab(name)]: attributes[name], + } + : {}, + }; + } }); return tiptapAttributes; From e26588fa3bcf50add01cd2a6fb1b00c3a48a6937 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Mon, 10 Jul 2023 10:15:59 +0200 Subject: [PATCH 5/8] fix: implement generic for PropSchema type --- packages/core/src/extensions/Blocks/api/blockTypes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index f7934572b..9bafbc54f 100644 --- a/packages/core/src/extensions/Blocks/api/blockTypes.ts +++ b/packages/core/src/extensions/Blocks/api/blockTypes.ts @@ -33,9 +33,9 @@ export type TipTapNode< // 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 unknown[]; - default: unknown; +export type PropSpec = { + default: T; + values?: readonly T[]; }; // Defines multiple block prop specs. The key of each prop is the name of the @@ -43,7 +43,7 @@ export type PropSpec = { // 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 = Record>; // 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 the type of the From 157acae20e24631fd2490371e87a55bd481da4f1 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Tue, 11 Jul 2023 09:11:50 +0200 Subject: [PATCH 6/8] wip: prop validation using zod --- examples/editor/package.json | 3 +- examples/editor/src/App.tsx | 66 +++++++++++++++++++ package-lock.json | 14 +++- packages/core/package.json | 3 +- .../blockManipulation/blockManipulation.ts | 1 + .../api/nodeConversions/nodeConversions.ts | 33 ++++++---- .../core/src/extensions/Blocks/api/block.ts | 13 ++-- .../src/extensions/Blocks/api/blockTypes.ts | 18 ++--- .../extensions/Blocks/api/defaultBlocks.ts | 44 +++++++------ .../extensions/Blocks/nodes/BlockContainer.ts | 4 +- .../DefaultButtons/BlockColorsButton.tsx | 6 +- .../DefaultButtons/TextAlignButton.tsx | 11 ++-- .../DefaultDropdowns/BlockTypeDropdown.tsx | 15 +++-- .../components/FormattingToolbar.tsx | 6 +- packages/react/src/ReactBlockSpec.tsx | 3 +- tests/utils/customblocks/Alert.tsx | 16 +++-- tests/utils/customblocks/Button.tsx | 11 +++- tests/utils/customblocks/Embed.tsx | 11 ++-- tests/utils/customblocks/Image.tsx | 13 ++-- tests/utils/customblocks/ReactAlert.tsx | 14 ++-- tests/utils/customblocks/ReactImage.tsx | 13 ++-- tests/utils/customblocks/Separator.tsx | 5 +- tests/utils/customblocks/TableOfContents.tsx | 7 +- tests/utils/draghandle.ts | 2 +- tests/utils/mouse.ts | 14 ++-- 25 files changed, 236 insertions(+), 110 deletions(-) diff --git a/examples/editor/package.json b/examples/editor/package.json index 1101a3d4e..791f58953 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 55ce63eb9..65e17c7f5 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -3,6 +3,64 @@ import "@blocknote/core/style.css"; import { BlockNoteView, useBlockNote } from "@blocknote/react"; import styles from "./App.module.css"; +import { DefaultBlockSchema, defaultBlockSchema } from '@blocknote/core'; + +import { z } from 'zod'; +import { createReactBlockSpec, ReactSlashMenuItem, defaultReactSlashMenuItems } from '@blocknote/react'; + +export const AccordionBlock = createReactBlockSpec({ + type: 'accordion', + propSchema: z.object({ + label: z.string().optional(), + autoLayout: z.object({ + enabled: z.boolean(), + }), + }), + render: ({ editor, block }) => { + console.log(block.props) + + return ( + <> +

Accordion

+ {block.props.autoLayout?.enabled ? ( +
+ asdfasdf +
) : <>} + + ); + }, + 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( + [ + { + type: 'accordion', + props: { + label: 'Accordion', + 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 +68,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 f66c08954..0e7e071cd 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 91c2b7fed..beb372216 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/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index f49729475..a61aa802f 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -19,6 +19,7 @@ export function insertBlocks( const nodesToInsert: Node[] = []; for (const blockSpec of blocksToInsert) { + console.log(blockSpec) nodesToInsert.push(blockToNode(blockSpec, editor.schema)); } diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index dc7f53c22..4d4374d6a 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") { @@ -160,6 +165,8 @@ export function blockToNode( contentNode = schema.nodes[type].create(block.props, nodes); } + console.log(contentNode) + const children: Node[] = []; if (block.children) { @@ -378,22 +385,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 f8f7b7c6d..0f15f725a 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -29,17 +29,22 @@ export function propsToAttributes< ) { const tiptapAttributes: Record = {}; - Object.entries(blockConfig.propSchema).forEach(([name, spec]) => { - if (typeof spec.default === 'string') { + Object.entries(blockConfig.propSchema.shape).forEach(([name, spec]) => { + // If the spec schema has a 'default' value, + // we use that as the default value for the attribute. + // https://zod.dev/?id=default + const parsedSpec = spec.safeParse(undefined); + + if (parsedSpec.success && typeof parsedSpec.data === "string") { tiptapAttributes[name] = { - default: spec.default, + default: parsedSpec.data, 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[camelToDataKebab(name)] !== spec.default + attributes[camelToDataKebab(name)] !== spec ? { [camelToDataKebab(name)]: attributes[name], } diff --git a/packages/core/src/extensions/Blocks/api/blockTypes.ts b/packages/core/src/extensions/Blocks/api/blockTypes.ts index 9bafbc54f..88a65ee1f 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 = { - default: T; - values?: readonly T[]; -}; - // 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 the type of the // 'default' property if values are not specified. -export type Props = { - [PType in keyof PSchema]: PSchema[PType]["values"] extends readonly unknown[] - ? PSchema[PType]["values"][number] - : PSchema[PType]["default"]; -}; +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 @@ -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 b6e941b15..7ab1790d5 100644 --- a/packages/core/src/extensions/Blocks/api/defaultBlocks.ts +++ b/packages/core/src/extensions/Blocks/api/defaultBlocks.ts @@ -1,46 +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, - values: ["transparent", "red", "orange", "yellow", "blue"] as const, - }, - textColor: { - default: "black" as const, // TODO - values: ["black", "red", "orange", "yellow"] as const, - }, - 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 3120ba825..d3fad13b7 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 bbf12c68d..4515fb4e8 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 ( diff --git a/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx b/packages/react/src/FormattingToolbar/components/DefaultButtons/TextAlignButton.tsx index dde4fe634..5f4cb1cd8 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 b6ce8fe6f..bf0799896 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 85b3650d0..49772061a 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 7b8aa7884..63fca6e70 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 8df5df14f..81baf48c3 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 5e861e63a..a5afe611f 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 9fbd82201..543787345 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 9b03dc077..d7300d441 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 e89b8586c..fdc9078a2 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 f410fbd13..315a569b4 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 39e84067d..1b37afc8d 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 e29844d95..f98a97c85 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 a576070b8..b3c4714d9 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 308b4f39b..56d1ad939 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 }); } From c871179234c646ed715b3dc6b800fa83c44aac90 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Tue, 11 Jul 2023 09:50:18 +0200 Subject: [PATCH 7/8] fix: attributes are not set to custom elements --- examples/editor/src/App.tsx | 35 ++++++++++--------- .../blockManipulation/blockManipulation.ts | 1 - .../api/nodeConversions/nodeConversions.ts | 2 -- .../core/src/extensions/Blocks/api/block.ts | 32 ++++++++--------- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 65e17c7f5..c4c11f162 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -1,31 +1,31 @@ // 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"; -import { DefaultBlockSchema, defaultBlockSchema } from '@blocknote/core'; - -import { z } from 'zod'; -import { createReactBlockSpec, ReactSlashMenuItem, defaultReactSlashMenuItems } from '@blocknote/react'; - export const AccordionBlock = createReactBlockSpec({ type: 'accordion', propSchema: z.object({ - label: z.string().optional(), + label: z.string(), autoLayout: z.object({ enabled: z.boolean(), - }), + }).optional(), }), render: ({ editor, block }) => { - console.log(block.props) - return ( <> -

Accordion

- {block.props.autoLayout?.enabled ? ( -
- asdfasdf -
) : <>} +

{block.props.label}

+ { + block.props.autoLayout?.enabled ? + ( +
+ Enabled +
+ ) : + <> + } ); }, @@ -40,12 +40,13 @@ export const insertAccordion = new ReactSlashMenuItem< (editor) => { editor.insertBlocks( [ + // Default values are set here { type: 'accordion', props: { - label: 'Accordion', + label: 'Default', autoLayout: { - enabled: true, + enabled: true } }, }, diff --git a/packages/core/src/api/blockManipulation/blockManipulation.ts b/packages/core/src/api/blockManipulation/blockManipulation.ts index a61aa802f..f49729475 100644 --- a/packages/core/src/api/blockManipulation/blockManipulation.ts +++ b/packages/core/src/api/blockManipulation/blockManipulation.ts @@ -19,7 +19,6 @@ export function insertBlocks( const nodesToInsert: Node[] = []; for (const blockSpec of blocksToInsert) { - console.log(blockSpec) nodesToInsert.push(blockToNode(blockSpec, editor.schema)); } diff --git a/packages/core/src/api/nodeConversions/nodeConversions.ts b/packages/core/src/api/nodeConversions/nodeConversions.ts index 4d4374d6a..3b07844d4 100644 --- a/packages/core/src/api/nodeConversions/nodeConversions.ts +++ b/packages/core/src/api/nodeConversions/nodeConversions.ts @@ -165,8 +165,6 @@ export function blockToNode( contentNode = schema.nodes[type].create(block.props, nodes); } - console.log(contentNode) - const children: Node[] = []; if (block.children) { diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 0f15f725a..3aa90c5d8 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -31,26 +31,24 @@ export function propsToAttributes< Object.entries(blockConfig.propSchema.shape).forEach(([name, spec]) => { // If the spec schema has a 'default' value, - // we use that as the default value for the attribute. + // we use it as the default value for the attribute. // https://zod.dev/?id=default const parsedSpec = spec.safeParse(undefined); - if (parsedSpec.success && typeof parsedSpec.data === "string") { - tiptapAttributes[name] = { - default: parsedSpec.data, - 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[camelToDataKebab(name)] !== spec - ? { - [camelToDataKebab(name)]: attributes[name], - } - : {}, - }; - } + tiptapAttributes[name] = { + default: parsedSpec.success && typeof parsedSpec.data === "string" ? parsedSpec.data : '', + 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[camelToDataKebab(name)] !== spec + ? { + [camelToDataKebab(name)]: attributes[name], + } + : {}, + }; }); return tiptapAttributes; From aa2c5105373476466db20e41a7c5d7319e527704 Mon Sep 17 00:00:00 2001 From: Daryl Castro Date: Tue, 11 Jul 2023 10:11:18 +0200 Subject: [PATCH 8/8] wip: refinements --- .../core/src/extensions/Blocks/api/block.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/block.ts b/packages/core/src/extensions/Blocks/api/block.ts index 3aa90c5d8..b27f2dfc7 100644 --- a/packages/core/src/extensions/Blocks/api/block.ts +++ b/packages/core/src/extensions/Blocks/api/block.ts @@ -30,24 +30,24 @@ export function propsToAttributes< const tiptapAttributes: Record = {}; Object.entries(blockConfig.propSchema.shape).forEach(([name, spec]) => { - // If the spec schema has a 'default' value, - // we use it as the default value for the attribute. - // https://zod.dev/?id=default - const parsedSpec = spec.safeParse(undefined); - tiptapAttributes[name] = { - default: parsedSpec.success && typeof parsedSpec.data === "string" ? parsedSpec.data : '', + 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[camelToDataKebab(name)] !== spec - ? { - [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 {} + }, }; });