🐢

BlockNoteで文字数制限しようとしたら思ったより深くまで潜る必要があったので、せっかくなのでまとめてみる

2024/05/23に公開

BlockNoteの紹介

そもそも BlockNote とは、ブロックベースのリッチテキストエディタのnpmライブラリです。
国内ではあまり記事とかも書かれておらず、これから多く使われていくと嬉しいなと思っています。BlockNoteとても素敵なライブラリなので是非皆さん使ってみてください。

https://www.blocknotejs.org/

initial release が Mar 25, 2023 でした。

GitHubのinitial release のスクリーンショット

技術選定の話とかその他カスタマイズの話などの話は今回はスコープ外です。

手っ取り早く解決策だけ知りたい方は解決編からどうぞ

文字数を制限したい

BlockNoteをアプリケーションで使用するうえで、ユーザが入力する文字数の上限を設定する必要がありました。
入力された文章を無限に保存するわけにもいきませんし、少ない文章で使いたい事もあるかもしれません。入力文字数の上限を設定するのはとても大切です。

シンプルに考えました。

if (textLength >= textLimit) {
  // 文字数の上限を超えているので、入力をキャンセル何かしらを行う
  return;
}

こんな感じだろうと。

制限するには数えたい

文字数を制限するということは、文字数を数えるという事です。いや当たり前ですけど。
上のコードで言う所の text_length を数えなければいけません。

数えるのはループすれば出来そう

BlockNoteはブロックベースのリッチテキストエディタなので、平文で文字列が格納されているわけではありません。

たとえばこんなテキストがあるとします。

デモ的な文章が入力されているスクリーンショット

この程度の文章でも内部的にはこんな感じのJSONが生成されます

長いので折りたたんだJSON
[
  {
    "id": "a2c57d5f-b949-4643-82bb-dfa277c7efb0",
    "type": "paragraph",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left"
    },
    "content": [
      {
        "type": "text",
        "text": "デモへようこそ!",
        "styles": {}
      }
    ],
    "children": []
  },
  {
    "id": "a866c2ea-628b-4d48-bcc7-7a607d44b9b1",
    "type": "paragraph",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left"
    },
    "content": [],
    "children": []
  },
  {
    "id": "95cc8e80-68b4-4090-a068-d56a970a3e56",
    "type": "paragraph",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left"
    },
    "content": [
      {
        "type": "text",
        "text": "ブロック一覧:",
        "styles": {
          "bold": true
        }
      }
    ],
    "children": []
  },
  {
    "id": "e0d9b8c5-310c-45ee-bac3-7ba253761fb8",
    "type": "paragraph",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left"
    },
    "content": [
      {
        "type": "text",
        "text": "標準テキスト",
        "styles": {}
      }
    ],
    "children": []
  },
  {
    "id": "894c634c-afe2-40ce-a93b-0ba44dfbe91c",
    "type": "heading",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left",
      "level": 1
    },
    "content": [
      {
        "type": "text",
        "text": "見出し1",
        "styles": {}
      }
    ],
    "children": []
  },
  {
    "id": "835b3f9e-290b-4c9f-9272-0248ba2a9079",
    "type": "heading",
    "props": {
      "textColor": "default",
      "backgroundColor": "default",
      "textAlignment": "left",
      "level": 2
    },
    "content": [
      {
        "type": "text",
        "text": "見出し2",
        "styles": {}
      }
    ],
    "children": []
  },
]

とはいえよく見ると中身は jsonData[index]["content"][index]["text"] にありそうです。これを数えれば合計の文字数は取得出来そう。

const fullText = jsonData
    .flatMap((block) => block.content.map((c) => c.text))
    .join("");
  return fullText.length;

実際には使っていないのですが、こんな感じの処理で取得出来そうです。

文字数を超えたら入力を制限したい

文字数を取得するのは出来そうだったので、文字数が制限を超えたら入力をさせないような処理を入れようと思います。

ここはあまり調べずに思いつきで「 onChangereturn false すればいけないかなー」とか思っていました。試してみました。

onChangeではできなかった

まあ出来ませんでした。それはそうですよねって感じがします。この場合の onChange は変更された後のコールバックで、副作用というか事後の処理です。

こんな感じでやりましたが、特に何も起きません

  const handleChange = () => {
    return false;
  };
  return <BlockNoteView editor={editor} onChange={handleChange} />;

念の為 onChangeコードを見てみましたが、 () => void なので返り値見てないですね。はい。

変更の処理に割り込んで介入できるのはどこだろう

onChangeのコードを調べて「ここじゃない」ことに気づき、ついコードの海に潜り始めてしまいました
ダイビングのマスクをして酸素ボンベを加えている水中での自撮り
これは海に潜っている時の写真

早速ですが、onChangeから、実際にテキストが編集された時に介入できるポイント探してみます

