📗

Tiptapコードリーディング:Text + 位置の数え方

に公開

前回
https://zenn.dev/karintou/articles/6733f12f974c79

今回はTextです。

ドキュメント (version:3x)
https://tiptap.dev/docs/editor/extensions/nodes/text

コード (version:3x)
https://github.com/ueberdosis/tiptap/blob/main/packages/extension-text/src/text.ts

基本

Parahraphの内側に入るテキストです。コードは数行しかないNodeオブジェクトですね。
group: 'inline'で、contentを受け取らないリーフノードになります。

終わり。

位置の数え方

これだけだと味気ないので、リッチテキストエディタにおける位置の数え方も調べてみましょう。

ProseMirrorのGuideに説明が書いてあります。

https://prosemirror.net/docs/guide/

基本の数え方とテキスト選択

Tiptap(ProseMirror)では、ドキュメントの位置を表すために2つの方法があります。
1つ目はDOMのようにツリー構造、2つ目はドキュメントに整数で位置を対応させる方法です。

多くの場合で2つ目の方法が便利ですが、このカウント規則は結構混乱します。

例えば公式に書いてある例だとこんな感じです

0   1 2 3 4    5
 <p> O n e </p>

5            6   7 8 9 10    11   12            13
 <blockquote> <p> T w o <img> </p> </blockquote>

例えば、Oneのnとeの間にキャレットを移動したいときは、setTextSelection(3)です。
pタグの前、blockquoteタグの後ろにも数字が割り振られていますが、規則はこのようになります。

  • ドキュメントの開始位置、つまり最初のコンテンツの直前の位置は0
  • リーフノードではないノード(pやh1など)への出入りは、1トークンとしてカウント
    • リーフノード(テキストなど)への出入りはカウントしない
  • テキストノード内の各文字は1つのトークンとしてカウント
  • コンテンツ (画像など) を許可しないリーフノードも、単一のトークンとしてカウント

最初と最後はドキュメントの開始位置・終了位置として数値が割り振られています。
ここでポイントなのが、キャレットの位置 => 数値は必ず1つに対応しますが、数値 => キャレットは複数に対応する可能性があります。

例えば、setTextSelectionで0と1を入力すると、どちらもOneの先頭に移動します。
仮にこれらが異なる位置として認識されるのであれば、0はpタグの外側に位置するはず。(pタグに背景色をつけるとわかりやすいかも)
この場合は0から2回右に移動した時にOとnの間に移動しますが、そうはならず0と1は同じ位置になります。

ブラウザのcontenteditableの挙動と、ProseMirror側の数値割り当てに違いがあるため、このような挙動になるのかと思われます。

NodeSelectionとは

この一見不要に見える数値は、1つの用途としてNodeSelectionがあります。
ProseMirrorには主にNodeSelectionとTextSelectionの2種類あり、NodeSelectionは名前の通りNode全体を選択できます。

紫の枠線で囲まれているところがNodeSelectionのイメージ。

これはノードごと編集したい時に便利です。選択したい要素の外側の数値でsetNodeSelectionを呼ぶと使えます。

例えば先頭のpタグをNodeSelectionするときは、1ではなく0を使います(1だとエラーになる)。blockquoteは5, その中のpは6です。

let node = $pos.nodeAfter!でノードを取得しているので、タグの外側じゃないとダメなのでしょう。
https://github.com/ProseMirror/prosemirror-state/blob/master/src/selection.ts#L325-L376

最後に

位置の割り当てあたりは結構あやふやだったので、いい勉強になリました。
次はもう少し複雑なノードを見てみよう。

Discussion