目次
Lexcial Editorとは
Lexical EditorはLexicalというオープンソースプロジェクトの一部でモジュール型で拡張可能な高性能のテキストエディタを作成するためのツールです。Facebook(現 Meta)の開発者によって主導されReactと一緒に使用されることを前提として設計されています。Lexicalはテキストエディタのパフォーマンスを最適化しながらも複雑な機能を実装できるように作られています。
Lexical
主な特徴
- 高パフォーマンス : Lexicalは非常に高速に動作することが特徴です。大量のテキストや複雑なインタラクションが発生しても、迅速かつ安定して動作します。
- 拡張性 : プラグインベースのアーキテクチャを採用しており、エディタに簡単に様々な機能を追加できます。プラグインには書式設定、画像の挿入、コードブロックのサポートなど、複雑な機能を実装するものが含まれます。
- モジュール型設計 : Lexicalは非常にモジュール化されており、必要に応じてさまざまな機能を選んで使用することができます。必要な部分だけを取り込んで使うことができるため柔軟でカスタマイズが簡単です。
- Reactとの統合 : LexicalはReactベースのライブラリでReactコンポーネントとの互換性が高く、React環境と自然に連携します。そのため、React環境でのテキストエディタ実装において大きな利点があります。
- ドキュメントモデル : Lexicalは状態管理においてDocument Object Model (DOM)に似た内部モデルを使用しており、エディタの状態を効率的に維持し、操作します。これにより、テキストの変更やフォーマットなどの複雑な動作を効率的に管理できます。
- 協働編集機能のサポート : 複数のユーザーが同時にドキュメントを編集する協働編集機能もLexicalを通じて実装できます。WebSocketやCRDTなどの技術を統合してこの機能を実現することができます。
使用例
- ブログエディタ
- メッセージアプリのテキスト入力フィールド
- ドキュメント編集ツール
- コードエディタ
長所
- 軽量性 : 多くのエディタ機能を提供しつつ基本的な構造は非常に軽量です。
- 柔軟性 : 高度にカスタマイズ可能で開発者が必要に応じて機能を追加または削除できます。
- 安全性 : Lexicalはクラッシュを防ぎ、予測可能な動作を行うように設計されています。
主要な概念
- EditorState : 現在のエディタの状態を示すオブジェクトでエディタの状態変化を追跡および管理します。
- Nodes : Lexicalの編集モデルではテキスト、ブロック、画像などはすべてNode(ノード)として表現されます。
- Commands : Lexicalの機能を実行するために使用されるコマンドシステムで、プラグインや機能をトリガーするために使用されます。
Lexicalは高性能なテキストエディタが必要なさまざまなウェブアプリケーションに利用され、Reactベースの開発環境で非常に簡単に統合できる点で非常に有用です。
目次機能に関して
目次機能の概要
上記イメージの赤い枠に当たるところのことで、この記事の上段にもついている目次のことです。
今回実装した目次機能はWantedlyのストーリー、社内報でユーザーがコンテンツを簡単にナビゲートできるようにするツールです。記事内の主要なセクションを視覚的に一覧表示し、特定のセクションへ迅速に移動できるようにします。
主な特徴
- 自動生成 : ドキュメントの見出しやセクションを自動で検出し目次を生成します。H2、H3のHTMLヘッダータグに基づいて機能し各セクションを効率よく整理します。
- 簡単なナビゲーション : 目次内の各セクションタイトルをクリックするとそのセクションへすぐにジャンプできるため、長いドキュメントやページでもユーザーは必要な情報を素早く見つけることができます。
- 動的更新 : ページに新しいセクションが追加されたり削除されたりすると目次も自動的に更新され、最新の状態が保持されます。これによりユーザーは常に正確な情報を得ることができます。
- カスタマイズの柔軟性 : 目次のスタイルや機能をユーザーのニーズに合わせて調整できるよう、追加・削除したり、開く・閉じることもできます。
実装プロセス
- ヘッダータグの抽出 : ドキュメント内のH2〜H3タグを検出し、それに基づいて目次を生成するコードを作成しました。
- スクロール : 目次に追加された見出しとそのセクションをキー(Lexical Editorの各要素はユニークキーを持っています)で連動しました。目次の各項目をクリックした際に連動したキーに該当する要素の位置にスクロールできるように実装しました。
- 開く・閉じる : これは目次のデザイン的な機能のことです。目次の高さを計算し、スムーズなアニメーション付きのアコーディオン式に実装しました。
機能のメリット
- ユーザー体験の向上 : 長いページやドキュメントをナビゲートする際の時間を大幅に短縮します。
- 直感的なナビゲーション : セクションごとに構成された目次はユーザーがドキュメントの構造を一目で把握できるようにします。
- 柔軟な適用 : ブログ、技術文書、オンラインマニュアルなどさまざまな種類のコンテンツに対応可能で幅広く利用できます。
- SEO向上 : 目次機能を活用することにより、ヘッダーを太字として使うことが減って構造と用途に合う記事作成を期待しています。ただし、文字サイズが調整できないため機能追加の必要性を感じています。
この機能によりウォンテッドリー記事の読みやすさと利便性を大幅に向上させ、ユーザーの良い体験を提供することができるようになりました。
実装の話
バージョン
目次機能実装に向けて当時最新だった Lexcial v0.16.0 を利用しています。現在の最新バージョンは8月2日付の v0.17.0になっています。
利用したPlug in
現状目次機能を簡単に追加できるPlug inはないですが、その機能をより簡単に実装できるTableOfContentsPlugin が存在します。
import { TableOfContentsPlugin } from "@lexical/react/LexicalTableOfContentsPlugin";
TableOfContentsPluginが Editor内のHeadingタグを取得してくれるので、目次機能に必要な初期実装を多くスキップすることができました。
取得したHeading Tagは以下のような形になり
[
key: NodeKey,
text: string,
tag: HeadingTagType
]
以下のように渡して使うことができます。
<TableOfContentsPlugin>
{(tableOfContents) => <TableOfContents entries={tableOfContents} />}
</TableOfContentsPlugin>
TableOfContents
次はHeadingタグを受け取った TableOfContents の一部です。
useEffect(() => {
return mergeRegister(
editor.registerCommand<TableOfContentsPayload>(
INSERT_TABLEOFCONTENTS_COMMAND,
() => {
editor.update(() => {
const tableOfContentsNode = $createTableOfContentsNode(entries);
$insertNodeToNearestRoot(tableOfContentsNode);
});
return true;
},
COMMAND_PRIORITY_CRITICAL
),
editor.registerCommand<TableOfContentsPayload>(
UPDATE_TABLEOFCONTENTS_COMMAND,
(payload) => {
editor.update(() => {
const root = $getRoot();
const existingNodes = root
.getChildren()
.filter((node) => node instanceof TableOfContentsNode) as TableOfContentsNode[];
existingNodes.forEach((node) => {
$updateTableOfContentsNode(editor, node, payload.entries);
});
});
return true;
},
COMMAND_PRIORITY_CRITICAL
)
);
}, [editor, entries]);
目次機能はInserterから追加するようになっているため、それを担当する INSERT_TABLEOFCONTENTS_COMMAND の追加コマンド。動的更新を担当する UPDATE_TABLEOFCONTENTS_COMMAND の更新コマンド2つに構成しています。
* 注意点として TableOfContentsPlugin は全ての変化に反応するので 「Heading タグを作成する(Editorが変更される) → 目次が更新される → Editorが更新される → 目次が更新される → Editorが更新される → ... 」という無限ループに落ちないように気を付ける必要があります。
TableOfContentsNode
$createTableOfContentsNode と $updateTableOfContentsNode は TableOfContentsNodeと命名したコンポーネントの中に定義されています。
再度編集に戻ったときブロックを検知して目次として機能できるようにするコード、作成完了時目次をブロック化してJSONに書くコードも一緒に定義しています。
また、以下の形でHeadingタグを目次の形にする TableOfContentsBody と繋がっています。
public decorate(): JSX.Element {
return (
<Selectable nodeKey={this.__key}>
<TableOfContentsBody tableOfContents={this.__entries} />
</Selectable>
);
}
TableOfContentsBody
目次の見た目と各種機能を定義しているコンポーネントです。
const [editor] = useLexicalComposerContext();
useLexicalComposerContext で LexicalComposer の内ならどこからでも Editor の要素を取得できるので、スクロール機能を実装することもわりと簡単です。
ここでは主にUI実装なので上記 TableOfContentsNode の後からは難しいことはありませんでした。
次はDrag&Dropを実装する
実はPoCまで終わっているのでそのうちにリリースでき、より便利な記事の作成・編集ができるようになれればと思っています 🙏