From 42cba0201c052c93962ef0b82914cb9a81c39d56 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 30 Apr 2023 20:26:35 +0100 Subject: [PATCH 1/6] Add HTML deserialization recipe to docs --- .../docs/concepts/serialization.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 62cd7cdf132..8dc39bd4e7e 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -298,3 +298,116 @@ export type SerializedTextNodeV2 = Spread< export type SerializedTextNode = SerializedTextNodeV1 | SerializedTextNodeV2; ``` +### Handling extended HTML styling + +Since the TextNode is foundational to all Lexical packages, including the plain text use case. Handling any rich text logic is undesirable. This creates the need to override the TextNode to handle serialization and deserialization of HTML/CSS styling properties to achieve full fidelity between JSON <-> HTML. Since this is a very popular use case, below we are proving a recipe to handle the most common use cases. + +You need to override the base TextNode: + +```js +const initialConfig: InitialConfigType = { + namespace: 'editor', + theme: editorThemeClasses, + onError: (error: any) => console.log(error), + nodes: [ + ExtentedTextNode, + { replace: TextNode, with: (node: TextNode) => new ExtentedTextNode(node.__text, node.__key) }, + ListNode, + ListItemNode, + ] + }; +``` + +and create a new Extended Text Node plugin + +```js +import { + $isTextNode, + DOMConversion, + DOMConversionMap, + DOMConversionOutput, + NodeKey, + TextNode, + SerializedTextNode +} from 'lexical'; + +export class ExtentedTextNode extends TextNode { + constructor(text: string, key?: NodeKey) { + super(text, key); + } + + static getType(): string { + return 'extended-text'; + } + + static clone(node: ExtentedTextNode): ExtentedTextNode { + return new ExtentedTextNode(node.__text, node.__key); + } + + static importDOM(): DOMConversionMap | null { + const importers = TextNode.importDOM(); + return { + ...importers, + span: () => ({ + conversion: patchStyleConversion(importers?.span), + priority: 1 + }) + }; + } + + static importJSON(serializedNode: SerializedTextNode): TextNode { + return TextNode.importJSON(serializedNode); + } + + exportJSON(): SerializedTextNode { + return super.exportJSON(); + } +} + +function patchStyleConversion( + originalDOMConverter?: (node: HTMLElement) => DOMConversion | null +): (node: HTMLElement) => DOMConversionOutput | null { + return (node) => { + const original = originalDOMConverter?.(node); + if (!original) { + return null; + } + const originalOutput = original.conversion(node); + + if (!originalOutput) { + return originalOutput; + } + + const backgroundColor = node.style.backgroundColor; + const color = node.style.color; + const fontStyle = node.style.fontStyle; + const fontWeight = node.style.fontWeight; + const textDecoration = node.style.textDecoration; + const textDecorationLine = node.style.textDecorationLine; + + return { + ...originalOutput, + forChild: (lexicalNode, parent) => { + const originalForChild = originalOutput?.forChild ?? ((x) => x); + const result = originalForChild(lexicalNode, parent); + if ($isTextNode(result)) { + const style = [ + backgroundColor ? `background-color: ${backgroundColor}` : null, // background color + color ? `color: ${color}` : null, // color + fontStyle ? `font-style: ${fontStyle}` : null, // italic + fontWeight ? `font-weight: ${fontWeight}` : null, // bold + textDecoration ? `text-decoration: ${textDecoration}` : null, // underline + textDecorationLine ? `text-decoration-line: ${textDecorationLine}` : null, // strikethrough + ] + .filter((value) => value != null) + .join('; '); + if (style.length) { + return result.setStyle(style); + } + } + return result; + } + }; + }; +} +``` From facaa2c8933500fe8d5a145600fd199b59b852f9 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Sun, 30 Apr 2023 20:32:28 +0100 Subject: [PATCH 2/6] Add HTML deserialization recipe to docs --- packages/lexical-website/docs/concepts/serialization.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 8dc39bd4e7e..483a0018056 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -384,6 +384,7 @@ function patchStyleConversion( const fontWeight = node.style.fontWeight; const textDecoration = node.style.textDecoration; const textDecorationLine = node.style.textDecorationLine; + const textAlign = node.style.textAlign; return { ...originalOutput, @@ -398,6 +399,7 @@ function patchStyleConversion( fontWeight ? `font-weight: ${fontWeight}` : null, // bold textDecoration ? `text-decoration: ${textDecoration}` : null, // underline textDecorationLine ? `text-decoration-line: ${textDecorationLine}` : null, // strikethrough + textAlign ? `text-align: ${textAlign}` : null, // alignment ] .filter((value) => value != null) .join('; '); From 3509b7cd3420390feded77fb58f9d50732c18e29 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 1 May 2023 17:19:53 +0100 Subject: [PATCH 3/6] Add HTML deserialization recipe to docs --- .../lexical-website/docs/concepts/serialization.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 483a0018056..f7f78da6447 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -310,8 +310,8 @@ const initialConfig: InitialConfigType = { theme: editorThemeClasses, onError: (error: any) => console.log(error), nodes: [ - ExtentedTextNode, - { replace: TextNode, with: (node: TextNode) => new ExtentedTextNode(node.__text, node.__key) }, + ExtendedTextNode, + { replace: TextNode, with: (node: TextNode) => new ExtendedTextNode(node.__text, node.__key) }, ListNode, ListItemNode, ] @@ -331,7 +331,7 @@ import { SerializedTextNode } from 'lexical'; -export class ExtentedTextNode extends TextNode { +export class ExtendedTextNode extends TextNode { constructor(text: string, key?: NodeKey) { super(text, key); } @@ -340,8 +340,8 @@ export class ExtentedTextNode extends TextNode { return 'extended-text'; } - static clone(node: ExtentedTextNode): ExtentedTextNode { - return new ExtentedTextNode(node.__text, node.__key); + static clone(node: ExtendedTextNode): ExtendedTextNode { + return new ExtendedTextNode(node.__text, node.__key); } static importDOM(): DOMConversionMap | null { From 7d28cbb80877e0d4fedeb58f51c21373fbaeef05 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Mon, 1 May 2023 23:04:46 +0100 Subject: [PATCH 4/6] Add only the missing styles --- .../docs/concepts/serialization.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index f7f78da6447..fca2670368d 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -380,11 +380,9 @@ function patchStyleConversion( const backgroundColor = node.style.backgroundColor; const color = node.style.color; - const fontStyle = node.style.fontStyle; + const fontFamily = node.style.fontFamily; const fontWeight = node.style.fontWeight; const textDecoration = node.style.textDecoration; - const textDecorationLine = node.style.textDecorationLine; - const textAlign = node.style.textAlign; return { ...originalOutput, @@ -393,13 +391,11 @@ function patchStyleConversion( const result = originalForChild(lexicalNode, parent); if ($isTextNode(result)) { const style = [ - backgroundColor ? `background-color: ${backgroundColor}` : null, // background color - color ? `color: ${color}` : null, // color - fontStyle ? `font-style: ${fontStyle}` : null, // italic - fontWeight ? `font-weight: ${fontWeight}` : null, // bold - textDecoration ? `text-decoration: ${textDecoration}` : null, // underline - textDecorationLine ? `text-decoration-line: ${textDecorationLine}` : null, // strikethrough - textAlign ? `text-align: ${textAlign}` : null, // alignment + backgroundColor ? `background-color: ${backgroundColor}` : null, + color ? `color: ${color}` : null, + fontFamily ? `font-family: ${fontFamily}` : null, + fontWeight ? `font-weight: ${fontWeight}` : null, + textDecoration ? `text-decoration: ${textDecoration}` : null, ] .filter((value) => value != null) .join('; '); From 98fc678f650026fe0d7645b1283d5d304fbba496 Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Fri, 5 May 2023 19:02:15 +0100 Subject: [PATCH 5/6] Handle beyond spans --- .../docs/concepts/serialization.md | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index fca2670368d..201e1ffee71 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -348,20 +348,48 @@ export class ExtendedTextNode extends TextNode { const importers = TextNode.importDOM(); return { ...importers, + code: () => ({ + conversion: patchStyleConversion(importers?.code), + priority: 1 + }), + em: () => ({ + conversion: patchStyleConversion(importers?.em), + priority: 1 + }), + i: () => ({ + conversion: patchStyleConversion(importers?.i), + priority: 1 + }), + s: () => ({ + conversion: patchStyleConversion(importers?.s), + priority: 1 + }), span: () => ({ conversion: patchStyleConversion(importers?.span), priority: 1 - }) + }), + strong: () => ({ + conversion: patchStyleConversion(importers?.strong), + priority: 1 + }), + sub: () => ({ + conversion: patchStyleConversion(importers?.sub), + priority: 1 + }), + sup: () => ({ + conversion: patchStyleConversion(importers?.sup), + priority: 1 + }), + u: () => ({ + conversion: patchStyleConversion(importers?.u), + priority: 1 + }), }; } static importJSON(serializedNode: SerializedTextNode): TextNode { return TextNode.importJSON(serializedNode); } - - exportJSON(): SerializedTextNode { - return super.exportJSON(); - } } function patchStyleConversion( From 26feb4509b1fb4d5420d46d933c1caabf752d51e Mon Sep 17 00:00:00 2001 From: Ivaylo Pavlov Date: Fri, 19 May 2023 21:43:57 +0100 Subject: [PATCH 6/6] Clean up --- .../lexical-website/docs/concepts/serialization.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/lexical-website/docs/concepts/serialization.md b/packages/lexical-website/docs/concepts/serialization.md index 201e1ffee71..5d048ce630c 100644 --- a/packages/lexical-website/docs/concepts/serialization.md +++ b/packages/lexical-website/docs/concepts/serialization.md @@ -356,14 +356,6 @@ export class ExtendedTextNode extends TextNode { conversion: patchStyleConversion(importers?.em), priority: 1 }), - i: () => ({ - conversion: patchStyleConversion(importers?.i), - priority: 1 - }), - s: () => ({ - conversion: patchStyleConversion(importers?.s), - priority: 1 - }), span: () => ({ conversion: patchStyleConversion(importers?.span), priority: 1 @@ -380,10 +372,6 @@ export class ExtendedTextNode extends TextNode { conversion: patchStyleConversion(importers?.sup), priority: 1 }), - u: () => ({ - conversion: patchStyleConversion(importers?.u), - priority: 1 - }), }; } @@ -410,6 +398,7 @@ function patchStyleConversion( const color = node.style.color; const fontFamily = node.style.fontFamily; const fontWeight = node.style.fontWeight; + const fontSize = node.style.fontSize; const textDecoration = node.style.textDecoration; return { @@ -423,6 +412,7 @@ function patchStyleConversion( color ? `color: ${color}` : null, fontFamily ? `font-family: ${fontFamily}` : null, fontWeight ? `font-weight: ${fontWeight}` : null, + fontSize ? `font-size: ${fontSize}` : null, textDecoration ? `text-decoration: ${textDecoration}` : null, ] .filter((value) => value != null)