Closed ChanhuiSeok closed 6 months ago
I think we are supposed to write styles for those classes.(I wrote styles for when I used this) It helps to apply multiple formatting to same text? instead of wrapping them inside different tags. In case you wanted properties being used inside the lexical playground here it is
I believe I have the same issue, and I do not believe it is just a styling concern.
It seems that $generateHtmlFromNodes correctly applies style classes to the HTML, for me it is something like this (snippet):
<span class="textUnderline">aaaa</span>
However as far as i can tell $generateNodesFromDOM does not have any logic to apply this in reverse (take the class and generate an underline node). So in my case where I am saving the output of $generateHtmlFromNodes and then reloading it via $generateNodesFromDOM I lose some subset of my styling on reload
Has anybody figured out any work around for this? I'm running into the same issue where most of my styles are getting dropped from the conversion from HTML back into lexical nodes.
So I think what you are looking for is exportDOM/importDOM, see here: https://lexical.dev/docs/concepts/serialization#lexicalnodeexportdom
For built in nodes I don't think think there is a great way to do this besides monkey patching, maybe I'm wrong, see this issue here: https://github.com/facebook/lexical/issues/1262
This is what I did for exportDOM for the build in TextNode:
const exportDOM = TextNode.prototype.exportDOM
TextNode.prototype.exportDOM = function (editor: LexicalEditor) {
if (this.__format & IS_UNDERLINE) {
const dom = document.createElement("u")
dom.textContent = this.__text
const maybeUnderline: string | string[] | undefined = editor._config.theme.text?.["underline"]
if (maybeUnderline) {
dom.className = Array.isArray(maybeUnderline) ? maybeUnderline.join(" ") : maybeUnderline
}
return { element: dom }
} else {
return exportDOM.apply(this, [editor])
}
}
For my use case importDOM is already set up to handle u elements, it was just not exporting that way, but not sure of your use case.
I resolved this issue by saving the Editor State instead of $generateNodesFromDOM
and then using setEditorState
to load the editor.
The editorState
contains all the styles such as the formatting and alignments, so there is no issue that the style is missing as before!
getEditorState
.setEditorState
.// How to load current editor
const [editor] = useLexicalComposerContext();
useEffect(() => {
// ...
editor.setEditorState(YOUR_SAVED_EDITOR_STATE);
}, [editor]);
Is it just me or is IS_UNDERLINE
only exported from the dev build and not the prod build? Lexical ships with a LexicalConstants.d.ts
file but no matching module. Storybook ends up complaining that it can't find lexical/LexicalConstants
, and running rg IS_UNDERLINE node_modules/lexical
returns the following:
Lexical.dev.js:const IS_UNDERLINE = 1 << 3;
Lexical.dev.js:const IS_ALL_FORMATTING = IS_BOLD | IS_ITALIC | IS_STRIKETHROUGH | IS_UNDERLINE | IS_CODE | IS_SUBSCRIPT | IS_SUPERSCRIPT; // Text node details
Lexical.dev.js: underline: IS_UNDERLINE
Lexical.dev.js: const prevUnderlineStrikethrough = prevFormat & IS_UNDERLINE && prevFormat & IS_STRIKETHROUGH;
Lexical.dev.js: const nextUnderlineStrikethrough = nextFormat & IS_UNDERLINE && nextFormat & IS_STRIKETHROUGH;
LexicalConstants.d.ts:export declare const IS_UNDERLINE: number;
It looks like the solution from @pbrown-iris-construction works as long as I define const IS_UNDERLINE = 1 << 3
. It would be great to see Lexical support exporting underlines in HTML as a core feature.
@pbrown-iris-construction do u have a solution for 'editor-text-strikethrough' and text alignment?
I've implemented a patch-function that adds missed tag conversion (for Lexical@0.6.3: sub
, sup
, s
) and background-color
and color
styles transfer from the DOM node to the Lexical node
const createMissedFormatConverter = (format: TextFormatType) => {
return (): DOMConversionOutput => {
return {
forChild: lexicalNode => {
if ($isTextNode(lexicalNode)) {
lexicalNode.toggleFormat(format);
}
return lexicalNode;
},
node: null,
};
};
};
const 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;
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,
color ? `color: ${color}` : null,
]
.filter(isNotNull)
.join('; ');
if (style.length) {
return result.setStyle(style);
}
}
return result;
},
};
};
};
export function applyHtmlToRichContentPatches() {
const importers = TextNode.importDOM();
TextNode.importDOM = function _() {
return {
...importers,
span: () => ({
conversion: patchStyleConversion(importers?.span),
priority: 0,
}),
sub: () => ({
conversion: createMissedFormatConverter('subscript'),
priority: 0,
}),
sup: () => ({
conversion: createMissedFormatConverter('superscript'),
priority: 0,
}),
s: () => ({
conversion: createMissedFormatConverter('strikethrough'),
priority: 0,
}),
};
};
const missedFormatTag: Array<[TextFormatType, string]> = [
['underline', 'u'],
['strikethrough', 's'],
];
const exportDOM = TextNode.prototype.exportDOM;
TextNode.prototype.exportDOM = function _(this: TextNode, editor: LexicalEditor) {
const { element } = exportDOM.apply(this, [editor]);
if (!element) {
return { element };
}
let wrapped = element;
for (const [format, tag] of missedFormatTag) {
if ($hasFormat(this, format)) {
const wrapper = document.createElement(tag);
wrapper.appendChild(element);
wrapped = wrapper;
}
}
return { element: wrapped };
};
}
UPD After some investigation I figured out that the way of determining of applied formats for the node wasn't correct for each cases, so I found the another way and updated the code snippet above using this
export function $hasFormat(node: TextNode, format: TextFormatType): boolean {
const currentFormat = node.getFormat();
return node.getFormatFlags(format, null) < currentFormat;
}
@byTimo I've a query regarding these patches, where do we have to include them? Because currently when I'm implementing directly these patches into my code I'm getting attached error
@byTimo I've a query regarding these patches, where do we have to include them? Because currently when I'm implementing directly these patches into my code I'm getting attached error
Actually these patches are just some kind of global monkey-patches, so you can call applyHtmlToRichContentPatches
somewhere in the module where your rich editor component is declared.
In my case, the patch is being called in the file with the Lexical editor configuration, but you can call it something like this
src/path/to/your/component/filename.ts
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import {applyHtmlToRichContentPatches) from 'path/to/patch';
....
applyHtmlToRichContentPatches();
const config = {
namespace: 'richEditor',
editable: true,
};
...
export const RichEditor = () => {
...
return (
<LexicalComposer initialConfig={config}>
...
</LexicalComposer>
)
}
BTW, I've updated the code snippet above a bit. Please, take a look if you're using this.
const [editor] = useLexicalComposerContext(); useEffect(() => { // ... editor.setEditorState(YOUR_SAVED_EDITOR_STATE); }, [editor]);
Hello @ChanhuiSeok! Can you give me a detailed example? I'm having the same problem but didn't found a solution yet.
Thanks!
I ran into similar problems as described here but found a few different fixes and improved a bit on what @byTimo did. First it wasnt clear to me that I had to add styling for the specific theme classes, a lot of patches weren't necessary for me after that. (Note: I only had issues with underlining) Coloring and background-color weren't working but I think that is intentional, so instead of patching I extended and override the TextNode like this:
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,
]
};
the extende node:
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;
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,
color ? `color: ${color}` : null
]
.filter((value) => value != null)
.join('; ');
if (style.length) {
return result.setStyle(style);
}
}
return result;
}
};
};
}
I am facing styling issues as well.
I have styles applied to all the elements except img
element.
When I resize the image in the editor and try to render the HTML generated by $generateHtmlFromNodes
, it overflows in x direction and none of the width, height are applied. I can see the values in editorState json but I don't see that coveted in the HTML.
I tried upgrading to the latest version. It didn't fix.
I have version 0.10.0
right now.
Please help!
--
Edit: NVM.
I just had to make the following update in ImageNode.tsx
exportDOM(): DOMExportOutput {
const element = document.createElement('img');
element.setAttribute('src', this.__src);
element.setAttribute('alt', this.__altText);
+ element.setAttribute('width', this.__width.toString());
+ element.setAttribute('height', this.__height.toString());
return { element };
}
I ran into similar problems as described here but found a few different fixes and improved a bit on what @byTimo did. First it wasnt clear to me that I had to add styling for the specific theme classes, a lot of patches weren't necessary for me after that. (Note: I only had issues with underlining) Coloring and background-color weren't working but I think that is intentional, so instead of patching I extended and override the TextNode like this:
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, ] };
the extende node:
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; 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, color ? `color: ${color}` : null ] .filter((value) => value != null) .join('; '); if (style.length) { return result.setStyle(style); } } return result; } }; }; }
This is the canonical way to do this.
https://github.com/facebook/lexical/issues/2452#issuecomment-1435638298 This works well. A small note that we may need to handle em
and strong
in addition to span
because of https://github.com/facebook/lexical/blob/74471ab9cfca443d3ba15fefbe487cd1d4a24049/packages/lexical/src/nodes/LexicalTextNode.ts#L118-L126
https://github.com/facebook/lexical/issues/2452#issuecomment-1435638298 This works well. A small note that we may need to handle
em
andstrong
in addition tospan
because of https://github.com/facebook/lexical/blob/74471ab9cfca443d3ba15fefbe487cd1d4a24049/packages/lexical/src/nodes/LexicalTextNode.ts#L118-L126
strong and em are handled in the docs example: https://lexical.dev/docs/concepts/serialization#handling-extended-html-styling
When using $generateNodesFromDOM to convert HTML to Lexical, styles such as
underline
oralign
are not being applied.In the case of HTML with styles that changes tag name such as
bold
(<strong>
) orItalic
(<em>
), the style looks well when converted to lexical using $generateNodesFromDOM.However, for HTML with styles that only add className(e.g.
editor-text-*
), such asunderline
orstrikethrough
, the styles don't seem to be applied when converted to lexical.I referenced the convert code(HTML -> Lexical - lexical doc).
Lexical version: 0.3.5
Steps To Reproduce
(* example HTML string - No style applied to text(
STYLE TEST
))<div class="editor-input" contenteditable="true" spellcheck="true" data-lexical-editor="true" style="user-select: text; white-space: pre-wrap; word-break: break-word;" role="textbox"><p class="editor-paragraph ltr" dir="ltr"><span data-lexical-text="true">STYLE TEST</span></p></div>
case 1. bold
<strong>
tag.case 2. underline
Select underline - The tagname is not changed(still
<span>
) and onlyeditor-text-underline
is added to the classname. The underline is also visible in the editor.However, when convert it to lexical based on html obtained in step 1 and insert, converted without underline applied.
case 3. bold + italic
Select bold and italic. The tagname is
<strong>
, and italics only apply toeditor-text-italic
classname.However, when convert and insert it, only applied
bold(<strong>)
. (where is theeditor-text-italic
? 😢 )Considering the above cases, when converting html string to lexical, style or classname is ignored and it seems to be applied only based on tagname.
Link to code example: HTML -> Lexical
The current behavior
$generateNodesFromDOM does not apply styles properly
The expected behavior
If $generateNodesFromDOM is used, the style and className of the dom must be maintained.