アプリケーションにRich Text Editorの導入を予定している際、どのライブラリを利用するか悩んでいませんか?Rich Text EditorといってもQuillのように少しの設定でRich Text Editorとしてすぐに利用できるものからTiptapのようなHeadlessタイプのものまでさまざまなライブラリがあります。Headlessタイプは実装に必要となる機能のみ提供を行い、一般的なRich Text Editorが持つツールバーが事前に準備されていないためUIを各自が作成する必要があります。Headlessタイプのものは設定が難しいのではと不安になるかもしれませんがドキュメントのサンプルコードを参考にすることでツールバーを作成することができます。本文書は数あるRich Text Editorの中でHeadlessタイプのTiptapの設定方法について説明を行っています。TipTapは特定のフレームワーク/UIライブラリに縛られないのでVanilla JavaScript, React, Vue, Svelteなどで利用することができます。本文書ではReactを利用して動作確認を行っていきます。

本ブログではこれまでEditor.js, Draft.js, Slate, ProseMirrorなどのEditorの記事を公開しているので他のEditorにも興味がある人はぜひ参考にしてください。

Tiptap Editorとは

TiptapはHeadless Editor FrameworkでEditorの他にも共同編集の行えるCollaborationやContent AIなどの機能を利用することができます。すべての機能がオープンソースでフリーというわけではなくCotent AIなどの機能は有償となります。

TiptapはRichText EditorのProseMirrorを元に作成(ProseMirrorのWrapper)されており、Extensionsをインストールすることで機能の拡張を行うことができます。例えばEditorの中で文字列をBold(太字)に変換したい場合にはBoldに対応したExtensionsをインストールする必要があります。Headless Editorのため機能が提供されているだけなのでUIは各自が設定する必要があります。

Tiptapを利用するためにはProseMirrorの知識が必須ではありませんがさまざまな場面で役に立ちます。Tiptapを利用する前にProseMirrorを知っておきたいという人は下記の公開済みの記事が参考になります。

動作確認環境の構築

Viteを利用して動作を行う環境を構築します。

Reactプロジェクトの作成

“npm create vite”コマンドを利用してプロジェクトの作成を行います。プロジェクト名はmy-tiptap-projectとしていますが任意の名前をつけることができます。ReactのTypeScript環境で動作確認を行うためオプションには”– –template react-ts”を設定しています。


 % npm create vite@latest my-tiptap-project -- --template react-ts

プロジェクトの作成が完了したらmy-tiptap-projectに移動して”npm install”コマンドを実行してJavaScriptのパッケージをインストールします。

デフォルトのスタイルを無効にするためsrcディレクトリのmani.tsxファイルでindex.cssファイルのimport文をコメントしておきます。


import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
// import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Tiptapライブラリのインストール

Tiptapを利用するために3つのパッケージのインストールを行います。@tiptap/pm(ProseMirrorライブラリ)はどのフレームワーク/UIでもインストールを行い、React用の場合は@tiptap/reactをインストールする必要があります。TiptapではEditorの中で文字列をBold(太字)にしたい場合には@tiptap/extension-boldのExtensionをインストールする必要がありますが個別にExtensionsをインストールする代わりに@tiptop/starter-kitをインストールすると一般的に利用される頻度の高いExtensionsが含まれています。もし利用したい機能がStarterKitに含まれていない場合は個別にExtensionsのインストールを行います。


 % npm install @tiptap/react @tiptap/pm @tiptap/starter-kit

@tiptop/starter-kitに含まれているExtensionsについてはドキュメントで確認することができます。見出し(Heading), 段落(Paragraph), 太字(Bold), イタリック(Italic), リスト(Ordered List, BulletList)などが含まれています。

はじめてのTiptap

TiptapはEditorなのでブラウザ上での文字の入力を行えることが必須機能です。ここではTiptapを利用してブラウザ上で文字が入力できるように設定を行い、もっとも基本的な機能である文字列をBold(太字)にするための設定を確認します。

ブラウザ上で文字列の入力

srcディレクトリにcomponentsディレクトリを作成してTiptap.tsxファイルを作成します。Tiptap.tsxファイルにはStarterKitを利用してTiptapを動作するために必要となる最低限のコードを記述します。Editor上に表示させる初期値となるcontentはHTMLで記述することができます。利用するExtentionsはextensions配列に記述します。


import { EditorProvider } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';

const extensions = [StarterKit];

const content = '<p>Hello World!</p>';

const Tiptap = () => {
  return <EditorProvider extensions={extensions} content={content} />;
};

export default Tiptap;

作成したTiptapコンポーネントをApp.tsxファイルでimportします。


import Tiptap from './components/Tiptap';

const App = () => {
  return (
    <div style={{ margin: '1em' }}>
      <h1>Tiptap Editor</h1>
      <Tiptap />
    </div>
  );
};

export default App;

