Closed80

Lexical Playgroundの中を覗く

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism


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

hajimismhajimism

そういえば下の方にtree構造が

embedとか入れたら単純なmdでデータ永続化するのは難しそうな気もするけど...
例えばFigmaだったら ![figma](figma link)とかでいいのかな。

hajimismhajimism

特に気になるのはここらへんかな〜

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

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

hajimismhajimism


ふつうにこれで対処した。themeにて。

    listitem: "[&:has(ul)]:list-none",
hajimismhajimism

olも対処するとこうか

[&:has(:is(ol,ul))]:list-none

ネストしたら白丸にするとかabcにするとかしようと思ったら複雑そうだな

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism
hajimismhajimism

ImageNodeがDecoratorNodeを継承して定義されていて、その中のdecorateメソッドでImageComponentが使われている。
https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageNode.tsx#L205-L222

ImagePluginの方で、コマンドの登録を行って
https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ImagesPlugin/index.tsx#L220-L232

DragDropPasteなどでコマンドが呼ばれているって寸法かな
https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/DragDropPastePlugin/index.ts#L25-L51

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

Imageが難しいので簡単そうなやついこーおもてCollapsible(toggle)を見に行った。
ふつうにdetails要素を返しているっぽい。
https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/CollapsiblePlugin/CollapsibleContainerNode.ts#L55-L66

hajimismhajimism

stylingのやり方もdom生成時にclassNameつけてCSS読み込んでいるだけなので割と簡単に導入できそう

hajimismhajimism
import LexicalClickableLinkPlugin from "@lexical/react/LexicalClickableLinkPlugin";

こいつを入れたらリンクがクリックできるようになった

hajimismhajimism

MentionsPluginを見てる。本体はLexicalTypeaheadMenuPluginとかいうやつだった。
https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/MentionsPlugin/index.tsx#L687-L721

hajimismhajimism

ざっくりいうと、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
      }

そいつが発動するのはtriggerFnQueryMatchを返したときらしい。triggerFnにはユーザーの入力が全部細切れに渡されているってことなのかな。

function getPossibleQueryMatch(text: string): QueryMatch | null {
hajimismhajimism

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

hajimismhajimism
hajimismhajimism

寄り道にはなるけどこのuseModalの実装が汎用的でおもしろい
https://github.com/facebook/lexical/blob/643566ddfd/packages/lexical-playground/src/hooks/useModal.tsx

menuRenderFnでも見かけたReactDOM.createPortal()初めてみたけどめちゃ重要だな。Renderの場所を指定できるらしい。モーダルなど、親要素のスタイリングに縛られたくないものをbody直下にRenderしたいときとかに便利。
https://react.dev/reference/react-dom/createPortal#rendering-a-modal-dialog-with-a-portal

実際にLexicalのModalも、どこから呼ばれてもbody直下にRenderされるようにしてる。
https://github.com/facebook/lexical/blob/643566ddfd/packages/lexical-playground/src/ui/Modal.tsx#L86-L106

hajimismhajimism

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

hajimismhajimism

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

hajimismhajimism

一応FigmaPluginも覗いておしまいにするか〜。
本体の実装はNodeの登録だけになっててシンプル。
https://github.com/facebook/lexical/blob/643566ddfd/packages/lexical-playground/src/plugins/FigmaPlugin/index.tsx
問題はNodeですよね〜

hajimismhajimism

今後の展望:色々自前で実装してみる

  • TogglePlugin
  • SlashManuPlugin
  • EmojiShortcutPlugin
  • ImagePlugin(w/ Vercel Blob)
このスクラップは2023/05/04にクローズされました