Lexical
An extensible text editor framework that does things differently
https://lexical.dev/
Lexcial Editorとは
主な特徴
使用例
長所
主要な概念
目次機能に関して
目次機能の概要
主な特徴
実装プロセス
機能のメリット
実装の話
バージョン
利用したPlug in
TableOfContents
TableOfContentsNode
TableOfContentsBody
次はDrag&Dropを実装する
Lexical EditorはLexicalというオープンソースプロジェクトの一部でモジュール型で拡張可能な高性能のテキストエディタを作成するためのツールです。Facebook(現 Meta)の開発者によって主導されReactと一緒に使用されることを前提として設計されています。Lexicalはテキストエディタのパフォーマンスを最適化しながらも複雑な機能を実装できるように作られています。
Lexicalは高性能なテキストエディタが必要なさまざまなウェブアプリケーションに利用され、Reactベースの開発環境で非常に簡単に統合できる点で非常に有用です。
上記イメージの赤い枠に当たるところのことで、この記事の上段にもついている目次のことです。
今回実装した目次機能はWantedlyのストーリー、社内報でユーザーがコンテンツを簡単にナビゲートできるようにするツールです。記事内の主要なセクションを視覚的に一覧表示し、特定のセクションへ迅速に移動できるようにします。
この機能によりウォンテッドリー記事の読みやすさと利便性を大幅に向上させ、ユーザーの良い体験を提供することができるようになりました。
目次機能実装に向けて当時最新だった Lexcial v0.16.0 を利用しています。現在の最新バージョンは8月2日付の v0.17.0になっています。
現状目次機能を簡単に追加できる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>
次は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が更新される → ... 」という無限ループに落ちないように気を付ける必要があります。
$createTableOfContentsNode と $updateTableOfContentsNode は TableOfContentsNodeと命名したコンポーネントの中に定義されています。
再度編集に戻ったときブロックを検知して目次として機能できるようにするコード、作成完了時目次をブロック化してJSONに書くコードも一緒に定義しています。
また、以下の形でHeadingタグを目次の形にする TableOfContentsBody と繋がっています。
public decorate(): JSX.Element {
return (
<Selectable nodeKey={this.__key}>
<TableOfContentsBody tableOfContents={this.__entries} />
</Selectable>
);
}
目次の見た目と各種機能を定義しているコンポーネントです。
const [editor] = useLexicalComposerContext();
useLexicalComposerContext で LexicalComposer の内ならどこからでも Editor の要素を取得できるので、スクロール機能を実装することもわりと簡単です。
ここでは主にUI実装なので上記 TableOfContentsNode の後からは難しいことはありませんでした。
実はPoCまで終わっているのでそのうちにリリースでき、より便利な記事の作成・編集ができるようになれればと思っています 🙏