Lexical関連のお勉強
ドキュメント
丁寧に書かれてるから参考に
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 に限定する意図があります)。
そんなことできるんだ…実装
よくわからない(´・ω・`)
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型は、型パラメーターで指定したクラスのインスタンスを作成するための型を表現することができます。これは、クラスを動的に作成する必要がある場合に有用です。例えば、プログラムの実行時にクラスを動的に作成して、それをインスタンス化する必要がある場合などに使用されます。
ちょっとわかったようなわからないような。
as const
こんな書き方あるのも初めて知った。
(TypeScriptの勉強になりつつあるな…)
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
APIリファレンスに意味がかかれてなくてつらたん。
関数名で察しろってことかな
一見すると editor.update 内でeditorの参照がないのに、アップデートされるのはどういう仕組だろ?
$wrapNodes
とかでも useLexicalComposerContext() とかが呼ばれてごにょごにょ更新したりしてるのかな
$wrapNodesのソース
$wrapNodesImpl
も見てみたけど特に参照してるようには見えないな。
SelectionRange#anchor をreplaceしてるけど、これで変更されるのかな
$createHeadingNodesのソース
$applyNodeReplacement
HeadingNode
HeadingNodeの実装を見てわかってきたかも(確証はもてないけど)。
要はLexicalNodeのインタフェースとしてじゃなく、HeadingNode , QuoteNodeなど各クラスとして保持してる(TSの場合コンパイル後にはインタフェースの情報はなくなるため instanceof など使えなくなるが、それを防げる)ってことなのかな・・・?
以下の感じになった。
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 & Deserialization の importJson
はstaticでメソッド定義してるのね。理解。
以下を実装することでカスタムタグが作れるみたい。
(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;
}
$warpNodes は、今のLixicalNodeのブロックを更新する、という関数っぽい。
どんどん外側にラップしていくようなやり方ってあるのかな
簡単にはできなさそうだから、必要になったら調べる。
ヒントになりそうなの
($handleOutdentで、今のnodeを削除して上のに置き換えるみたいな処理してるような気がする。ちゃんと読んでないけど)
というか、HeadingNode、 exportDOMを実装してないけどHTMLに出力したらどうなるんだろ。
やってみたら普通にタグが出力された。
デフォルトの動きがどこかで設定されてるっぽい。
まぁ使うことなさそうだからそういうものと思っておこう。
ここにも Lexical の基本が載ってる。ありがたい。
https://zenn.dev/stin/articles/lexical-rich-editor-trial より
Lexical では $ プレフィックス関数は特別な場所でしか呼び出せないようになっています。 editor.update() はそのひとつで、EditorState の更新を行えます。
(中略)
editorState.read() は editor.update() と同様に $ プレフィックス関数が使用できるスコープです。この中でステートを読み、 setBlockType に値を渡します。
なるほどねぇ
$isListNode の定義内容
export declare function $isListNode(node: LexicalNode | null | undefined): node is ListNode;
この node is ListNode
の書き方をすると、IDE上ではそのブロック内では自動的にダウンキャストしてくれるみたい(そういう振る舞いしてる)。知らなかった
まだよくわかってないけど、 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://playground.lexical.dev/ のソースコードを見るほうが学習になりそう
ソース