facebook / lexical

Lexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.
https://lexical.dev
MIT License
19.09k stars 1.61k forks source link

Bug: $generateHtmlFromNodes and tables #5081

Open emaborsa opened 11 months ago

emaborsa commented 11 months ago

Lexical version:

@lexical/react 0.10.0 lexical 0.10.0

as well with @lexical/react 0.12.2 lexical 0.12.2

Problem

Unexpected style for generated html table using $generateHtmlFromNodes

Lexical JSON

{
  "root": {
    "children": [
      {
        "children": [],
        "direction": null,
        "format": "",
        "indent": 0,
        "type": "paragraph",
        "version": 1
      },
      {
        "children": [
          {
            "children": [
              {
                "children": [
                  {
                    "detail": 0,
                    "format": 1,
                    "mode": "normal",
                    "style": "",
                    "text": "Modelle",
                    "type": "text",
                    "version": 1
                  }
                ],
                "direction": null,
                "format": "",
                "indent": 0,
                "type": "tablecell",
                "version": 1,
                "headerState": 0
              },
              {
                "children": [
                  {
                    "detail": 0,
                    "format": 1,
                    "mode": "normal",
                    "style": "",
                    "text": "Rabatt",
                    "type": "text",
                    "version": 1
                  }
                ],
                "direction": null,
                "format": "",
                "indent": 0,
                "type": "tablecell",
                "version": 1,
                "headerState": 0
              }
            ],
            "direction": null,
            "format": "",
            "indent": 0,
            "type": "tablerow",
            "version": 1
          },
          {
            "children": [
              {
                "children": [
                  {
                    "children": [
                      {
                        "detail": 0,
                        "format": 0,
                        "mode": "normal",
                        "style": "",
                        "text": "Cupra Leon, Cupra leon Sportstourer ",
                        "type": "text",
                        "version": 1
                      }
                    ],
                    "direction": "ltr",
                    "format": "",
                    "indent": 0,
                    "type": "paragraph",
                    "version": 1
                  }
                ],
                "direction": null,
                "format": "",
                "indent": 0,
                "type": "tablecell",
                "version": 1,
                "headerState": 0
              },
              {
                "children": [
                  {
                    "children": [
                      {
                        "detail": 0,
                        "format": 0,
                        "mode": "normal",
                        "style": "",
                        "text": "15 % ",
                        "type": "text",
                        "version": 1
                      }
                    ],
                    "direction": null,
                    "format": "",
                    "indent": 0,
                    "type": "paragraph",
                    "version": 1
                  }
                ],
                "direction": null,
                "format": "",
                "indent": 0,
                "type": "tablecell",
                "version": 1,
                "headerState": 0
              }
            ],
            "direction": null,
            "format": "",
            "indent": 0,
            "type": "tablerow",
            "version": 1
          }
        ],
        "direction": "ltr",
        "format": "",
        "indent": 0,
        "type": "table",
        "version": 1
      }
    ],
    "direction": "ltr",
    "format": "",
    "indent": 0,
    "type": "root",
    "version": 1
  },
}

Current result

<p class="editor-paragraph">
  <br/>
  <table class="editor-table">
    <colgroup>
      <col>
        <col/>
        <tbody>
          <tr>
            <td class="editor-table-cell" style="border: 1px solid black; width: 350px; vertical-align: top; text-align: start;">
              <b>
                <strong class="editor-text-bold">Modelle</strong>
              </b>
            </td>
            <td class="editor-table-cell" style="border: 1px solid black; width: 350px; vertical-align: top; text-align: start;">
              <b>
                <strong class="editor-text-bold">Rabatt</strong>
              </b>
            </td>
          </tr>
          <tr>
            <td class="editor-table-cell" style="border: 1px solid black; width: 350px; vertical-align: top; text-align: start;">
              <p class="editor-paragraph" dir="ltr">
                <span>Cupra Leon, Cupra leon Sportstourer&nbsp;</span>
              </p>
            </td>
            <td class="editor-table-cell" style="border: 1px solid black; width: 350px; vertical-align: top; text-align: start;">
              <p class="editor-paragraph">
                <span>15 %&nbsp;</span>
              </p>
            </td>
          </tr>
        </tbody>
      </table>

Expected result

