Open23

Lexical関連のお勉強

br_branchbr_branch

https://zenn.dev/stin/articles/lexical-rich-editor-trial より

import { HeadingNode } from "@lexical/rich-text";
import { Klass, LexicalNode } from "lexical";

export const nodes: Klass<LexicalNode>[] = [HeadingNode];

Klass はインスタンス化可能なクラスだけに限定するためのユーティリティ型です(TypeScript では class の定義と同時に interface が定義されるので、 class に限定する意図があります)。

そんなことできるんだ…実装
https://github.com/facebook/lexical/blob/1cf4eada0c414f2747bffad7f47dd0668057554a/packages/lexical/src/LexicalEditor.ts#L42-L45

よくわからない(´・ω・`)
new を必須にしてることでinterfaceが定義されないようにしてるって…コト?
それやることで何が嬉しいんだろ?

試しに以下のようにしてみたらエラーが出た。

export const nodes: LexicalNode[] = [HeadingNode];

TS2740: Type 'typeof HeadingNode' is missing the following properties from type 'LexicalNode': __type, __key, __parent, __prev, and 36 more.
タイプ 'typeof HeadingNode' には、タイプ 'LexicalNode' の次のプロパティがありません: __type、__key、__parent、__prev、およびその他 36 個。

へぇ…?

宣言みてみたら、たしかに HeadingNodeには __type とか定義されてない。
定義されてないけど、なんでそれで上手くいくんだろ?(´・ω・`)

LexicalNode#constructorが除外されてるから上記のエラーが起きなくなるってことなのかな

やっぱ何が嬉しいんだろ?
調べてもよくわからないからとりあえず棚上げして次行こ。

====
ChatGPTに聞いてみた。


このコードは、ジェネリック型パラメーターTを持つKlass型をエクスポートしています。Tは、LexicalNodeを拡張したクラスの型を指定することができます。

Klass型は、クラスを表現するためのオブジェクト型です。具体的には、newシグネチャを持つオブジェクト型として定義されています。newシグネチャは、クラスのインスタンスを作成するための関数の型を表します。

このKlass型は、以下のように使用されることがあります。

class MyNode extends LexicalNode {
  // ...
}

const MyNodeClass: Klass<MyNode> = MyNode;
const instance = new MyNodeClass();

この例では、MyNodeクラスがLexicalNodeを拡張しているため、Klass<MyNode>はnewシグネチャとLexicalNode型を持つオブジェクト型になります。MyNodeClassにはMyNodeクラス自体を代入することができます。instanceは、MyNodeClassから作成されたMyNodeクラスのインスタンスです。

このように、Klass型は、型パラメーターで指定したクラスのインスタンスを作成するための型を表現することができます。これは、クラスを動的に作成する必要がある場合に有用です。例えば、プログラムの実行時にクラスを動的に作成して、それをインスタンス化する必要がある場合などに使用されます。


ちょっとわかったようなわからないような。

br_branchbr_branch

https://zenn.dev/stin/articles/lexical-rich-editor-trial より

  const formatHeading = useCallback(
    (type: HeadingTagType) => {
      if (blockType !== type) {
        editor.update(() => {
          const selection = $getSelection();
          if ($isRangeSelection(selection)) {
            $wrapLeafNodesInElements(selection, () => $createHeadingNode(type));
          }
        });
      }
    },
    [blockType, editor],
  );

やってることはわかるんだけど、 $getSelection() でSlectionの内容が取得できる…?
Selectionはglobal or docmentのselectionをいい感じに計算してくれてるだけのユーティリティ関数なのかな

なお、現在は $wrapLeafNodesInElements はなくなって $wrapNodes に置き換わっている
PR: https://github.com/facebook/lexical/pull/3020/files
ドキュメント: https://lexical.dev/docs/api/modules/lexical_selection#wrapnodes

br_branchbr_branch

APIリファレンスに意味がかかれてなくてつらたん。
関数名で察しろってことかな

br_branchbr_branch

一見すると editor.update 内でeditorの参照がないのに、アップデートされるのはどういう仕組だろ?
$wrapNodes とかでも useLexicalComposerContext() とかが呼ばれてごにょごにょ更新したりしてるのかな

br_branchbr_branch

$wrapNodesImpl も見てみたけど特に参照してるようには見えないな。

SelectionRange#anchor をreplaceしてるけど、これで変更されるのかな

br_branchbr_branch

https://zenn.dev/link/comments/81abb1d42a8a1d

HeadingNodeの実装を見てわかってきたかも(確証はもてないけど)。
要はLexicalNodeのインタフェースとしてじゃなく、HeadingNode , QuoteNodeなど各クラスとして保持してる(TSの場合コンパイル後にはインタフェースの情報はなくなるため instanceof など使えなくなるが、それを防げる)ってことなのかな・・・?

