Lexical Playgroundの中を覗く

公式ドキュメントを読んだりちょっと触ってみたりした。仕組みの概要は掴めたしちょっと触っただけで大体はいい感じになることがわかったけれど、イマイチうまくいかない部分がある。しかし公式ドキュメントはまだ充実してない。公式Playgroundがドキュメント代わりに用意されているので、そちらを読んでいく。

まずはPlaygroundそのものを触って、どんな機能があるか確かめていく。

@-mention

hashtag
ここらへんはなんとなく把握してた

Selectionと同時にメニューが出る

speech to text
Dragonなんとかだよね。

Block Editorになってるな

Linkはこんな感じ

画像の貼り付けも簡単

[]
でcheckboxでるわ

textはcolorもfont-familyもsizeもいじれる。alignも変えられる。

「```」でcode block出てくるわけではないな。

insertめっちゃあるな

おーFigma埋めれる。これは作りたいものに欲しいやつかも。

Horizontal Ruleは横線だけども---
で出現するわけではない

そういえばindentできてるな

セル結合など見た目の調整がいろいろできるテーブル

Experimentalなtableもあるけどこっちは期待通りに動かない

Sticky note。すごいけどいらん。

toggle。これは嬉しいね。


数式も

Exicaldrawすごいな!任意のお絵かきが画像として貼り付けられる

そういえば下の方にtree構造が
embedとか入れたら単純なmdでデータ永続化するのは難しそうな気もするけど...
例えばFigmaだったら 
とかでいいのかな。

そういえばZennは@[service](link)
だな

特に気になるのはここらへんかな〜
- list系の挙動
- imageの処理
- @-mention(
/
でコマンドパレット出現みたいなことができそうだから) - embeded Figma
- toggle
- Exicaldraw

見ていくかー

とりま本体
ざっと気になるのはここらへんかな?
- DragDropPaste
- MentionsPlugin
- ImagesPlugin
- FigmaPlugin
- LexicalClickableLinkPlugin
- TabIndentationPlugin
- CollapsiblePlugin

公式提供っぽいのでTabIndentationPluginを手元で試したらこうなった。これはふつうにstylingの問題っぽい。

該当スタイルはここらへん。

list-insideを追加したら難しいことになった...

ふつうにこれで対処した。themeにて。
listitem: "[&:has(ul)]:list-none",

olも対処するとこうか
[&:has(:is(ol,ul))]:list-none
ネストしたら白丸にするとかabcにするとかしようと思ったら複雑そうだな

ためしにNotion覗いたらこれさえもdivだったしなんならulとかliとか使わずに全部divだった。完全にBlockをfrom scratchで定義しているんだな。これは真似できんわ。

とりあえずいい感じになったからいいや

こうなるとListMaxIndentLevelPluginも欲しくなるのでPlaygroundのやつをコピペする

さあさあ、画像見ていきますか

そもそもnodeから定義しなくてはいけない

ほんでもってImageそのものがこれ

ImageNodeがDecoratorNodeを継承して定義されていて、その中のdecorateメソッドでImageComponentが使われている。
ImagePluginの方で、コマンドの登録を行って
DragDropPasteなどでコマンドが呼ばれているって寸法かな

ちなみにPasteなどのイベントはここらへんでコマンドとして登録されているらしい。本体。

DRAG_DROP_PASTEはそこらへん色々まとめてるのか、しかもこれはrich-textから出てるし。複雑だな〜。

とりあえず大まかな構造は理解した...
肝心のimage urlどこで管理しているの問題がまだ

ここでマジックが起きているように見える?何かしらでsrcが作られているはずなんだけど...

うーんわからん、なんでこのpayloadにもろもろの情報が入っている?
contenteditableにimageをpasteしたときにはそうなる?

あー、なんか、よしなにやってくれるな
<div contentEditable>Hello</div>
Paste後

base64でエンコードされてるね

なのでdecoratorの中でこいつをどっかのstrageにuploadする処理を書けば良いのかな。decoratorが返すのはあくまでも
にしておいて、この形式が来たら画像として表示するようなTransformerを書く。

(だ、だり〜)

そういえばLexicalさん、decoraterでJSX.Elementを返せばそのまま描画してくれるんだな。

playgroudの方では.lexical
っていう独自のファイルでimport/exportができるようになってて、その際に画像も普通に保存されてるな。
これをサーバーに保存しても良いのかな...。
でもtext fileの方が圧倒的にサイズ小さいよなー

エンコードというかFileReader使ってたの普通にここにあったわ。
ImagePluginからの責務はあくまでsrcを受け取ってImageNodeを作るところで、fileを変換するのはDragDropPasteの責務らしい。ここでuploadすれば大丈夫。

こいつは今度Vercel Blobと併せて試す

Imageが難しいので簡単そうなやついこーおもてCollapsible(toggle)を見に行った。
ふつうにdetails要素を返しているっぽい。

import LexicalClickableLinkPlugin from "@lexical/react/LexicalClickableLinkPlugin";
こいつを入れたらリンクがクリックできるようになった

MentionsPluginを見てる。本体はLexicalTypeaheadMenuPlugin
とかいうやつだった。

そいつはここにあるんだけど、結構ごついからあまり深追いしないほうが良い気がする。

かわりにinterfaceとMentionsPluginでの使い方を見て分析する。

ざっくりいうと、onQueryChange
にsetStateを渡せば適当なStateで入力を管理できて、
const [queryString, setQueryString] = useState<string | null>(null);
...
onQueryChange={setQueryString}
そいつをもとに計算したoptions
を渡しつつ、menuRenderFn
で表示の方法を制御できるらしい。
options={options}
menuRenderFn={(
anchorElementRef,
{selectedIndex, selectOptionAndCleanUp, setHighlightedIndex},
) =>
anchorElementRef.current && results.length
? ReactDOM.createPortal(
<div className="typeahead-popover mentions-menu">
<ul>
{options.map((option, i: number) => (
<MentionsTypeaheadMenuItem
index={i}
isSelected={selectedIndex === i}
onClick={() => {
setHighlightedIndex(i);
selectOptionAndCleanUp(option);
}}
onMouseEnter={() => {
setHighlightedIndex(i);
}}
key={option.key}
option={option}
/>
))}
</ul>
</div>,
anchorElementRef.current,
)
: null
}
そいつが発動するのはtriggerFn
がQueryMatch
を返したときらしい。triggerFnにはユーザーの入力が全部細切れに渡されているってことなのかな。
function getPossibleQueryMatch(text: string): QueryMatch | null {

コード読んでて気がついたけれど、/
でmenuでるのもう実装されているな。
ほんでTriggerFnを返すutil的なものが提供されているっぽい。

useBasicTypeaheadTriggerMatch
で検索かけたらEmoji入力が実装されているのも見つけたわ。

Emojiもいいね、あとで覗こう

ぼくが本当に実装したいものはこっちにあった

さっきと構造は大体同じなんだけど、一部メニューが選択されたときにモーダルが出てくるようにしてあるのでModalの状態管理もここで行っている。

ただのオブジェクト生成したいときにclass多用しているな。同時にInterfaceにもなって便利なのか。

いやすごいな、dynamicにoption生成もしてる

option
本体としては、queryString
があるときは引っかかるやつを、そうでないときはbaseOption
全部を返すようにしている。

こういう用途だとメモ化は重要だろうな

寄り道にはなるけどこのuseModal
の実装が汎用的でおもしろい
menuRenderFnでも見かけたReactDOM.createPortal()
初めてみたけどめちゃ重要だな。Renderの場所を指定できるらしい。モーダルなど、親要素のスタイリングに縛られたくないものをbody直下にRenderしたいときとかに便利。
実際にLexicalのModalも、どこから呼ばれてもbody直下にRenderされるようにしてる。

うわ〜、Reactの新しい公式ドキュメントじっくり読むのもやりたくなってきた〜

とりまComponentPickerMenuPlugin
は単調だけど見た目は派手なので自前実装してみるの良さそう!

一応FigmaPluginも覗いておしまいにするか〜。
本体の実装はNodeの登録だけになっててシンプル。
問題はNodeですよね〜

DnDはここみたい。今は深追いしないけどメモ。

今後の展望:色々自前で実装してみる
- TogglePlugin
- SlashManuPlugin
- EmojiShortcutPlugin
- ImagePlugin(w/ Vercel Blob)