“npm run dev”コマンドを実行して開発サーバを起動してブラウザからhttp://localhost:5173/にアクセスを行います。


% npm run dev

> my-tiptap-project@0.0.0 dev
> vite


  VITE v5.4.2  ready in 293 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

content変数に設定した”Hello World!”の文字列が表示されればTiptapは正常に動作しています。

初期画面
初期画面

画面を見ただけではEditorかどうかわかりませんが”Hello World!”にカーソルを合わせると太枠が表示されコンテンツの入力を行うことができます。まブラウザのデベロッパーツールを利用して要素を確認するとdiv要素にcontentEditable属性がtrueに設定されています。

文字列の入力とdiv要素へのcontenteditable属性の確認
文字列の入力とdiv要素へのcontenteditable属性の確認

contenteditable属性をtrueに設定することでブラウザ上で文字の入力が可能になります。contenteditable属性についてはもう少し知りたい人は公開済みの記事を参考にしてください。

starter-kitを利用しなかった場合

もしstarter-kitを利用しなかった場合は@tiptap/extension-document, @tiptap/extension-text, @tiptap/extension-paragraphの3つのExtensionsをインストールする必要があります。設定は下記のように行います。


import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import { EditorProvider } from '@tiptap/react';

const extensions = [Document, Text, Paragraph];

const content = '<p>Hello World!</p>';

const Tiptap = () => {
  return <EditorProvider extensions={extensions} content={content} />;
};

export default Tiptap;

Bold(太字)への変更

文字のスタイルをBold(太字)に変更したい場合にはmacOSでは”Command+B”(WindowsではCtrl+B)のキーボードショートカットが設定されているのでブラウザ上で文字列を選択して”Command+B”を実行すると太字になります。もう一度”Command+B”を実行すると元のスタイルに戻ります。

キーボードショートカットによるスタイルの変更
キーボードショートカットによるスタイルの変更

Bold(太字)やItalic(イタリック)、Heading(見出し)にもショートカットが存在します。その他のExtensionsのショートカットはドキュメントのhttps://tiptap.dev/docs/editor/core-concepts/keyboard-shortcutsに掲載されています。

またキーボードショートカットを利用しなくてもマークダウンの記法を利用しても文字をBold(太字)にすることができます。**Hello**と入力すると自動でBold(太字)に変更されます。# を利用すると見出しとして表示されます。

Tiptapの動作確認

ツールバーの追加

キーボードショートカットではなくボタンにより文字の装飾などのスタイルを変更する方法を確認していきます。ボタンはツールバーを作成してその中に追加していきます。エディターの入力エリアの上部にツールバーを設定するためにcomponentsディレクトリの下にToolbar.tsxファイルを作成します。


import { useCurrentEditor } from '@tiptap/react';

const Toolbar = () => {
  const { editor } = useCurrentEditor();

  if (!editor) return null;

  return (
    <div>
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        disabled={!editor.can().chain().focus().toggleBold().run()}
      >
        Bold
      </button>
    </div>
  );
};

export default Toolbar;

useCurrentEditor Hookを利用することでTiptapのEditorのインスタンスにアクセスすることができます。Editorインスタンスが持つメソッドを利用してスタイルの変更を行いますがExtensionsをインストールすることでEditorが実行できるメソッドが増えます。それぞれのExtentsionsがどのようなコマンド(メソッド)を持つかはドキュメントのExtensionsで確認することができます。

editor.can().chain().focus().toggleBold().run()は実行するとtrueかfalseが戻されます。テキストを選択している場合は太字にするか太字を解除するかという処理が行われますがテキストを選択していない場合に上記のメソッドを実行するとスタイルに影響は与えず、toggleBoldが実行できるかどうかチェックを行い実行できない場合はdisabledによりボタンを無効にします。

作成したToolbarコンポーネントはEditProviderのslotBeforeに指定することでEditorの入力エリアの上部にToolbarコンポーネントを表示することができます。


import { EditorProvider } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Toolbar from './Toolbar';

const extensions = [StarterKit];

const content = '<p>Hello World!</p>';

const Tiptap = () => {
  return (
    <EditorProvider
      slotBefore={<Toolbar />}
      extensions={extensions}
      content={content}
    />
  );
};

export default Tiptap;

ブラウザで確認すると下記のように表示され、文字列を選択して”Bold”ボタンをクリックすると文字列が太字となります。

Toolbarの表示
Toolbarの表示

BoldやItalicなどはstarter-kitに含まれているのでItalicを追加したい場合には新しいbuttonを追加してitalicに関するメソッドを設定で完了します。


import { useCurrentEditor } from '@tiptap/react';

const Toolbar = () => {
  const { editor } = useCurrentEditor();

  if (!editor) return null;

  return (
    <div>
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        disabled={!editor.can().chain().focus().toggleBold().run()}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        disabled={!editor.can().chain().focus().toggleItalic().run()}
      >
        Italic
      </button>
    </div>
  );
};