<p class="editor-paragraph">
  <br/>
  <table class="editor-table">
    <tbody>
      <tr>
        <td class="editor-table-cell">
          <b>
            <strong class="editor-text-bold">Modelle</strong>
          </b>
        </td>
        <td class="editor-table-cell">
          <b>
            <strong class="editor-text-bold">Rabatt</strong>
          </b>
        </td>
      </tr>
      <tr>
        <td class="editor-table-cell">
          <p class="editor-paragraph" dir="ltr">
            <span>Cupra Leon, Cupra leon Sportstourer&nbsp;</span>
          </p>
        </td>
        <td class="editor-table-cell">
          <p class="editor-paragraph">
            <span>15 %&nbsp;</span>
          </p>
        </td>
      </tr>
    </tbody>
  </table>
</p>
acywatson commented 11 months ago

Can you be more specific about which part doesn't meet your expectations here?

emaborsa commented 11 months ago

Actually all the tds style. Why are they added?

murilodaluz commented 10 months ago

Same issue to me I found in the exportDOM of the LexicalTableCellNode has this weird style injection. Does anyone know why? https://github.com/facebook/lexical/blob/ed8f85fb5e5106dba237e2a166e57acaa28e8fbe/packages/lexical-table/src/LexicalTableCellNode.ts#L141

To overcome this, it is possible to use "replace" with a "CustomCellNode" rewriting the function without the injection. It works, but...

image

// illustrative code
import { SerializedTableCellNode, TableCellNode } from '@lexical/table'
import {
  $applyNodeReplacement,
  type DOMConversionMap,
  type DOMExportOutput,
  type LexicalEditor,
  DOMConversionOutput
} from 'lexical'
import { nanoid } from 'nanoid'
import { $createTableNode } from './TableNode'

export const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/

export declare const TableCellHeaderStates: {
  BOTH: number
  COLUMN: number
  NO_STATUS: number
  ROW: number
}

export type Cell = {
  colSpan: number
  json: string
  type: 'normal' | 'header'
  id: string
  width: number | null
}

export type Row = {
  cells: Array<Cell>
  height: null | number
  id: string
}

export type Rows = Array<Row>

export type TableCellHeaderState = (typeof TableCellHeaderStates)[keyof typeof TableCellHeaderStates]
export class CustomTableCellNode extends TableCellNode {
  static getType(): string {
    return 'custom-tablecell'
  }
  exportDOM(editor: LexicalEditor): DOMExportOutput {
    const domExported = super.exportDOM(editor)
    if (domExported.element) {
      const element_ = domExported.element as HTMLElement
      element_.style.border = ''
      element_.style.width = this.getWidth() ? `${this.getWidth()}px` : ''
      return {
        ...domExported,
        element: element_
      }
    }

    return domExported
  }

  exportJSON(): SerializedTableCellNode {
    return {
      ...super.exportJSON(),
      type: 'custom-tablecell'
    }
  }

  static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
    const colSpan = serializedNode.colSpan || 1
    const rowSpan = serializedNode.rowSpan || 1
    const cellNode = $applyNodeReplacement(
      new CustomTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined)
    ) as TableCellNode
    cellNode.__rowSpan = rowSpan
    cellNode.__backgroundColor = serializedNode.backgroundColor || null
    return cellNode
  }

  static importDOM(): DOMConversionMap | null {
    return {
      table: (_node: Node) => ({
        conversion: convertTableElement,
        priority: 0
      })
    }
  }

  static clone(node: CustomTableCellNode) {
    const cellNode = new CustomTableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key)
    cellNode.__rowSpan = node.__rowSpan
    cellNode.__backgroundColor = node.__backgroundColor
    return cellNode
  }
}

export function $createTableCellNode(headerState: TableCellHeaderState, colSpan = 1, width?: number): TableCellNode {
  return $applyNodeReplacement(new CustomTableCellNode(headerState, colSpan, width))
}

function createCell(type: 'normal' | 'header'): Cell {
  return {
    colSpan: 1,
    id: nanoid(),
    json: emptyEditorJSON,
    type,
    width: null
  }
}

const emptyEditorJSON =
  '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'

const plainTextEditorJSON = (text: string) =>
  text === ''
    ? emptyEditorJSON
    : `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`

export function createRow(): Row {
  return {
    cells: [],
    height: null,
    id: nanoid()
  }
}