https://github.com/TypeCellOS/BlockNote/blob/v0.13.2/packages/core/src/editor/BlockNoteEditor.ts#L994-L1006

ん? this._tiptapEditor.on("update", cb);onChange に渡されたコールバック関数はthis._tiptapEditorupdate イベントに渡されています。

Tiptap?

_tiptapEditor ってもしかしてヘッドレスなリッチテキストエディタを開発系Saasとして提供しているTiptapの事か・・・?

https://tiptap.dev/

更に潜ってみよう

https://github.com/TypeCellOS/BlockNote/blob/v0.13.2/packages/core/src/editor/BlockNoteEditor.ts#L347-L352

ふむ、 BlockNoteTipTapEditor か。これはいよいよ

https://github.com/TypeCellOS/BlockNote/blob/v0.13.2/packages/core/src/editor/BlockNoteTipTapEditor.ts#L23

おっと extends TiptapEditor

https://github.com/TypeCellOS/BlockNote/blob/v0.13.2/packages/core/src/editor/BlockNoteTipTapEditor.ts#L3

そうですよね、 @tiptap/core ですよね。

https://github.com/TypeCellOS/BlockNote/blob/v0.13.2/packages/core/package.json#L56-L76

めっちゃ dependencies しとる

ということでBlockNoteはTiptapが公開している @tiptap ライブラリにエディタ部分は依存していました。

Tiptapでできるんじゃないか?

https://github.com/ueberdosis/tiptap/tree/main

Tiptapが公開しているコードに依存しているようです。ヘッドレスリッチテキストエディタを提供しており、UIを自在にハンドリングできるツールです。こんな豪華なツールをMITライセンスで公開されているのは助かりますね。

実際に文字列を受け取って変数に格納したり、フォーマットを定義していたりするのはtiptap側のようです。BlocNoteはモダンなブロックベースのエディタUIを定義している感じです。

ベースの機能っぽい文字数を数えたり、入力を制限する機能は tiptap でなんとかする必要がありそうです。

では tiptapに潜ってみましょう

ダイビングスーツを着て海の中で手を広げてポーズを取っている写真
これも海に潜っている時の写真

まずは公式のリファレンスから読んでみましたが使い方は理解できますが、文字数を数えている機能を見つけられず。(後に見つけられます)

コードを探してみます

ありそう

我々には検索という文明の利器があります。やってみましょう

limit length でリポジトリを検索してみます

https://github.com/search?q=repo%3Aueberdosis%2Ftiptap limit length&type=code

docs/api/extensions/character-count.md というファイルがヒットしました。 character-count なのできっと文字数が数えられるはずです。

https://github.com/ueberdosis/tiptap/blob/ef7d195311746983bbe17b8f87c64a0be81b7ccd/docs/api/extensions/character-count.md?plain=1#L2

数が数えられる上に上限の文字数も設定出来そうです。

標準の機能ではなく拡張機能として提供されていました。想定ユースケースが長文を基本としているのだと思います。

関連するドキュメントなどを集めて目を通しておきます

tiptapのextentionはどうblocknoteに使うのか

ここで、BlockNoteに戻ってきます。私はそもそもBlockNoteで文字数を数えて制限したいだけだったはずです。

tiptapのExtentionにCharacterCountというものが提供されている事がわかりましたが、それはどのように使えるのでしょう。BlockNoteで。

はじめの頃に _tiptapEditor という変数がありました。その辺りを見てみましょう。

https://github.com/TypeCellOS/BlockNote/blob/0f495e25c65cb45f3cd852f0071b6faeb4921bec/packages/core/src/editor/BlockNoteEditor.ts#L347-L353

tiptapOptions という値が渡されてインスタンス化されています。

https://github.com/TypeCellOS/BlockNote/blob/0f495e25c65cb45f3cd852f0071b6faeb4921bec/packages/core/src/editor/BlockNoteEditor.ts#L324-L345

extentions というのがいました。 newOptions._tiptapOptions?.extensions という変数に入っているか、 extensions に入っていれば動くようです。

extensions はBlockNoteで用意されているものなので追加は出来なさそうです。多分。( _tiptapOptions で出来そうだったのであまり見てない)

長くなりましたが、解決はこちら

パッケージをインストールします

npm i @tiptap/extension-character-count

useCreateBlockNote に渡せば文字数を制限できます。

import CharacterCount from '@tiptap/extension-character-count';

// ...

const editor = useCreateBlockNote({
  _tiptapOptions: {
    extensions: [
      CharacterCount.configure({
        limit,
      }),
    ],
  },
});

実際に動いているサンプルはこちらから

結論

ダイビングは非日常で楽しいので皆さんオススメです

ウミガメかわいい
ウミガメかわいい

Discussion