headless WYSIWYGエディタ「tiptap」がアツい
この記事は 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 とは
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 に対応しているようです。
3.TypeScript 対応
最近 Web サービスを開発する上でのデファクトスタンダードになってきた TypeScript。型なしの素 JS はもう触りたくないですよね(私はそう)。
旧来のエディタライブラリだと、歴史が長いが故に TS 非対応だったり一部の FW と相性が悪かったりするので、やはり公式 TS 対応は嬉しいですね。
その他
細かい機能ですがシンタックスハイライティングにも標準で対応しています。
lowlightと組み合わせるようです。
ハイライティングの CSS も自前で書くのはネックですね...
軽くハンズオン
1.Nextjs をセットアップ
❯ npx create-next-app hirame-admin-front --template typescript
そのままだとプロジェクトルートに pages
ディレクトリがあるので src
配下へ移動します。
2.エディタ本体の実装
ボタン
H1
, H2
のようなスタイル付けボタンの発火用に onclick を渡せるようにしておきます。
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 がつくので、そちらにもスタイルを当ててあげましょう。
.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;
}
エディタ本体
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 いらないと思う。多分。 )
.AppEditor {
margin: 0;
border: 1px solid #000;
padding: 5px;
>:hover,
>:focus,
>:focus-visible {
outline: none;
}
}
起動してみる。
nextjs を起動しましょう。
ここまでで大凡 30 分かからないくらいだと思います。
ここからは各々のシステム・サービスに合わせたエディタ設計ができるかなと思います!
ソースコードはこちら
最後に
本当は以前の記事 ↓ と組み合わせて k8s 環境の同時編集エディタを作ろうかと思っていたのですが、無事に時間がありませんでした。
Redis を用いて k8s 環境下でも WebSocket スケールアウトをする内容です。興味あればご一読ください。
12 月はあっという間に時間が流れていきます。皆さまアドベントカレンダーはぜひ計画的に書いてみてください。
参考記事
Discussion