😇

Facebook製エディタLexicalを試してみたよ!(2) 機能追加してみた!

2022/04/21に公開

Facebook(Meta)製のエディタライブラリのLexicalについて、もうちょっと調べる時間がもらえたので第2弾です!

前回の記事で書いた通り、やっぱり公式のプレイグラウンドのようなツールバーのUIは無いっぽいです。
ライブラリにはエディタの機能だけがあって、必要なUIは各自で作ってもらうという方針なのかなと思いました。

sandboxからツールバーのコードを拝借してきて設置したところ、無事にプレイグラウンドと同じものが動きました!

Nodeについて

Lexicalエディタの中ではすべての要素がNodeという単位で管理されているようです。

プラグインを使ってh1とかリンクとか色々な要素を使うためには、まず使いたい種類のノードを初期設定で指定する必要があるみたい。
ノードの指定を外すと、処理は通るけど機能はしませんでした。

import { HeadingNode, QuoteNode } from "@lexical/rich-text"
import { AutoLinkNode, LinkNode } from "@lexical/link"

const Editor: NextPage = () => {  
  // エディタ設定
  const initialConfig:any = {
    theme: ExampleTheme,
    onError,
    // 使いたいノードを指定する
    nodes: [
      HeadingNode,
      QuoteNode,
      AutoLinkNode,
      LinkNode
    ]
  };
  
  return (
    <>
      <LexicalComposer initialConfig={initialConfig}>

リストやテーブルやコードブロックなど、それぞれ対応したノードが用意されていました。
独自で機能を作りたい時は、カスタムノードを自作する必要があるみたいです。

そこで、公式ドキュメントにあったテキストカラーを変更するカスタムノードを作って、機能を追加してみようと思います!!

ノードの種類

Lexicalのノードは基本となる5種類(Root, LineBreak, Element, Text, Decorator)があって、そのうちカスタムノードに使えるのは ElementNode TextNode DecoratorNode の3種類だそうです。

ElementNode

ElementNode は h1 や p などのブロックレベル要素とリンクなどのインライン要素に使われるそうです。
ブロックレベルとインラインて全部やん!と思ったんですが、strong や u などの装飾については TextNode に該当するらしいです。どうやって使い分けるんでしょう? 属性値を使うかどうかみたいな?

TextNode

TextNode はテキストを装飾するためのノードのようです。
format mode style のプロパティを持っています。

format は装飾の種類で、bold, italic, underline, strikethrough, code, subscript, superscript が指定できます。

mode は、たぶん削除に関するルールだと思います。
token は変更不可で、削除するときは一度に消えます。
inert もtokenと同じく変更不可ですが、カーソルを中に入れて部分選択することもできなくなるようです。
segment は単語区切りで削除されるようです。
ドキュメントには書いていませんでしたが、たぶん1文字ずつ削除される 未指定 っていう状態がデフォルトであるんでしょうね。nullを指定すればいいのかな?

style はCSSが記述できるようです。

DecoratorNode

DecoratorNode はリファレンスを読んでもよくわかりませんでした。
また詳しいことがわかったら報告します。

カスタムノードの作り方

基本ルール

ノードクラスのプロパティは、__から始まるというルールがあるそうです。

export class ColoredNode extends TextNode {
  __color: string;

ノードには getTypeclone というクラスメソッドが必須とのことです。

getTypeは、ノードの名称を返します。マニュアルには特に書いていませんでしたが、たぶんエディタの中で一意でないとダメなのかなと思いました。

  static getType(): string {
    return 'colored';
  }

cloneは、ノードを複製するための処理を書きます。エディタの中でコピー&ペーストする時に働くようです。

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

コンストラクタについて

ノードのコンストラクタは、constructor(text: string, key?: NodeKey) というのが基本形のようです。

  constructor(text: string, key?: NodeKey): void {
    super(text, key);
  }

text はTextNodeの時はテキストそのものを指すみたいです。ElementNodeの時はこの引数が何に使われているのか分かりませんでした。HeadingNodeでは初期化時に h1 などの文字列が渡されてました。

key はnullableで、HeadingNodeの初期化時は未指定でした。cloneの時しか使わないかもですね。カスタムノードでコンストラクタの引数が増えた時は、key を最後に置いておくもののようです。

createDomupdateDom

createDomupdateDom は、そのノードに対応したDOM(HTMLエレメント)を作るためのメソッドです。

createDom はDOMの初期化メソッドで、HTMLElement を返します。

  createDOM(): HTMLElement {
    const dom = document.createElement('p');
    return dom;
  }

updateDom はDOMを更新した時に createDom で作り直す必要があるかどうかを true/false で返すみたいです。
現時点ではどういう場面でtrueを返すべきなのか分かりませんでした・・・

  updateDOM(prevNode: ColoredNode, dom: HTMLElement): boolean {
    return false;
  }

テキストカラーを変更するカスタムノードを作る

今回はTextNodeを拡張して、styleプロパティを使ってCSSでテキストカラーを指定できるカスタムノードを作りたいと思います!

ノードを作る

ここは公式サイトの通り。

ColoredNode.ts
export class ColoredNode extends TextNode {
  __color: string;

  constructor(text: string, color: string, key?: NodeKey): void {
    super(text, key);
    this.__color = color;
  }

  static getType(): string {
    return 'colored';
  }

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

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

  updateDOM(
    prevNode: ColoredNode,
    dom: HTMLElement,
    config: EditorConfig,
  ): boolean {
    const isUpdated = super.updateDOM(prevNode, dom, config);
    if (prevNode.__color !== this.__color) {
      dom.style.color = this.__color;
    }
    return isUpdated;
  }
}

ツールバーを拡張する

まずはツールバーにカラー変更ボタンを作ります。
こんな感じ。

ToolbarPlugin.tsx
<Divider />
<button
  onClick={()=>onColorSelect("black")}
  className="toolbar-item spaced">
</button>
<button
  onClick={()=>onColorSelect("red")}
  className="toolbar-item spaced">
</button>
<button
  onClick={()=>onColorSelect("green")}
  className="toolbar-item spaced">
</button>
<button
  onClick={()=>onColorSelect("blue")}
  className="toolbar-item spaced">
</button>

次に、選択した部分をさっき作ったカスタムノード ColoredNode に置き換える処理を書きます。

ToolbarPlugin.tsx
  const onColorSelect = (color:string) => {
    editor.update(() => {
      const selection:any = $getSelection();
      if ($isRangeSelection(selection)) {
        const text = selection.getTextContent()
        const colored = new ColoredNode(text, color)
        selection.removeText()
        selection.insertNodes([colored])
      }
    })
  }

これで準備OKです!

できあがり!

実際にこの機能を使ってみると、ちゃんとテキストの色が変わりました! やった!

だけど、このままだと実用はできません。
この方法だと、選択した部分にboldとかの装飾があったら、装飾が消えてしまうんです。

あと、色を変えたテキストにboldなどの装飾をかけても、やっぱり色が戻ってしまいました。

うまくいきませんね。
もっと上手な方法があるはずなんですが・・・
これでも、かなりがんばったんですけどね!

今回はこれでせいいっぱい。
やっぱり公式サイトのリファレンスが充実するまでは、まだまだ手を出すのは早いのかなと思いました!

手探りしんどい!!!

Discussion