br_branchbr_branch

以下の感じになった。

export type Klazz<T extends Test> = {
    new (...args: any[]): T;
} & Omit<Test, 'constructor'>;

export class Test {
    static getType(): string {
        return "test";
    }
}

export class Test2 extends Test {
    static getType(): string {
        return "test2";
    }
}

export class Test3 extends Test {
    static getType(): string {
        return "test3";
    }
}

export const tests: Klazz<Test>[] = [Test2, Test3];


const test = new Test2();
tests.forEach((t) => 
    if (test instanceof t) {
        // @ts-ignore
        console.log(t.getType());
    }
});
// test2 gのみ表示される

const tests2: Test[] = [new Test2(), new Test3()];
// 以下実行するとエラー発生
tests2.forEach((t) => 
    // @ts-ignore
    if (test instanceof t) {
        console.log(t.getType());
    }
});

なるほど、Klassを作成することで、型の配列を作成できるのか。
だから Serialization & DeserializationimportJson はstaticでメソッド定義してるのね。理解。

br_branchbr_branch

以下を実装することでカスタムタグが作れるみたい。
https://lexical.dev/docs/concepts/serialization
(Lexical -> HTMLのもあるけど、ひとまずJSONだけ)

  • exportJSON(): SerializedHeadingNode
  • importJSON(jsonNode: SerializedLexicalNode): LexicalNode

exportJSONの例

export type SerializedHeadingNode = Spread<
  {
    tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
    type: 'heading';
    version: 1;
  },
  SerializedElementNode
>;

exportJSON(): SerializedHeadingNode {
  return {
    ...super.exportJSON(),
    tag: this.getTag(),
    type: 'heading',
    version: 1,
  };
}

importJSONの例

static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
  const node = $createHeadingNode(serializedNode.tag);
  node.setFormat(serializedNode.format);
  node.setIndent(serializedNode.indent);
  node.setDirection(serializedNode.direction);
  return node;
}
br_branchbr_branch

$warpNodes は、今のLixicalNodeのブロックを更新する、という関数っぽい。
どんどん外側にラップしていくようなやり方ってあるのかな

br_branchbr_branch

というか、HeadingNode、 exportDOMを実装してないけどHTMLに出力したらどうなるんだろ。

br_branchbr_branch

やってみたら普通にタグが出力された。
デフォルトの動きがどこかで設定されてるっぽい。

まぁ使うことなさそうだからそういうものと思っておこう。

br_branchbr_branch

https://zenn.dev/stin/articles/lexical-rich-editor-trial より

Lexical では $ プレフィックス関数は特別な場所でしか呼び出せないようになっています。 editor.update() はそのひとつで、EditorState の更新を行えます。
(中略)
editorState.read() は editor.update() と同様に $ プレフィックス関数が使用できるスコープです。この中でステートを読み、 setBlockType に値を渡します。

なるほどねぇ

br_branchbr_branch

$isListNode の定義内容

export declare function $isListNode(node: LexicalNode | null | undefined): node is ListNode;

この node is ListNode の書き方をすると、IDE上ではそのブロック内では自動的にダウンキャストしてくれるみたい(そういう振る舞いしてる)。知らなかった

br_branchbr_branch

まだよくわかってないけど、 ListPlugin / CheckListPluginは 0.8.0 からタブの動きを明示的に指定しなきゃいけなくなったのかも。

v0.8.0 (2023-02-09)
このリリースでは、以下のようないくつかの変更点があります: -@lexical/list からindentListとoutdentListを削除しました。 -@lexical/reactからLexicalContentEditableタイプをリファクタし、HTMLDivElement属性をより正確に使えるようにしました。

本リリースでは、ノード置換にノード変換を適用する機能の追加、タブインデントのサイズの指定、共同編集のためのYJS更新元の追跡のサポートの改善などを行いました。

削除の際に既存の書式が反映されるようにする (#3867) Dominic Gannaway
RangeSelectionにスタイルプロパティを追加 (#3863) Dominic Gannaway
リストのインデントの簡略化 (#3809) EgonBolton
ContentEditableの型を更新 (#3580) John Flockton
formatプロパティを省略できるようにした (#3812) カリバシ
タブのインデントをカスタマイズできるようにする (#3802) John Flockton
ノード変換を元のノードだけでなく、オーバーライドするノードにも適用する (#3639) mizunoさん
feat: オリジンに基づいてyjsから更新タグを設定する (#3608) El-Hussein Abdelraouf

https://github.com/facebook/lexical/blob/main/CHANGELOG.md