export default Toolbar;

追加したItalicボタンを利用してスタイルを変更することができます。

Italicボタンを追加後のど動作確認
Italicボタンを追加後のど動作確認

このように文字のスタイルの変更設定は簡単に行うことができます。

Extensionsのインストール

さらに文字列に下線をつけれるようにunderlineボタンを追加してドキュメントを参考にtoggleUnderlineメソッドを設定します。


import { useCurrentEditor } from '@tiptap/react';

const Toolbar = () => {
  const { editor } = useCurrentEditor();

  if (!editor) return null;

  return (
    <div>
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        disabled={!editor.can().chain().focus().toggleBold().run()}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        disabled={!editor.can().chain().focus().toggleItalic().run()}
      >
        Italic
      </button>
      <button
        onClick={() => editor.chain().focus().toggleUnderline().run()}
        disabled={!editor.can().chain().focus().toggleUnderline().run()}
      >
        Underline
      </button>
    </div>
  );
};

export default Toolbar;

StarerKit ExtensionにはUnderlineのExtensionは含まれていないため動作しません。StarterKitに含まれていないExtensionsを利用したい場合には追加でインストールする必要があります。


% npm install @tiptap/extension-underline

インストールすれば終わりではなくextentionsの配列にインストールしたUnderlineを追加する必要があります。


import { EditorProvider } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import Toolbar from './Toolbar';

const extensions = [StarterKit, Underline];

const content = '<p>Hello World!</p>';

const Tiptap = () => {
  return (
    <EditorProvider
      slotBefore={<Toolbar />}
      extensions={extensions}
      content={content}
    />
  );
};

export default Tiptap;

エラーは解消されUnderlineボタンを利用して文字に下線をつけることができます。

下線の表示
下線の表示

Extensionsの一部のみ説明を行いましたが、画像、リンクやテーブルの設定もExtensionsを追加することで簡単に機能を追加することができます。

入力した内容の確認

ブラウザ上で入力したコンテンツをHTMLで取得したい場合にはgetHTMLメソッドを利用することができます。JSONで取得したい場合にはgetJSONで取得することができます。


const html = editor.getHTML();

入力したコンテンツの保存

ブラウザのリロードをするとエディターに入力したコンテンツはリセットされてしまうので入力したコンテンツをブラウザのlocalStorageを利用して保持する方法を確認します。実際のアプリケーションではデータベースに保存することになります。

updateイベントの設定

Tiptapにはいくつかイベントが準備されているのでイベント一つupdateを利用します。updateはコンテンツに更新が行われるとイベントが発火されます。利用できるイベントについてはドキュメントのEvents in Tiptapから確認することができます。

updateイベントを設定するためにはonUpdateをEditorProviderに設定してイベントが発火された場合に実行する処理をhandleUpdate関数に記述します。handleUpdate関数ではeditor.getHTMLメソッドを利用して入力した内容をHTMLで取得しています。


import { EditorProvider } from '@tiptap/react';
import type { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import Toolbar from './Toolbar';

const extensions = [StarterKit, Underline];

const content = 'Hello World!';

const Tiptap = () => {

  const handleUpdate = ({ editor }: { editor: Editor }) => {
    const html = editor.getHTML();
    console.log(html);
  };

  return (
    <EditorProvider
      slotBefore={<Toolbar />}
      extensions={extensions}
      content={content}
      onUpdate={handleUpdate}
    />
  );
};

export default Tiptap;

ブラウザで動作確認すると文字を入力する度にupdateイベントが発火されてブラウザのコンソールに入力した内コンテンツのHTMLが表示されます。

localStorageへの保存

editorインスタンスのgetHTMLメソッドで入力した内容をHTMLで取得できたのでそのままlocalStorageに保存します。

保存した内容を取得するコードも追加します。


import { EditorProvider } from '@tiptap/react';
import type { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import Toolbar from './Toolbar';

const extensions = [StarterKit, Underline];

const Tiptap = () => {
  const content =
    localStorage.getItem('content') || '<p>Hello World!</p>';

  const handleUpdate = ({ editor }: { editor: Editor }) => {
    const html = editor.getHTML();
    localStorage.setItem('content', html);
  };

  return (
    <EditorProvider
      slotBefore={<Toolbar />}
      extensions={extensions}
      content={content}
      onUpdate={handleUpdate}
    />
  );
};

export default Tiptap;

ブラウザのデベロッパーツールからlocalStorageを確認するとupdateイベントによりコンテンツを更新する度に保存される内容が変わります。

localStorageへの保存の確認
localStorageへの保存の確認

localStorageの保存と取得ができればブラウザをリロードしても入力したコンテンツを保持することができます。

Rich Text Editorを利用する機能がある場合はぜひTiptapも候補に入れてみてください。