function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
  const rowElems = domNode.querySelectorAll('tr')
  if (!rowElems || rowElems.length === 0) {
    return null
  }
  const rows: Rows = []
  for (let y = 0; y < rowElems.length; y++) {
    const rowElem = rowElems[y]
    const cellElems = rowElem.querySelectorAll('td,th')
    if (!cellElems || cellElems.length === 0) {
      continue
    }
    const cells: Array<Cell> = []
    for (let x = 0; x < cellElems.length; x++) {
      const cellElem = cellElems[x] as HTMLElement
      const isHeader = cellElem.nodeName === 'TH'
      const cell = createCell(isHeader ? 'header' : 'normal')
      cell.json = plainTextEditorJSON(JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')))
      cells.push(cell)
    }
    const row = createRow()
    row.cells = cells
    rows.push(row)
  }
  return { node: $createTableNode(rows) }
}
nocodehummel commented 10 months ago

Same issue. Why does $generateHtmlFromNodes add styles? Styles are not visible when inspecting the table elements in the editor. But the generated HTML includes hardcoded styles for th and td.

My workaround is to remove any style attributes from the HTML. I use the Lexical theme to configured classes to separate styling from the content.

const regex = /(style=".+?")/gm;
const editorState = editor.getEditorState().toJSON();
let html = $generateHtmlFromNodes(editor, null);
html = html.replace(regex, '');
console.log(html);
murilodaluz commented 9 months ago

Same issue. Why does $generateHtmlFromNodes add styles? Styles are not visible when inspecting the table elements in the editor. But the generated HTML includes hardcoded styles for and .

My workaround is to remove any style attributes from the HTML.

const regex = /(style=".+?")/gm;
const editorState = editor.getEditorState().toJSON();
let html = $generateHtmlFromNodes(editor, null);
html = html.replace(regex, '');
console.log(html);

If you only remove the style, when you try to define a width for the column, it will not be defined in the generated HTML.

emaborsa commented 9 months ago

Same issue. Why does $generateHtmlFromNodes add styles? Styles are not visible when inspecting the table elements in the editor. But the generated HTML includes hardcoded styles for and .

My workaround is to remove any style attributes from the HTML.

const regex = /(style=".+?")/gm;
const editorState = editor.getEditorState().toJSON();
let html = $generateHtmlFromNodes(editor, null);
html = html.replace(regex, '');
console.log(html);

Be aware, removing all the styles you are going to loose the italic and bold of the paragraphs.

nocodehummel commented 9 months ago

Same issue. Why does $generateHtmlFromNodes add styles? Styles are not visible when inspecting the table elements in the editor. But the generated HTML includes hardcoded styles for and . My workaround is to remove any style attributes from the HTML.

const regex = /(style=".+?")/gm;
const editorState = editor.getEditorState().toJSON();
let html = $generateHtmlFromNodes(editor, null);
html = html.replace(regex, '');
console.log(html);

Be aware, removing all the styles you are going to loose the italic and bold of the paragraphs.

This is not an issue in my implementation since I always use the theme classes to control styling.

emaborsa commented 9 months ago

Same issue. Why does $generateHtmlFromNodes add styles? Styles are not visible when inspecting the table elements in the editor. But the generated HTML includes hardcoded styles for and . My workaround is to remove any style attributes from the HTML.

const regex = /(style=".+?")/gm;
const editorState = editor.getEditorState().toJSON();
let html = $generateHtmlFromNodes(editor, null);
html = html.replace(regex, '');
console.log(html);

Be aware, removing all the styles you are going to loose the italic and bold of the paragraphs.

This is not an issue in my implementation since I always use the theme classes to control styling.

If in lexical you set italic and bold to a paragraph, one of them is set as a style and if you remove it, how can you apply your style?

nocodehummel commented 9 months ago

I use the below theme that adds the specified class to each of the HTML elements.

const themeClasses = {
    paragraph: 'editorParagraph',
    link: 'editorLink',
    text: {
        bold: 'editorBold',
        underline: 'editorUnderline',
        italic: 'editorItalic',
    }......
}
<LexicalComposer
    initialConfig={{
        namespace: 'markdown-editor',
        theme: themeClasses,
        nodes: EDITOR_NODES,
        onError: (error) => {
            console.log(error);
        },
    }}
>

This renders HTML my text is <strong class='editorBold'>bold</strong> :-) which I format with external CSS. I prefer to work with classes and leave the CSS outside the editor state so I can adjust the CSS as needed.