Lexical リッチテキストエディタ解説
はじめに#
iamfax.comではリッチテキストエディターとしてlexical
を利用しています。
これはMeta(facebook)社のOSSのライブラリです。
iamfax.comを作成するにあたり、当初はdraft.js
というFacebook社製のリッチテキストエディターを使っていましたが開発終了してしまい、lexicalに切り替えました。
今回はLexicalというエディターのコンセプトを簡単に解説をしていきたいと思います。
リッチテキストエディターとは#
数あるリッチテキストエディタ(通称WYSIWYGエディター)ライブラリの中でLexicalを利用したのはOSSで自由に拡張できるところが有りました。
開発元もmeta社が提供しており信頼性がありますし、なによりlexicalのコンセプトに共感できました。(後述)
lexicalは一体何をしているのかというと、簡単に言えば状態を管理しているエンジン
となります。順を追って説明していきます。
簡単なリッチテキストエディターを実装してみる#
リッチテキストエディターは実はJavascriptで簡単に実装できます。
HTMLでタグのにcontentEditable
をtrueにすることで編集が自在にできるようになります。
H1, H2, H3, Pをボタンで切り替える簡単なリッチテキストエディターを作ってみましょう。
<body>
<div>
<button onclick="changeTag('p')">p</button>
<button onclick="changeTag('h1')">h1</button>
<button onclick="changeTag('h2')">h2</button>
<button onclick="changeTag('h3')">h3</button>
</div>
<div contenteditable="true" id="editableContent">
<p>ここにテキストを入力してください。</p>
</div>
<script>
function changeTag(tagName) {
const editableContent = document.getElementById('editableContent');
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const newNode = document.createElement(tagName);
range.surroundContents(newNode);
editableContent.focus();
}
</script>
</body>
changeTag
という関数ではgetElementById
でターゲットのdivをidから取りh1などのtagの変更する処理をしています。これだけでボタンの押下でタグを変更できます。
lexicalは何をしているのか#
Lexicalも同様の処理をしていますが、lexicalを使う目的は状態管理をしてくれているためです。
JavaScriptだけでリッチテキストエディターを書いてくと様々な壁にぶつかります。
例えばバックスペースを押していくと、自身のタグまで消しすぎてしまう問題やカーソル制御の問題などで、前者に関しては、例えばこれ以上消さないというようなロジックを書く必要がでてきます。
JSでの制御処理を書いていくと今度はUndo/Redoの挙動がおかしくなったりします。そのため今度はUndo/Redoの実装、つまり状態管理をJSで実装しなくてはいけなくなります。
実はLexicalではこうしたUndo/redoが正しく動作するように状態を制御をしてくれているのです。
状態の管理以外にも、エディターの状態更新APIを提供するなどしています。
イメージとしては、OSのような役割(資源管理やシステムコール定義など)を担っていると考えられます。
Lexicalの使い方#
Lexicalでは中核のシステムコールを備えたOSのようなもので、ボタンを押してH1, H2にするなどの具体的な実装はユーザーが作ることになります。
それではどうやってそれを実現するのかをH1ボタンを実装例で説明します。
import {
createCommand,
} from "lexical";
const FORMAT_HEADING_COMMAND = createCommand('format_heading');// 一意なStringを適当に指定
createCommandという関数を通じてコマンドを作ります。次に作ったコマンドを登録します。
import {
$isRangeSelection,
$getSelection
} from "lexical";
import {
$createHeadingNode,
} from "@lexical/rich-text";
import { $setBlocksType } from "@lexical/selection";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
const [editor] = useLexicalComposerContext(); // editorをとってくる
editor.registerCommand(FORMAT_HEADING_COMMAND, () => { // 先程のcreateCommandで作ったコマンド
editor.update(() => { // editor の状態更新にはupdate関数を利用
const selection = $getSelection(); // 現在のセレクションをとってくる
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h1')); // 選択範囲を h1 に変換
}
});
return true;
}, 0);
ごちゃごちゃと書いていますが、要はgetSelectionで現在のセレクト部分をとってきて、そのBlockTypeを更新しているだけです。
HTMLで書いたのと同じですよね。registerCommand
関数にはFORMAT_HEADING_COMMAND
を渡しており、このボタンでeditorの状態の更新するように登録します。
このようにLexicalでは、動作(h1ボタン押下、マウス操作)から状態管理の核のEditorStateを更新、を書いていくことで動作を定義していっています。
editorのステート管理の詳細などについては、https://dio.la/article/lexical-state-updates に詳細に書かれていますので参考ください。
最後に#
今回は簡単にLexicalのコンセプトと動作を簡単な例を取り説明しました。
前述の通りLexicalは、OSのように基本機能を提供してくれているだけで高度なアプリケーション等は自分で作るものと考えていくとスッキリしてくると思います。
iamfax.comを立ち上げるに当たり、リッチテキストエディターの実装を自分でする事考えましたが、後々の拡張性を考えるとやはりオレオレフレームワークでは限界があることからlexicalの採用に至りました。
Reactを選んだのも、例えばh1の実装
などを自分でやる必要が有り、Reactならその手間がなくなるためNext.jsを使うことを選択しました。
lexicalの設計思想が「Lexicalは編集のプリミティブだけを提供し、高度な機能は全てユーザー空間に委ねる」という感じで、UNIX哲学の「小さく尖ったツールを作り、組み合わせて使う」とも通じていて、親しみを持ったこともlexical採用の大きな理由です。
lexicalの動作は次のサイトで確認できます。 https://playground.lexical.dev/
こちらの実装例はgithubにかかれています。実装に当たり、是非参考にしてみてください。なお、他の言語でもlexicalの導入する試みが有り、たとえばsvelteでも有志が頑張っています。
色々と実装大変そうだなぁなんて思うのではないでしょうか?(私がvue.jsやsvelte.jsを選ばなかった理由です)
実は開発当初はlexicalはtypescriptで書かれておらずJSでした。自分で型を書いて頑張った記憶が有ります。今はFB社がtsで書きなおしたのですね。。羨ましい。。