Bug: $generateNodesFromDOM does not apply styles properly

See original GitHub issue

When using $generateNodesFromDOM to convert HTML to Lexical, styles such as underline or align are not being applied.

In the case of HTML with styles that changes tag name such as bold(<strong>) or Italic(<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 as underline or strikethrough, 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

  1. Select bold - it is applied to <strong> tag. 스크린샷 2022-06-17 오전 11 05 53
  2. When convert it to lexical based on the html string obtained in step 1 and insert it, the bold style is well as shown below. 스크린샷 2022-06-17 오전 11 14 31

case 2. underline

  1. Select underline - The tagname is not changed(still <span>) and only editor-text-underline is added to the classname. 스크린샷 2022-06-17 오전 11 18 01 The underline is also visible in the editor. 스크린샷 2022-06-17 오전 11 20 30

  2. However, when convert it to lexical based on html obtained in step 1 and insert, converted without underline applied. 스크린샷 2022-06-17 오전 11 21 34

case 3. bold + italic

  1. Select bold and italic. The tagname is <strong>, and italics only apply to editor-text-italic classname. 스크린샷 2022-06-17 오전 11 25 33

  2. However, when convert and insert it, only applied bold(<strong>). (where is the editor-text-italic? 😢 ) 스크린샷 2022-06-17 오전 11 31 41

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

const LoadHtmlPlugin = ({htmlString}: Props) => {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    editor.update(() => {
      const parser = new DOMParser();
      const dom = parser.parseFromString(htmlString, 'text/html');
      const nodes = $generateNodesFromDOM(editor, dom);

      // Select the root
      $getRoot().select();

      // Insert them at a selection.
      const selection = $getSelection() as RangeSelection;
      selection.insertNodes(nodes);
    });
  }, [editor]);
  return null;
};

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.

Issue Analytics

  • State:open
  • Created a year ago
  • Reactions:18
  • Comments:10 (2 by maintainers)

github_iconTop GitHub Comments

8reactions
byTimocommented, Dec 15, 2022

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;
}
6reactions
patches11commented, Jun 22, 2022

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

lexicaljs - Setting Editor from HTML - Stack Overflow
Got it working with the following code editor.update(() => { // In the browser you can use the native DOMParser API to parse...
Read more >
How to Fix CSS file is not Working with HTML in Chrome
Fix CSS file is not Working with HTML in Chrome | Problem Solved Show SupportBuy Me a COFFEE: https://buymeacoffee.com/Webjahin  ...
Read more >
@lexical/html - NPM Package Overview - Socket
This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code....
Read more >
240592 - Implement HTML Imports - chromium - Monorail
Tracking bug to HTML Imports implementation: ... use it in imports ... For example, the spec as defined is not very useful for...
Read more >
How to Style a Form with HTML and CSS - Hello Dev World
We are going use some CSS to take our form from yesterday from this… ... we can style it properly on the left...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found