facebook / lexical

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

Bug: "excludeFromCopy: true" edits the exported HTML #5192

Open matisszemturis opened 11 months ago

matisszemturis commented 11 months ago

I created a cusom node that is used as "SelectionNode" for persisting active selection in editor at all times.

image

This nodes styling should not be exported in to HTML. Output after adding "SelectionNode" should still be the same as input before. But if we pass excludeFromCopy:true, then HTML is still edited.

Is there a way to create a Node that will appear in UI, but won't make exported HTML different?

Lexical version: 0.13.1

Steps To Reproduce

  1. Open sandbox
  2. Write text
  3. Select part of text
  4. Click "Toggle Selection"
  5. Text gets highlighted
  6. HTML export should look the same as it was after step 3

Link to code example: https://codesandbox.io/s/lexical-plain-text-example-forked-jmxl95?file=/src/nodes/SelectionNode.js

The current behavior

image

The expected behavior

image
acywatson commented 11 months ago

I really thought there was a way to do this. Thanks for reporting.

matisszemturis commented 9 months ago

@acywatson Is there anything that could be done to speed up this bugs resolving?

matisszemturis commented 7 months ago

For now, i created a this quick fix to merge sibling span elements together. It could be used like this:

 const generatedHTML = $generateHtmlFromNodes(editor)
 const htmlWithSpansMerged = mergeSiblings(generatedHTML)

This is not optimal, because of two things:

  1. We are doing an additional loop through the whole DOM tree
  2. It is possible to miss a tagName that should be merged or we might accidentaly not include a tagName.

    export function mergeSiblings(element: HTMLElement | string): string {
    if (typeof element === 'string') {
    const tempDiv = document.createElement('div')
    tempDiv.innerHTML = element
    return mergeSiblings(tempDiv)
    }
    
    const children = element.childNodes
    const isElement = element.nodeType === Node.ELEMENT_NODE
    
    if (isElement) {
    for (let i = 0; i < children.length; i++) {
      const child = children[i] as HTMLElement
      const nextChild = children?.[i + 1] as HTMLElement
    
      const isEqual = child.nodeName === nextChild?.nodeName
      const shouldMerge = isEqual && !PREVENT_MERGE_NODES.includes(child.nodeName as NodeName)
    
      if (shouldMerge) {
        child.innerHTML += nextChild.innerHTML
        nextChild.remove()
        i--
      } else {
        mergeSiblings(child)
      }
    }
    }
    return element.innerHTML
    }

So this:

<h1 class="h1" dir="ltr">
  <span style="white-space: pre-wrap;">This</span>
  <span style="white-space: pre-wrap;"> is </span>
  <span style="white-space: pre-wrap;">heading</span>
</h1>

is exported as:

<h1 class="h1" dir="ltr">
 <span style="white-space: pre-wrap;">This is heading</span>
</h1>