facebook / lexical

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

Bug: key backward cause ImageNode delete unexpected #6851

Open emsurezhang opened 4 days ago

emsurezhang commented 4 days ago
截屏2024-11-21 21 01 37

In the playground, I insert an ImageNode and press enter to start a new ParagraphNode. The cursor is at ParagraphNode which key is 4. Now I press backward and expect only the ParagraphNode key 4 is deleted. But the result is the ImageNode is deleted too.

截屏2024-11-21 21 07 09
emsurezhang commented 4 days ago

In LexicalSelection.ts removeText() function, I simply remove the node.remove line, it workes fine.

/**
   * Removes the text in the Selection, adjusting the EditorState accordingly.
   */
  removeText(): void {
    if (this.isCollapsed()) {
      return;
    }
    const {anchor, focus} = this;
    const selectedNodes = this.getNodes();
    const firstPoint = this.isBackward() ? focus : anchor;
    const lastPoint = this.isBackward() ? anchor : focus;
    let firstNode = firstPoint.getNode();
    let lastNode = lastPoint.getNode();
    const firstBlock = $getAncestor(firstNode, INTERNAL_$isBlock);
    const lastBlock = $getAncestor(lastNode, INTERNAL_$isBlock);
    // If a token is partially selected then move the selection to cover the whole selection
    if (
      $isTextNode(firstNode) &&
      firstNode.isToken() &&
      firstPoint.offset < firstNode.getTextContentSize()
    ) {
      firstPoint.offset = 0;
    }
    if (lastPoint.offset > 0 && $isTextNode(lastNode) && lastNode.isToken()) {
      lastPoint.offset = lastNode.getTextContentSize();
    }

    selectedNodes.forEach((node) => {
      if (
        !$hasAncestor(firstNode, node) &&
        !$hasAncestor(lastNode, node) &&
        node.getKey() !== firstNode.getKey() &&
        node.getKey() !== lastNode.getKey()
      ) {
        console.log("remove node", node.getKey())
        **// node.remove();**
      }
    });

    const fixText = (node: TextNode, del: number) => {
      if (node.getTextContent() === '') {
        node.remove();
      } else if (del !== 0 && $isTokenOrSegmented(node)) {
        const textNode = $createTextNode(node.getTextContent());
        textNode.setFormat(node.getFormat());
        textNode.setStyle(node.getStyle());
        return node.replace(textNode);
      }
    };
    if (firstNode === lastNode && $isTextNode(firstNode)) {
      const del = Math.abs(focus.offset - anchor.offset);
      firstNode.spliceText(firstPoint.offset, del, '', true);
      fixText(firstNode, del);
      return;
    }
    if ($isTextNode(firstNode)) {
      const del = firstNode.getTextContentSize() - firstPoint.offset;
      firstNode.spliceText(firstPoint.offset, del, '');
      firstNode = fixText(firstNode, del) || firstNode;
    }
    if ($isTextNode(lastNode)) {
      lastNode.spliceText(0, lastPoint.offset, '');
      lastNode = fixText(lastNode, lastPoint.offset) || lastNode;
    }
    if (firstNode.isAttached() && $isTextNode(firstNode)) {
      firstNode.selectEnd();
    } else if (lastNode.isAttached() && $isTextNode(lastNode)) {
      lastNode.selectStart();
    }

    // Merge blocks
    const bothElem = $isElementNode(firstBlock) && $isElementNode(lastBlock);
    if (bothElem && firstBlock !== lastBlock) {
      firstBlock.append(...lastBlock.getChildren());
      lastBlock.remove();
      lastPoint.set(firstPoint.key, firstPoint.offset, firstPoint.type);
    }
  }

is there any suggestion to fix it ?

emsurezhang commented 4 days ago

I think the real reason is after delete ParagraphNode the selection should be collapsed, so in the remove.text function

if (this.isCollapsed()) {
      return;
}

will be return, the node.remove() will not run.

emsurezhang commented 4 days ago

update: in LexicalSelection.ts file, change the native selection move mode from "extend" to "move", cause isCollapsed is true. at line 1781: this.modify('extend', isBackward, 'character'); => this.modify('move', isBackward, 'character');