From d1de559d7337e1d75591840874fecbf454b8ce71 Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Tue, 28 Nov 2023 23:28:15 +0100 Subject: [PATCH 1/4] Fixed commands and internal copy/paste for inline content --- .../Blocks/api/inlineContent/createSpec.ts | 24 ++++- .../Blocks/api/inlineContent/internal.ts | 35 +++++++- .../extensions/Blocks/nodes/BlockContainer.ts | 28 +++--- packages/react/src/ReactInlineContentSpec.tsx | 89 +++++++++++++++---- 4 files changed, 139 insertions(+), 37 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 124923268a..9f9bf80256 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -1,8 +1,12 @@ import { Node } from "@tiptap/core"; import { nodeToCustomInlineContent } from "../../../../api/nodeConversions/nodeConversions"; import { propsToAttributes } from "../blocks/internal"; +import { Props } from "../blocks/types"; import { StyleSchema } from "../styles/types"; -import { createInlineContentSpecFromTipTapNode } from "./internal"; +import { + addInlineContentAttributes, + createInlineContentSpecFromTipTapNode, +} from "./internal"; import { InlineContentConfig, InlineContentFromConfig, @@ -57,6 +61,14 @@ export function createInlineContentSpec< return propsToAttributes(inlineContentConfig.propSchema); }, + parseHTML() { + return [ + { + tag: `.bn-inline-content-section[data-inline-content-type="${inlineContentConfig.type}"]`, + }, + ]; + }, + renderHTML({ node }) { const editor = this.options.editor; @@ -68,7 +80,15 @@ export function createInlineContentSpec< ) as any as InlineContentFromConfig<T, S> // TODO: fix cast ); - return output; + return { + dom: addInlineContentAttributes( + output.dom, + inlineContentConfig.type, + node.attrs as Props<T["propSchema"]>, + inlineContentConfig.propSchema + ), + contentDOM: output.contentDOM, + }; }, }); diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts index 9c623c44cf..38e99f713e 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts @@ -1,5 +1,6 @@ import { Node } from "@tiptap/core"; -import { PropSchema } from "../blocks/types"; +import { camelToDataKebab } from "../blocks/internal"; +import { Props, PropSchema } from "../blocks/types"; import { InlineContentConfig, InlineContentImplementation, @@ -7,6 +8,38 @@ import { InlineContentSpec, InlineContentSpecs, } from "./types"; +import { mergeCSSClasses } from "../../../../shared/utils"; + +// Function that adds necessary classes and attributes to the `dom` element +// returned from a custom inline content's 'render' function, to ensure no data +// is lost on copy & paste. +export function addInlineContentAttributes< + IType extends string, + PSchema extends PropSchema +>( + element: HTMLElement, + inlineContentType: IType, + inlineContentProps: Props<PSchema>, + propSchema: PSchema +): HTMLElement { + // Sets inline content section class + element.className = mergeCSSClasses( + "bn-inline-content-section", + element.className + ); + // Sets content type attribute + element.setAttribute("data-inline-content-type", inlineContentType); + // Adds props as HTML attributes in kebab-case with "data-" prefix. Skips props + // set to their default values. + Object.entries(inlineContentProps) + .filter(([prop, value]) => value !== propSchema[prop].default) + .map(([prop, value]) => { + return [camelToDataKebab(prop), value]; + }) + .forEach(([prop, value]) => element.setAttribute(prop, value)); + + return element; +} // This helper function helps to instantiate a InlineContentSpec with a // config and implementation that conform to the type of Config diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts index 443cc7d7fd..bba83b4308 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContainer.ts @@ -483,13 +483,12 @@ export const BlockContainer = Node.create<{ // Reverts block content type to a paragraph if the selection is at the start of the block. () => commands.command(({ state }) => { - const { contentType } = getBlockInfoFromPos( + const { contentType, startPos } = getBlockInfoFromPos( state.doc, state.selection.from )!; - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; + const selectionAtBlockStart = state.selection.from === startPos + 1; const isParagraph = contentType.name === "paragraph"; if (selectionAtBlockStart && !isParagraph) { @@ -504,8 +503,12 @@ export const BlockContainer = Node.create<{ // Removes a level of nesting if the block is indented if the selection is at the start of the block. () => commands.command(({ state }) => { - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; + const { startPos } = getBlockInfoFromPos( + state.doc, + state.selection.from + )!; + + const selectionAtBlockStart = state.selection.from === startPos + 1; if (selectionAtBlockStart) { return commands.liftListItem("blockContainer"); @@ -522,10 +525,8 @@ export const BlockContainer = Node.create<{ state.selection.from )!; - const selectionAtBlockStart = - state.selection.$anchor.parentOffset === 0; - const selectionEmpty = - state.selection.anchor === state.selection.head; + const selectionAtBlockStart = state.selection.from === startPos + 1; + const selectionEmpty = state.selection.empty; const blockAtDocStart = startPos === 2; const posBetweenBlocks = startPos - 1; @@ -552,17 +553,14 @@ export const BlockContainer = Node.create<{ // end of the block. () => commands.command(({ state }) => { - const { node, contentNode, depth, endPos } = getBlockInfoFromPos( + const { node, depth, endPos } = getBlockInfoFromPos( state.doc, state.selection.from )!; const blockAtDocEnd = false; - const selectionAtBlockEnd = - state.selection.$anchor.parentOffset === - contentNode.firstChild!.nodeSize; - const selectionEmpty = - state.selection.anchor === state.selection.head; + const selectionAtBlockEnd = state.selection.from === endPos - 1; + const selectionEmpty = state.selection.empty; const hasChildBlocks = node.childCount === 2; if ( diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 70f2db7c7d..2bf31e234f 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -1,9 +1,12 @@ import { + camelToDataKebab, createInternalInlineContentSpec, createStronglyTypedTiptapNode, InlineContentConfig, InlineContentFromConfig, nodeToCustomInlineContent, + Props, + PropSchema, propsToAttributes, StyleSchema, } from "@blocknote/core"; @@ -36,6 +39,40 @@ export type ReactInlineContentImplementation< // }>; }; +// Function that wraps the React component returned from 'blockConfig.render' in +// a `NodeViewWrapper` which also acts as a `blockContent` div. It contains the +// block type and props as HTML attributes. +export function reactWrapInInlineContentStructure< + BType extends string, + PSchema extends PropSchema +>( + element: JSX.Element, + inlineContentType: BType, + inlineContentProps: Props<PSchema>, + propSchema: PSchema +) { + return () => ( + // Creates inline content section element + <NodeViewWrapper + as={"span"} + // Sets inline content section class + className={"bn-inline-content-section"} + // Sets content type attribute + data-inline-content-type={inlineContentType} + // Adds props as HTML attributes in kebab-case with "data-" prefix. Skips + // props set to their default values. + {...Object.fromEntries( + Object.entries(inlineContentProps) + .filter(([prop, value]) => value !== propSchema[prop].default) + .map(([prop, value]) => { + return [camelToDataKebab(prop), value]; + }) + )}> + {element} + </NodeViewWrapper> + ); +} + // A function to create custom block for API consumers // we want to hide the tiptap node from API consumers and provide a simpler API surface instead export function createReactInlineContentSpec< @@ -50,6 +87,8 @@ export function createReactInlineContentSpec< name: inlineContentConfig.type as T["type"], inline: true, group: "inline", + selectable: inlineContentConfig.content === "styled", + atom: inlineContentConfig.content === "none", content: (inlineContentConfig.content === "styled" ? "inline*" : "") as T["content"] extends "styled" ? "inline*" : "", @@ -58,9 +97,13 @@ export function createReactInlineContentSpec< return propsToAttributes(inlineContentConfig.propSchema); }, - // parseHTML() { - // return parse(blockConfig); - // }, + parseHTML() { + return [ + { + tag: `.bn-inline-content-section[data-inline-content-type="${inlineContentConfig.type}"]`, + }, + ]; + }, renderHTML({ node }) { const editor = this.options.editor; @@ -72,9 +115,15 @@ export function createReactInlineContentSpec< ) as any as InlineContentFromConfig<T, S>; // TODO: fix cast const Content = inlineContentImplementation.render; - return renderToDOMSpec((refCB) => ( - <Content inlineContent={ic} contentRef={refCB} /> - )); + return renderToDOMSpec((refCB) => { + const FullContent = reactWrapInInlineContentStructure( + <Content inlineContent={ic} contentRef={refCB} />, + inlineContentConfig.type, + node.attrs as Props<T["propSchema"]>, + inlineContentConfig.propSchema + ); + return <FullContent />; + }); }, // TODO: needed? @@ -88,20 +137,22 @@ export function createReactInlineContentSpec< const ref = (NodeViewContent({}) as any).ref; const Content = inlineContentImplementation.render; - return ( - <NodeViewWrapper as="span"> - <Content - contentRef={ref} - inlineContent={ - nodeToCustomInlineContent( - props.node, - editor.inlineContentSchema, - editor.styleSchema - ) as any as InlineContentFromConfig<T, S> // TODO: fix cast - } - /> - </NodeViewWrapper> + const FullContent = reactWrapInInlineContentStructure( + <Content + contentRef={ref} + inlineContent={ + nodeToCustomInlineContent( + props.node, + editor.inlineContentSchema, + editor.styleSchema + ) as any as InlineContentFromConfig<T, S> // TODO: fix cast + } + />, + inlineContentConfig.type, + props.node.attrs as Props<T["propSchema"]>, + inlineContentConfig.propSchema ); + return <FullContent />; }, { className: "bn-ic-react-node-view-renderer", From bea2771694aa4e021c2b4cde084597e8c7a4280b Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Wed, 29 Nov 2023 00:00:46 +0100 Subject: [PATCH 2/4] Fixed internal copy/paste for styles --- .../Blocks/api/styles/createSpec.ts | 34 +++++++++++---- .../extensions/Blocks/api/styles/internal.ts | 26 ++++++++++++ packages/react/src/ReactInlineContentSpec.tsx | 26 +++++++----- packages/react/src/ReactStyleSpec.tsx | 41 ++++++++++++++----- 4 files changed, 97 insertions(+), 30 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts index 9f0d742f75..607be9fdde 100644 --- a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -1,6 +1,6 @@ import { Mark } from "@tiptap/core"; import { UnreachableCaseError } from "../../../../shared/utils"; -import { createInternalStyleSpec } from "./internal"; +import { addStyleAttributes, createInternalStyleSpec } from "./internal"; import { StyleConfig, StyleSpec } from "./types"; export type CustomStyleImplementation<T extends StyleConfig> = { @@ -31,17 +31,25 @@ export function createStyleSpec<T extends StyleConfig>( return { stringValue: { default: undefined, - // TODO: parsing - - // parseHTML: (element) => - // element.getAttribute(`data-${styleConfig.type}`), - // renderHTML: (attributes) => ({ - // [`data-${styleConfig.type}`]: attributes.stringValue, - // }), + parseHTML: (element) => element.getAttribute("data-value"), + renderHTML: (attributes) => + attributes.stringValue !== undefined + ? { + "data-value": attributes.stringValue, + } + : {}, }, }; }, + parseHTML() { + return [ + { + tag: `.bn-style[data-style-type="${styleConfig.type}"]`, + }, + ]; + }, + renderHTML({ mark }) { let renderResult: { dom: HTMLElement; @@ -58,7 +66,15 @@ export function createStyleSpec<T extends StyleConfig>( } // const renderResult = styleImplementation.render(); - return renderResult; + return { + dom: addStyleAttributes( + renderResult.dom, + styleConfig.type, + mark.attrs.stringValue, + styleConfig.propSchema + ), + contentDOM: renderResult.contentDOM, + }; }, }); diff --git a/packages/core/src/extensions/Blocks/api/styles/internal.ts b/packages/core/src/extensions/Blocks/api/styles/internal.ts index 648bb133d5..f52b04e037 100644 --- a/packages/core/src/extensions/Blocks/api/styles/internal.ts +++ b/packages/core/src/extensions/Blocks/api/styles/internal.ts @@ -7,6 +7,32 @@ import { StyleSpec, StyleSpecs, } from "./types"; +import { mergeCSSClasses } from "../../../../shared/utils"; + +// Function that adds necessary classes and attributes to the `dom` element +// returned from a custom style's 'render' function, to ensure no data is lost +// on internal copy & paste. +export function addStyleAttributes< + SType extends string, + PSchema extends StylePropSchema +>( + element: HTMLElement, + styleType: SType, + styleValue: PSchema extends "boolean" ? undefined : string, + propSchema: PSchema +): HTMLElement { + // Sets inline content section class + element.className = mergeCSSClasses("bn-style", element.className); + // Sets content type attribute + element.setAttribute("data-style-type", styleType); + // Adds style value as an HTML attribute in kebab-case with "data-" prefix, if + // the style takes a string value. + if (propSchema === "string") { + element.setAttribute("data-value", styleValue as string); + } + + return element; +} // This helper function helps to instantiate a stylespec with a // config and implementation that conform to the type of Config diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 2bf31e234f..22018eaba1 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -1,4 +1,5 @@ import { + addInlineContentAttributes, camelToDataKebab, createInternalInlineContentSpec, createStronglyTypedTiptapNode, @@ -39,15 +40,15 @@ export type ReactInlineContentImplementation< // }>; }; -// Function that wraps the React component returned from 'blockConfig.render' in -// a `NodeViewWrapper` which also acts as a `blockContent` div. It contains the -// block type and props as HTML attributes. +// Function that adds a wrapper with necessary classes and attributes to the +// component returned from a custom inline content's 'render' function, to +// ensure no data is lost on internal copy & paste. export function reactWrapInInlineContentStructure< - BType extends string, + IType extends string, PSchema extends PropSchema >( element: JSX.Element, - inlineContentType: BType, + inlineContentType: IType, inlineContentProps: Props<PSchema>, propSchema: PSchema ) { @@ -114,16 +115,19 @@ export function createReactInlineContentSpec< editor.styleSchema ) as any as InlineContentFromConfig<T, S>; // TODO: fix cast const Content = inlineContentImplementation.render; + const output = renderToDOMSpec((refCB) => ( + <Content inlineContent={ic} contentRef={refCB} /> + )); - return renderToDOMSpec((refCB) => { - const FullContent = reactWrapInInlineContentStructure( - <Content inlineContent={ic} contentRef={refCB} />, + return { + dom: addInlineContentAttributes( + output.dom, inlineContentConfig.type, node.attrs as Props<T["propSchema"]>, inlineContentConfig.propSchema - ); - return <FullContent />; - }); + ), + contentDOM: output.contentDOM, + }; }, // TODO: needed? diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index 645a519e48..0084bd0140 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -1,4 +1,8 @@ -import { createInternalStyleSpec, StyleConfig } from "@blocknote/core"; +import { + addStyleAttributes, + createInternalStyleSpec, + StyleConfig, +} from "@blocknote/core"; import { Mark } from "@tiptap/react"; import { FC } from "react"; import { renderToDOMSpec } from "./ReactRenderUtil"; @@ -28,17 +32,25 @@ export function createReactStyleSpec<T extends StyleConfig>( return { stringValue: { default: undefined, - // TODO: parsing - - // parseHTML: (element) => - // element.getAttribute(`data-${styleConfig.type}`), - // renderHTML: (attributes) => ({ - // [`data-${styleConfig.type}`]: attributes.stringValue, - // }), + parseHTML: (element) => element.getAttribute("data-value"), + renderHTML: (attributes) => + attributes.stringValue !== undefined + ? { + "data-value": attributes.stringValue, + } + : {}, }, }; }, + parseHTML() { + return [ + { + tag: `.bn-style[data-style-type="${styleConfig.type}"]`, + }, + ]; + }, + renderHTML({ mark }) { const props: any = {}; @@ -47,10 +59,19 @@ export function createReactStyleSpec<T extends StyleConfig>( } const Content = styleImplementation.render; - - return renderToDOMSpec((refCB) => ( + const renderResult = renderToDOMSpec((refCB) => ( <Content {...props} contentRef={refCB} /> )); + + return { + dom: addStyleAttributes( + renderResult.dom, + styleConfig.type, + mark.attrs.stringValue, + styleConfig.propSchema + ), + contentDOM: renderResult.contentDOM, + }; }, }); From a8f08eec903d68404d8d11e549bfdb2016cbc9f4 Mon Sep 17 00:00:00 2001 From: Matthew Lipski <matthewlipski@gmail.com> Date: Wed, 29 Nov 2023 00:10:44 +0100 Subject: [PATCH 3/4] Small cleanup --- .../Blocks/api/inlineContent/createSpec.ts | 17 ++++++--- .../Blocks/api/inlineContent/internal.ts | 2 +- .../Blocks/api/styles/createSpec.ts | 37 ++++++++----------- .../extensions/Blocks/api/styles/internal.ts | 23 +++++++++++- packages/react/src/ReactInlineContentSpec.tsx | 7 +--- packages/react/src/ReactStyleSpec.tsx | 24 ++---------- 6 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts index 9f9bf80256..534ce36bf2 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/createSpec.ts @@ -1,4 +1,5 @@ import { Node } from "@tiptap/core"; +import { ParseRule } from "@tiptap/pm/model"; import { nodeToCustomInlineContent } from "../../../../api/nodeConversions/nodeConversions"; import { propsToAttributes } from "../blocks/internal"; import { Props } from "../blocks/types"; @@ -41,6 +42,16 @@ export type CustomInlineContentImplementation< }; }; +export function getInlineContentParseRules( + config: InlineContentConfig +): ParseRule[] { + return [ + { + tag: `.bn-inline-content-section[data-inline-content-type="${config.type}"]`, + }, + ]; +} + export function createInlineContentSpec< T extends InlineContentConfig, S extends StyleSchema @@ -62,11 +73,7 @@ export function createInlineContentSpec< }, parseHTML() { - return [ - { - tag: `.bn-inline-content-section[data-inline-content-type="${inlineContentConfig.type}"]`, - }, - ]; + return getInlineContentParseRules(inlineContentConfig); }, renderHTML({ node }) { diff --git a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts index 38e99f713e..d081338be8 100644 --- a/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts +++ b/packages/core/src/extensions/Blocks/api/inlineContent/internal.ts @@ -12,7 +12,7 @@ import { mergeCSSClasses } from "../../../../shared/utils"; // Function that adds necessary classes and attributes to the `dom` element // returned from a custom inline content's 'render' function, to ensure no data -// is lost on copy & paste. +// is lost on internal copy & paste. export function addInlineContentAttributes< IType extends string, PSchema extends PropSchema diff --git a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts index 607be9fdde..14c1c2274f 100644 --- a/packages/core/src/extensions/Blocks/api/styles/createSpec.ts +++ b/packages/core/src/extensions/Blocks/api/styles/createSpec.ts @@ -1,6 +1,11 @@ import { Mark } from "@tiptap/core"; +import { ParseRule } from "@tiptap/pm/model"; import { UnreachableCaseError } from "../../../../shared/utils"; -import { addStyleAttributes, createInternalStyleSpec } from "./internal"; +import { + addStyleAttributes, + createInternalStyleSpec, + stylePropsToAttributes, +} from "./internal"; import { StyleConfig, StyleSpec } from "./types"; export type CustomStyleImplementation<T extends StyleConfig> = { @@ -17,6 +22,14 @@ export type CustomStyleImplementation<T extends StyleConfig> = { // TODO: support serialization +export function getStyleParseRules(config: StyleConfig): ParseRule[] { + return [ + { + tag: `.bn-style[data-style-type="${config.type}"]`, + }, + ]; +} + export function createStyleSpec<T extends StyleConfig>( styleConfig: T, styleImplementation: CustomStyleImplementation<T> @@ -25,29 +38,11 @@ export function createStyleSpec<T extends StyleConfig>( name: styleConfig.type, addAttributes() { - if (styleConfig.propSchema === "boolean") { - return {}; - } - return { - stringValue: { - default: undefined, - parseHTML: (element) => element.getAttribute("data-value"), - renderHTML: (attributes) => - attributes.stringValue !== undefined - ? { - "data-value": attributes.stringValue, - } - : {}, - }, - }; + return stylePropsToAttributes(styleConfig.propSchema); }, parseHTML() { - return [ - { - tag: `.bn-style[data-style-type="${styleConfig.type}"]`, - }, - ]; + return getStyleParseRules(styleConfig); }, renderHTML({ mark }) { diff --git a/packages/core/src/extensions/Blocks/api/styles/internal.ts b/packages/core/src/extensions/Blocks/api/styles/internal.ts index f52b04e037..27b32a3f7a 100644 --- a/packages/core/src/extensions/Blocks/api/styles/internal.ts +++ b/packages/core/src/extensions/Blocks/api/styles/internal.ts @@ -1,4 +1,4 @@ -import { Mark } from "@tiptap/core"; +import { Attributes, Mark } from "@tiptap/core"; import { StyleConfig, StyleImplementation, @@ -9,6 +9,27 @@ import { } from "./types"; import { mergeCSSClasses } from "../../../../shared/utils"; +export function stylePropsToAttributes( + propSchema: StylePropSchema +): Attributes { + if (propSchema === "boolean") { + return {}; + } + return { + stringValue: { + default: undefined, + keepOnSplit: true, + parseHTML: (element) => element.getAttribute("data-value"), + renderHTML: (attributes) => + attributes.stringValue !== undefined + ? { + "data-value": attributes.stringValue, + } + : {}, + }, + }; +} + // Function that adds necessary classes and attributes to the `dom` element // returned from a custom style's 'render' function, to ensure no data is lost // on internal copy & paste. diff --git a/packages/react/src/ReactInlineContentSpec.tsx b/packages/react/src/ReactInlineContentSpec.tsx index 22018eaba1..adf51e09a2 100644 --- a/packages/react/src/ReactInlineContentSpec.tsx +++ b/packages/react/src/ReactInlineContentSpec.tsx @@ -3,6 +3,7 @@ import { camelToDataKebab, createInternalInlineContentSpec, createStronglyTypedTiptapNode, + getInlineContentParseRules, InlineContentConfig, InlineContentFromConfig, nodeToCustomInlineContent, @@ -99,11 +100,7 @@ export function createReactInlineContentSpec< }, parseHTML() { - return [ - { - tag: `.bn-inline-content-section[data-inline-content-type="${inlineContentConfig.type}"]`, - }, - ]; + return getInlineContentParseRules(inlineContentConfig); }, renderHTML({ node }) { diff --git a/packages/react/src/ReactStyleSpec.tsx b/packages/react/src/ReactStyleSpec.tsx index 0084bd0140..cb401850b7 100644 --- a/packages/react/src/ReactStyleSpec.tsx +++ b/packages/react/src/ReactStyleSpec.tsx @@ -1,7 +1,9 @@ import { addStyleAttributes, createInternalStyleSpec, + getStyleParseRules, StyleConfig, + stylePropsToAttributes, } from "@blocknote/core"; import { Mark } from "@tiptap/react"; import { FC } from "react"; @@ -26,29 +28,11 @@ export function createReactStyleSpec<T extends StyleConfig>( name: styleConfig.type, addAttributes() { - if (styleConfig.propSchema === "boolean") { - return {}; - } - return { - stringValue: { - default: undefined, - parseHTML: (element) => element.getAttribute("data-value"), - renderHTML: (attributes) => - attributes.stringValue !== undefined - ? { - "data-value": attributes.stringValue, - } - : {}, - }, - }; + return stylePropsToAttributes(styleConfig.propSchema); }, parseHTML() { - return [ - { - tag: `.bn-style[data-style-type="${styleConfig.type}"]`, - }, - ]; + return getStyleParseRules(styleConfig); }, renderHTML({ mark }) { From 63caf0774c8ae7a1386194733bdd8967bdc123ae Mon Sep 17 00:00:00 2001 From: yousefed <yousefdardiry@gmail.com> Date: Wed, 29 Nov 2023 09:54:16 +0100 Subject: [PATCH 4/4] fix some tests --- .../fontSize/basic/external.html | 2 +- .../fontSize/basic/internal.html | 2 +- .../__snapshots__/mention/basic/external.html | 2 +- .../__snapshots__/mention/basic/internal.html | 2 +- .../__snapshots__/small/basic/external.html | 2 +- .../__snapshots__/small/basic/internal.html | 2 +- .../__snapshots__/tag/basic/external.html | 2 +- .../__snapshots__/tag/basic/internal.html | 2 +- .../api/exporters/html/htmlConversion.test.ts | 3 --- .../testCases/cases/customInlineContent.ts | 4 +-- .../src/api/testCases/cases/defaultSchema.ts | 26 +++++++++---------- .../ParagraphBlockContent.ts | 1 + .../fontSize/basic/external.html | 2 +- .../fontSize/basic/internal.html | 2 +- .../__snapshots__/mention/basic/external.html | 2 +- .../__snapshots__/mention/basic/internal.html | 2 +- .../__snapshots__/small/basic/external.html | 2 +- .../__snapshots__/small/basic/internal.html | 2 +- .../__snapshots__/tag/basic/external.html | 2 +- .../__snapshots__/tag/basic/internal.html | 2 +- 20 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html index 4c7e8f174d..49b9ce6858 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content"><span style="font-size: 18px">This is text with a custom fontSize</span></p> \ No newline at end of file +<p class="bn-inline-content"><span style="font-size: 18px" class="bn-style" data-style-type="fontSize" data-value="18px">This is text with a custom fontSize</span></p> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html index 3e2beaedd6..3fe864246c 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/fontSize/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><span style="font-size: 18px">This is text with a custom fontSize</span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><span style="font-size: 18px" class="bn-style" data-style-type="fontSize" data-value="18px">This is text with a custom fontSize</span></p></div></div></div></div> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html index e1513fed2d..2e6f533ca1 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content">I enjoy working with<span>@Matthew</span></p> \ No newline at end of file +<p class="bn-inline-content">I enjoy working with<span class="bn-inline-content-section" data-inline-content-type="mention" data-user="Matthew">@Matthew</span></p> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html index 7af6dad9c7..6ca7d81c2c 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/mention/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I enjoy working with<span>@Matthew</span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I enjoy working with<span class="bn-inline-content-section" data-inline-content-type="mention" data-user="Matthew">@Matthew</span></p></div></div></div></div> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html index 4206d07a95..35c3d5c232 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/small/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content"><small>This is a small text</small></p> \ No newline at end of file +<p class="bn-inline-content"><small class="bn-style" data-style-type="small">This is a small text</small></p> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html index 805c78112e..73836f647d 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/small/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><small>This is a small text</small></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><small class="bn-style" data-style-type="small">This is a small text</small></p></div></div></div></div> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html index 4229ae0a83..b8387e9a55 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html +++ b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content">I love <span>#<span>BlockNote</span></span></p> \ No newline at end of file +<p class="bn-inline-content">I love <span class="bn-inline-content-section" data-inline-content-type="tag">#<span>BlockNote</span></span></p> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html index dac5db0ca8..bac28633b0 100644 --- a/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html +++ b/packages/core/src/api/exporters/html/__snapshots__/tag/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I love <span>#<span>BlockNote</span></span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I love <span class="bn-inline-content-section" data-inline-content-type="tag">#<span>BlockNote</span></span></p></div></div></div></div> \ No newline at end of file diff --git a/packages/core/src/api/exporters/html/htmlConversion.test.ts b/packages/core/src/api/exporters/html/htmlConversion.test.ts index 9f52f2d558..f6592f1bb7 100644 --- a/packages/core/src/api/exporters/html/htmlConversion.test.ts +++ b/packages/core/src/api/exporters/html/htmlConversion.test.ts @@ -369,9 +369,6 @@ describe("Test HTML conversion", () => { for (const document of testCase.documents) { // eslint-disable-next-line no-loop-func it("Convert " + document.name + " to HTML", async () => { - if (document.name !== "complex/misc") { - return; - } const nameSplit = document.name.split("/"); await convertToHTMLAndCompareSnapshots( editor, diff --git a/packages/core/src/api/testCases/cases/customInlineContent.ts b/packages/core/src/api/testCases/cases/customInlineContent.ts index 8ad1828152..304df912cb 100644 --- a/packages/core/src/api/testCases/cases/customInlineContent.ts +++ b/packages/core/src/api/testCases/cases/customInlineContent.ts @@ -87,7 +87,7 @@ export const customInlineContentTestCases: EditorTestCases< user: "Matthew", }, content: undefined, - } as any, + } as any, // TODO ], }, ], @@ -103,7 +103,7 @@ export const customInlineContentTestCases: EditorTestCases< type: "tag", // props: {}, content: "BlockNote", - } as any, + } as any, // TODO ], }, ], diff --git a/packages/core/src/api/testCases/cases/defaultSchema.ts b/packages/core/src/api/testCases/cases/defaultSchema.ts index bb7ccf4526..87aa6b01b1 100644 --- a/packages/core/src/api/testCases/cases/defaultSchema.ts +++ b/packages/core/src/api/testCases/cases/defaultSchema.ts @@ -24,7 +24,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/empty", blocks: [ { - type: "paragraph" as const, + type: "paragraph", }, ], }, @@ -32,7 +32,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/basic", blocks: [ { - type: "paragraph" as const, + type: "paragraph", content: "Paragraph", }, ], @@ -41,12 +41,12 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/styled", blocks: [ { - type: "paragraph" as const, + type: "paragraph", props: { textAlignment: "center", textColor: "orange", backgroundColor: "pink", - } as const, + }, content: [ { type: "text", @@ -83,15 +83,15 @@ export const defaultSchemaTestCases: EditorTestCases< name: "paragraph/nested", blocks: [ { - type: "paragraph" as const, + type: "paragraph", content: "Paragraph", children: [ { - type: "paragraph" as const, + type: "paragraph", content: "Nested Paragraph 1", }, { - type: "paragraph" as const, + type: "paragraph", content: "Nested Paragraph 2", }, ], @@ -102,7 +102,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/button", blocks: [ { - type: "image" as const, + type: "image", }, ], }, @@ -110,7 +110,7 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/basic", blocks: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", @@ -123,20 +123,20 @@ export const defaultSchemaTestCases: EditorTestCases< name: "image/nested", blocks: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", width: 256, - } as const, + }, children: [ { - type: "image" as const, + type: "image", props: { url: "exampleURL", caption: "Caption", width: 256, - } as const, + }, }, ], }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts index a645ba347c..8c826f413e 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts @@ -15,6 +15,7 @@ export const ParagraphBlockContent = createStronglyTypedTiptapNode({ group: "blockContent", parseHTML() { return [ + { tag: "div[data-content-type=" + this.name + "]" }, { tag: "p", priority: 200, diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/external.html b/packages/react/src/test/__snapshots__/fontSize/basic/external.html index 00a5bc6b6e..6c8910692f 100644 --- a/packages/react/src/test/__snapshots__/fontSize/basic/external.html +++ b/packages/react/src/test/__snapshots__/fontSize/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content"><span style="font-size: 18px;">This is text with a custom fontSize</span></p> \ No newline at end of file +<p class="bn-inline-content"><span style="font-size: 18px;" class="bn-style" data-style-type="fontSize" data-value="18px">This is text with a custom fontSize</span></p> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/fontSize/basic/internal.html b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html index a41d39869a..998d9bcf8b 100644 --- a/packages/react/src/test/__snapshots__/fontSize/basic/internal.html +++ b/packages/react/src/test/__snapshots__/fontSize/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><span style="font-size: 18px;">This is text with a custom fontSize</span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><span style="font-size: 18px;" class="bn-style" data-style-type="fontSize" data-value="18px">This is text with a custom fontSize</span></p></div></div></div></div> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/mention/basic/external.html b/packages/react/src/test/__snapshots__/mention/basic/external.html index e1513fed2d..2e6f533ca1 100644 --- a/packages/react/src/test/__snapshots__/mention/basic/external.html +++ b/packages/react/src/test/__snapshots__/mention/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content">I enjoy working with<span>@Matthew</span></p> \ No newline at end of file +<p class="bn-inline-content">I enjoy working with<span class="bn-inline-content-section" data-inline-content-type="mention" data-user="Matthew">@Matthew</span></p> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/mention/basic/internal.html b/packages/react/src/test/__snapshots__/mention/basic/internal.html index 7af6dad9c7..6ca7d81c2c 100644 --- a/packages/react/src/test/__snapshots__/mention/basic/internal.html +++ b/packages/react/src/test/__snapshots__/mention/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I enjoy working with<span>@Matthew</span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I enjoy working with<span class="bn-inline-content-section" data-inline-content-type="mention" data-user="Matthew">@Matthew</span></p></div></div></div></div> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/small/basic/external.html b/packages/react/src/test/__snapshots__/small/basic/external.html index 4206d07a95..35c3d5c232 100644 --- a/packages/react/src/test/__snapshots__/small/basic/external.html +++ b/packages/react/src/test/__snapshots__/small/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content"><small>This is a small text</small></p> \ No newline at end of file +<p class="bn-inline-content"><small class="bn-style" data-style-type="small">This is a small text</small></p> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/small/basic/internal.html b/packages/react/src/test/__snapshots__/small/basic/internal.html index 805c78112e..73836f647d 100644 --- a/packages/react/src/test/__snapshots__/small/basic/internal.html +++ b/packages/react/src/test/__snapshots__/small/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><small>This is a small text</small></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content"><small class="bn-style" data-style-type="small">This is a small text</small></p></div></div></div></div> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/external.html b/packages/react/src/test/__snapshots__/tag/basic/external.html index 4229ae0a83..b8387e9a55 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/external.html +++ b/packages/react/src/test/__snapshots__/tag/basic/external.html @@ -1 +1 @@ -<p class="bn-inline-content">I love <span>#<span>BlockNote</span></span></p> \ No newline at end of file +<p class="bn-inline-content">I love <span class="bn-inline-content-section" data-inline-content-type="tag">#<span>BlockNote</span></span></p> \ No newline at end of file diff --git a/packages/react/src/test/__snapshots__/tag/basic/internal.html b/packages/react/src/test/__snapshots__/tag/basic/internal.html index dac5db0ca8..bac28633b0 100644 --- a/packages/react/src/test/__snapshots__/tag/basic/internal.html +++ b/packages/react/src/test/__snapshots__/tag/basic/internal.html @@ -1 +1 @@ -<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I love <span>#<span>BlockNote</span></span></p></div></div></div></div> \ No newline at end of file +<div class="bn-block-group" data-node-type="blockGroup"><div class="bn-block-outer" data-node-type="blockOuter" data-id="1"><div class="bn-block" data-node-type="blockContainer" data-id="1"><div class="bn-block-content" data-content-type="paragraph"><p class="bn-inline-content">I love <span class="bn-inline-content-section" data-inline-content-type="tag">#<span>BlockNote</span></span></p></div></div></div></div> \ No newline at end of file