📝

headless WYSIWYGエディタ「tiptap」がアツい

2021/12/16に公開

この記事は GAOGAO Advent Calendar 2021 ことしも GAOGAO まつりです 16 日目の記事です。

はじめに

こんにちは、up-tri です。
普段仕事では EC システムの開発運用をしていて縁が無いですが、今回 TypeScript 製のリッチテキストエディタをご紹介&軽くハンズオンしていきます。

WYSIWYG (読:ウィジウィグ) エディタとは

WYSIWYG とは、What You See Is What You Get の頭文字をとった略語です。
日本語に直すと「見たままが得られる」となるようです。

「WYSIWYG エディタ」とは、ディスプレイ上での編集画面がアウトプット(Web ページや印刷結果)と同じように表示されるエディタを指します。
身近な例ですと Microsoft Word がそれに該当します。

また、WordPress をはじめとする CMS が世間の Web サイトを多く占める現在では、WordPress の記事作成画面も広い意味で WYSIWYG エディタと呼べると思います。

Web システムの WYSIWYG エディタ

Web システム、とくに Web ページ上での WYSIWYG エディタのひとつにTinyMCEがあります。

TinyMCE は WordPress が v5.0 になって Gutenberg を導入するまで標準のエディタとしてバンドルもされていました。それも相まって JavaScript 製 WYSIWYG エディタは長らく TinyMCE の一強だったのです。
もちろん TinyMCE は Vuejs や React、Angular といったモダンフレームワークが Web フロント界を牛耳るよりも前から存在していましたが、それ故 UI とロジックが密結合していたりモダンフレームワークとの相性も完璧ではありません。

そのような背景から、近年後発の WYSIWYG エディタが色々と登場していますが、中でも異色なのが tiptapです。
(ここまで前置き)

tiptap とは

https://tiptap.dev/

https://github.com/ueberdosis/tiptap

MIT ライセンス下で頒布されており、現在v2.0 beta版です。
installationにもあるとおり、Vue や React だけでなく、また Svelte といった最新のフレームワークでの利用も想定されています。
(もちろん従来通り CDN 経由のべた書きでも使えます!)

tiptap くんの長所

1.headless

tiptap の最大の長所は、タイトルにもあるとおりheadlessという点です。
すなわち WYSIWYG エディタのロジックだけに集中していることになります。

従来のエディタには大体標準のスタイルが当て込んであり、ツールボタン群だけでなく View 部分のカスタマイズにコツが必要だったりしました。
tiptap は本当に UI 実装が無いので イチからボタン等を作り込むのは面倒ですが 導入システムに合った UI/UX を提供できます。

2.リアルタイム同時編集対応

個人的に結構すごいなと思っているのですが、tiptap はデフォルトで(MIT ライセンス下の OSS で)複数人での同時編集に対応しています。
前置きで登場した TinyMCE にも機能としては存在するのですが、ライセンス式&専用サービスを経由して利用する必要があり、費用面と柔軟さで課題がありました。

現在 WebRTC と WebSocket に対応しているようです。
https://tiptap.dev/guide/collaborative-editing

3.TypeScript 対応

最近 Web サービスを開発する上でのデファクトスタンダードになってきた TypeScript。型なしの素 JS はもう触りたくないですよね(私はそう)。
旧来のエディタライブラリだと、歴史が長いが故に TS 非対応だったり一部の FW と相性が悪かったりするので、やはり公式 TS 対応は嬉しいですね。

その他

細かい機能ですがシンタックスハイライティングにも標準で対応しています。
lowlightと組み合わせるようです。
ハイライティングの CSS も自前で書くのはネックですね...

軽くハンズオン

1.Nextjs をセットアップ

❯ npx create-next-app hirame-admin-front --template typescript

そのままだとプロジェクトルートに pages ディレクトリがあるので src 配下へ移動します。

https://nextjs.org/docs/advanced-features/src-directory

