
はじめに
ゆかっしゅです。
2021年に事務職からデザイナー/コーダーにジョブチェンジをし、現在はフロントエンドエンジニアにスキルアップするべく、モダンフロントエンドを学習しています。
ReactやTypeScriptの基礎学習は終了したのに、なかなか0から自分だけでアプリを作れないので手を動かすべく「Reactアプリ100本ノック」 に挑戦してみようと思います。
Reactアプリ100本ノックルール
- 主要なライブラリやフレームワークはReactである必要がありますが、その他のツールやライブラリ(例: Redux, Next.js, Styled Componentsなど)を組み合わせて使用することは自由
- TypeScriptを利用する
- 要件をみたせばデザインなどは自由
09. Memo
問題
ユーザーが思いついたことを迅速かつ効率的に記録し、整理するためのツールを提供します。リッチテキストエディタは、メモをフォーマットし、より視覚的かつ整理された形で情報を保存することを可能にします。
達成条件
- メモを作成できる
- メモを削除できる
- メモを選択すると選択されたメモの内容がエディタに表示される
実際に解いてみた
利用技術
React(19.0.0)
TypeScript(5.0)
Next.js(15.3.1)
Tailwindcss(4.0)
jotai(2.12.3)
tiptap(2.12.0)
Vercel
解答時間 4時間半
もうちょっとブロックパーツ部分のスタイル作りこみたかったんですけど、時間がかかりすぎちゃったのでやめました。
まず使うリッチテキストエディタライブラリの選定から始めました。最初は学習コスト少なそうな「Quill」を使おうと色々調べてたのですが、なんと React v19に対応していない ことがわかり、選定しなおしへ…
結局「tiptap」を使用しました。キャッチアップに時間がかかりました…
tiptapのドキュメントは↓こちら↓(サンプルコードもあります)
今後ブログとかに応用できそうだなぁと思ったので、備忘録的に「tiptap」の使い方をまとめたいと思います。
※tailwindcssと併用するときの注意点もあります。
リンク
【備忘録】Tiptap
準備
まずはインストールします。
Tiptapにはいろいろなキット(?)があり、使いたいものをインストールするような形です。
今回のようなBlog風のエディタを作成するには以下をインストールしました。
- @tiptap/react:React用のTiptapの公式パッケージ
- @tiptap/starter-kit:基本機能のセット(太字、打消し、リスト等)
- @tiptap/extension-heading:H1-H6などの見出しを使えるようにする拡張機能
- @tiptap/extension-link:アンカーリンクを使える由生にする拡張機能
pnpm add @tiptap/extension-heading @tiptap/extension-link @tiptap/react @tiptap/starter-kit
初期表示
editor.tsx
// components/Editor.tsx
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
export const Editor = () => {
const editor = useEditor({
extensions: [StarterKit],
content: "ここに初期表示されます。
",
});
return (
div className="p-4 border rounded">
EditorContent editor={editor} />
/div>
);
};
useEditor:先ほどインストールしたtiptap/react
にはいっているフックです。ここでエディターの初期化を行います。
extensions: [StarterKit]:さきほどインストールしたtiptap/starter-kit
をextensions
に設定することで基本的なリッチテキスト機能(太字、打消し、リスト等)が使用できます。
extensions: [StarterKit]:さきほどインストールしたtiptap/starter-kit
をextensions
に設定することで基本的なリッチテキスト機能(太字、打消し、リスト等)が使用できます。
content: “”:初期表示内容です。空にもできます。
このコードで下記のような実装ができます。
最初はと変わりませんがここから太字にするボタンなどを追加していきます。
機能ボタンの追加
toolmenu.tsx
import { Editor } from "@tiptap/react";
import {
MdFormatBold,
MdFormatStrikethrough,
MdRedo,
MdUndo,
MdOutlineTextFields,
MdCode,
MdFormatQuote,
MdOutlineAddLink,
MdLinkOff,
} from "react-icons/md";
import { FaListUl, FaListOl } from "react-icons/fa";
export const ToolMenu = ({ editor }: { editor: Editor }) => {
if (!editor) {
return null;
}
const headings = [
{ label: "H1", level: 1 },
{ label: "H2", level: 2 },
{ label: "H3", level: 3 },
] as const;
return (
div className="flex flex-wrap gap-2 border-b border-gray-600 p-4 text-2xl">
div className="relative group">
button className={!editor.isActive("heading") ? "opacity-20" : ""}>
MdOutlineTextFields />
/button>
div className="absolute left-0 top-full z-10 hidden flex-col gap-1 bg-white text-black p-2 rounded shadow-md group-hover:flex">
{headings.map((h) => (
button
key={h.level}
onClick={() =>
editor.chain().focus().toggleHeading({ level: h.level }).run()
}
className={`text-left px-2 py-1 hover:bg-gray-200 ${
editor.isActive("heading", { level: h.level })
? "font-bold"
: ""
}`}
>
{h.label}
/button>
))}
button
onClick={() => editor.chain().focus().setParagraph().run()}
className={`text-left px-2 py-1 hover:bg-gray-200 ${
editor.isActive("paragraph") ? "font-bold" : ""
}`}
>
P
/button>
/div>
/div>
button
type="button"
onClick={() => editor.chain().focus().toggleBold().run()}
className={!editor.isActive("bold") ? "opacity-20" : ""}
>
MdFormatBold />
/button>
button
type="button"
onClick={() => editor.chain().focus().toggleStrike().run()}
className={!editor.isActive("strike") ? "opacity-20" : ""}
>
MdFormatStrikethrough />
/button>
button
type="button"
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={!editor.isActive("bulletList") ? "opacity-20" : ""}
>
FaListOl />
/button>
button
type="button"
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={!editor.isActive("orderedList") ? "opacity-20" : ""}
>
FaListUl />
/button>
button
type="button"
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={!editor.isActive("blockquote") ? "opacity-20" : ""}
>
MdFormatQuote />
/button>
button
type="button"
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={!editor.isActive("codeBlock") ? "opacity-20" : ""}
>
MdCode />
/button>
button
type="button"
onClick={() => {
const url = prompt("リンク先のURLを入力してください");
if (url) {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: url })
.run();
}
}}
className={!editor.isActive("orderedlist") ? "opacity-20" : ""}
>
MdOutlineAddLink />
/button>
button
type="button"
onClick={() => editor.chain().focus().unsetLink().run()}
className={!editor.isActive("orderedlist") ? "opacity-20" : ""}
>
MdLinkOff />
/button>
button onClick={() => editor.chain().focus().undo().run()} type="button">
MdUndo />
/button>
button onClick={() => editor.chain().focus().redo().run()} type="button">
MdRedo />
/button>
/div>
);
};
トグルメニューのコンポーネントを作成しました。このコンポーネントに機能ボタンのコードが記載されています。
太字にするボタンを抜粋して説明します。
toolmenu.tsx
button
type="button"
onClick={() => editor.chain().focus().toggleBold().run()}
className={!editor.isActive("bold") ? "opacity-20" : ""}
>
MdFormatBold />
/button>
onClick={() => editor.chain().focus().toggleBold().run()}
正直、機能ボタンのonClick部分は一部を除いてほぼ一緒です。 editor.chain()
でTiptapのチェーン操作を実行します。(この後に続く処理を続けて実行するみたいな)
そして.focus()
でエディターにフォーカスし、最後のrun()
で実行します。
変わる部分は.toggleBold()
で今回は太字なので.toggleBold()
ですが、引用だったら.toggleBlockquote()
、打消し戦だったら.toggleStrike()
と変わります。
className={!editor.isActive(“bold”) ? “opacity-20” : “”}
ここは必須でないですが、太字機能がActiveになっている時にボタンの色を濃くしてます。("bold")
のところは必要に応じて("blockquote")
とか("strike")
にします。
React iconを使用しました。
【備忘録】TiptapとTailwindcss
上記のようにエディターやボタンの設定をしてもTailwindcssを使用していると、初期スタイルがないので見た目には何も変化がありません。
h1はこのスタイル、h2はこのスタイルと別でスタイルを充てる必要があります。
globals.css
@import "tailwindcss";
.prose h1 {
@apply text-3xl font-bold;
}
.prose h2 {
@apply text-2xl font-bold;
}
.prose h3 {
@apply text-lg font-bold;
}
.prose ol{
@apply list-disc w-full text-wrap px-10;
}
.prose ul{
@apply list-decimal w-full text-wrap px-10;
}
.prose blockquote{
@apply bg-neutral-200 italic inline-block px-2 rounded-sm;
}
.prose a{
@apply cursor-pointer underline;
}
.prose code{
@apply p-2 rounded-sm bg-gray-900 text-white;
}
正しい方法かはわかりませんが、私は今回global.cssにブロックパーツ用のスタイルを当てました。
おわりに
だんだんウェイトが重くなってきたので、毎日更新ではなく2日に一回、更新をしようと思います。
Views: 2