🖋️

LexicalのDOM出力でParagraph Nodeからspanを消したい

2024/09/14に公開

Lexicalを使っていて、段落を追加するたびにspanが入ってきて邪魔だと思ったことはありませんか?
正確に言うとParagraph NodeではなくText NodeのDOM出力ですが、spanを無理やり消したので残しておこうと思います

今回作成したコードはここにあります
https://github.com/shibaTT/lexical-overrides-text-node

サンプルはこちらから
https://shibatt.github.io/lexical-overrides-text-node/

Text NodeをOverrideする

Lexicalでは既存のノードをオーバーライドすることができます

今回はText Nodeをオーバーライドして CustomTextNode を作成していきます

CustomTextNodeの作成

/src/nodes/CustomTextNode.ts
import {
  $applyNodeReplacement,
  DOMExportOutput,
  EditorConfig,
  LexicalEditor,
  SerializedTextNode,
  TextNode,
} from "lexical";

export class CustomTextNode extends TextNode {
  static getType(): string {
    return "custom-text";
  }

  static clone(node: CustomTextNode): CustomTextNode {
    return new CustomTextNode(node.__text, node.__key);
  }

  exportDOM(editor: LexicalEditor): DOMExportOutput {
    const { element } = super.exportDOM(editor);
    const textContent = element ? element.textContent : "";
    const text = new Text(textContent ?? "");
    return { element: text };
  }

  createDOM(config: EditorConfig): HTMLElement {
    const element = super.createDOM(config);
    return element;
  }

  static importJSON(serializedNode: SerializedTextNode): CustomTextNode {
    const node = $createCustomTextNode(serializedNode.text);
    node.setFormat(serializedNode.format);
    node.setDetail(serializedNode.detail);
    node.setMode(serializedNode.mode);
    node.setStyle(serializedNode.style);
    return node;
  }

  exportJSON(): SerializedTextNode {
    return {
      ...super.exportJSON(),
      type: CustomTextNode.getType(),
      version: 1,
    };
  }
}

export const $createCustomTextNode = (text: string = ""): CustomTextNode => {
  return $applyNodeReplacement(new CustomTextNode(text));
};

ここで大事なのはexportDOMの部分になります

exportDOM(editor: LexicalEditor): DOMExportOutput {
  const { element } = super.exportDOM(editor);
  const textContent = element ? element.textContent : "";
  const text = new Text(textContent ?? "");
  return { element: text };
}

DOMExportOutputの中身は HTMLElement | Text | null の型なので、Text型を返してあげることで文字列だけを返すことができます
ただ、このままだとインラインスタイル(太字とか斜体文字とか)を適用していた場合、中身の文字しか出力されないので期待通りの動きにはなりません
そこで、面倒ですがインラインスタイルが適用されている場合はElementを返し、それ以外は中身の文字を返すという処理にすることで回避できます

exportDOM(editor: LexicalEditor): DOMExportOutput {
  const { element } = super.exportDOM(editor);
  if (element instanceof HTMLElement) {
    if (element.tagName === "B" || element.tagName === "I") {
      // 太字、斜体の場合はそのままElementを返す
      return { element: element.firstElementChild as HTMLElement };
    } else if (element.tagName === "U" || element.tagName === "S") {
      // 下線、取り消し線の場合は中身のspanを取り除いて返す
      const textContent = element.innerText;
      element.innerHTML = textContent;
      return { element };
    }
  }
  const textContent = element ? element.textContent : "";
  const text = new Text(textContent ?? "");
  return { element: text };
}

もう少し良い処理の仕方はあると思いますが、これでspanを取り除いて出力することができました

あとはエディタの初期設定にオーバーライドする設定を書き足すことで作成したCustomTextNodeが適用されるようになります

オーバーライド設定の追加

エディタの初期設定にあるnodesにCustomTextNodeを追加します
フルコードはこちらから見れます

App.tsx
const initialConfig = {
  namespace: "override",
  onError,
  nodes: [
+    CustomTextNode,
+    {
+      replace: TextNode,
+      with: (node: TextNode) => {
+        return new CustomTextNode(node.getTextContent());
+      },
+    },
  ],
};

これで文字を入力するたびにspanのないテキストが生成されるようになったかと思います

さいごに

修正や訂正、こうしたほうがいいよ、というのがあれば教えて下さい!
また、フルリモートで働けるフロントエンドエンジニアの求人も探してます
大したスキルは持ってないですけどね!

Discussion