2.エディタ本体の実装

ボタン

H1 , H2 のようなスタイル付けボタンの発火用に onclick を渡せるようにしておきます。

src/components/organisms/AppEditorButton/index.tsx
import React, { MouseEventHandler } from "react";
import styles from "./style.module.scss";

export type AppEditorButtonProps = {
  isActive: boolean;
  onClick?: MouseEventHandler<HTMLButtonElement>;
};

export const AppEditorButton: React.FC<AppEditorButtonProps> = ({
  isActive,
  onClick,
  children,
}) => {
  const className = [styles.AppEditorButton];
  if (isActive) {
    className.push(styles["AppEditorButton--active"]);
  }

  return (
    <button onClick={onClick} className={className.join(" ")}>
      {children}
    </button>
  );
};

効果 ON 状態のボタンには --active suffix がつくので、そちらにもスタイルを当ててあげましょう。

src/components/organisms/AppEditorButton/style.module.scss
.AppEditorButton {
  display: inline-block;
  margin: 0 3px;
  border: 1px solid #555;
  border-radius: 2px;
  padding: 1px 10px;
  background-color: #fff;
  color: #000;
  cursor: pointer;

  &:hover {
    background-color: #555;
    color: #fff;
  }
}

.AppEditorButton--active {
  background-color: #555;
  color: #fff;
}

エディタ本体

src/components/organisms/AppEditor/index.tsx
import { EditorContent, useEditor } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { AppEditorButton } from "../AppEditorButton"
import styles from "./style.module.scss"

export const AppEditor = () => {
  const editor = useEditor({
    extensions: [
      StarterKit
    ],
    content: "<p>Hello!</p>"
  })
  if (!editor) {
    return null
  }

  return (
    <>
      <AppEditorButton
        onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
        isActive={editor.isActive("heading", { level: 1 })}
      >H1</AppEditorButton>
      <AppEditorButton
        onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
        isActive={editor.isActive("heading", { level: 1 })}
      >H2</AppEditorButton>
      <AppEditorButton
        onClick={() => editor.chain().focus().toggleBold().run()}
        isActive={editor.isActive("bold")}
      >B</AppEditorButton>
      <AppEditorButton
        onClick={() => editor.chain().focus().toggleItalic().run()}
        isActive={editor.isActive("italic")}
      >I</AppEditorButton>
      <EditorContent className={styles.AppEditor} editor={editor} />
    </>
  )
}

こちらにも最小限のスタイルを付けます。
エディタ DOM の 1 つ内側に実際のコンテンツ入力エリアが生成されますが、focus系を無効化しておくのがコツです。
a11y の観点で outline: none; がわりと忌避されますが、エディタ入力枠には outline いらないと思う。多分。 )

src/components/organisms/AppEditor/style.module.scss
.AppEditor {
  margin: 0;
  border: 1px solid #000;
  padding: 5px;

  >:hover,
  >:focus,
  >:focus-visible {
    outline: none;
  }
}

起動してみる。

nextjs を起動しましょう。

起動イメージ

ここまでで大凡 30 分かからないくらいだと思います。
ここからは各々のシステム・サービスに合わせたエディタ設計ができるかなと思います!

ソースコードはこちら

https://github.com/up-tri/hirame-admin-front/tree/e8972a69392f96e1fdadc39fb2836ff8815949e0

最後に

本当は以前の記事 ↓ と組み合わせて k8s 環境の同時編集エディタを作ろうかと思っていたのですが、無事に時間がありませんでした。
Redis を用いて k8s 環境下でも WebSocket スケールアウトをする内容です。興味あればご一読ください。

https://zenn.dev/up_tri/articles/730e56443d1893

12 月はあっという間に時間が流れていきます。皆さまアドベントカレンダーはぜひ計画的に書いてみてください。

参考記事

https://www.web-meister.jp/guide/glossary/glossary_a/wysiwyg_editor.html

Discussion