リッチテキストエディタフレームワークlexicalの紹介

こんにちは。
株式会社アドグローブ ソリューション第三事業部の栁です。

本記事ではリッチテキストエディタフレームワークlexicalについてご紹介します。

■リッチテキストエディタフレームワークについて

仕事でSlackやTeamsを使ったことがある方なら見慣れてると思いますが
文字を入力する場所には、太字にしたり、下線を引いたり、
あるいは画像や動画を挿入したりする機能が付いていることがあります。

こうしたリッチテキスト的機能が求められた際、完全に1から自作するのは中々大変な作業となるのですが、そんな時に便利なのがリッチテキストエディタフレームワークです。

■lexicalとは

上述の通りlexicalはリッチテキストエディタフレームワークの一つです。
Meta社(旧Facebook)が開発しており、Draft.jsの後継にあたるものになります。
リリースは2022年と割と最近出てきたライブラリですが、機能はかなり豊富に揃ってます。

百聞は一見に如かず。公開されてるPlaygroundを見てみましょう。

playground.lexical.dev

リンクの内容はご覧いただけたでしょうか?
すごいですよね!

これをそのまま導入するだけで実戦で十分使えてしまいます。
右下のツールバーの中には「音声認識」なんて機能もあったりします。
自作のブログサイトでコメント欄にこんな機能あったら映えるのではないでしょうか。

■簡単に導入してみよう

lexicalはVanilla JS(素のJS)やVue.jsでも使えますが、
構成がReact.jsと似ているため、React.jsで導入することが多いかなと思います。

ここでもReact.jsをベースに導入紹介をしようかと思います。

【バージョン情報】
- Node.js :v20.13.1
- React.js :v18.3.1
- lexical :v0.18.0

【前提】 React.jsのテスト環境の用意 (お使いの環境が既にあればここは飛ばして良いです)
今回はViteを使って環境を用意しようと思います。
余談ですが、ひと昔前主流だったcreate-react-appは既にドキュメント落ちしているそうですね、、

  1. 下記からNode.jsをインストールします(Node.jsをインストール済みの場合は不要) nodejs.org
  2. npmを最新化する
npm install -g npm


3. Viteでプロジェクトを作成します

npm create vite@latest -y


4. Project name: » vite-projectと出るため、プロジェクト名を入力してEnterを押します
5. 表示されるフレームワークの中から「React」を選択します
6. 表示されるテンプレートの中から「TypeScript」を選択します
7. コンソールに表示されるコマンドを打って起動します

cd {プロジェクト名}
npm install
npm run dev


8. コンソールに表示される➜ Local: http://localhost:○○○○/のリンクを開き、Vite+Reactのマークの初期画面が表示されたらOKです

【リッチテキストエディタの作成】
1. lexicalをインストールします

npm install --save lexical @lexical/react


2. 最小構成のテキストエディタコンポーネントを作成します
- 下記ファイルを追加、更新してみましょう /src/plugins/ToolbarPlugin.tsx

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
  $getSelection,
  $isRangeSelection,
  FORMAT_TEXT_COMMAND,
  SELECTION_CHANGE_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';

const LowPriority = 1;


export default function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();
  const toolbarRef = useRef(null);
  const [isBold, setIsBold] = useState(false);

  const $updateToolbar = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setIsBold(selection.hasFormat('bold'));
    }
  }, []);

  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({editorState}) => {
        editorState.read(() => {
          $updateToolbar();
        });
      }),
      editor.registerCommand(
        SELECTION_CHANGE_COMMAND,
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        (_payload, _newEditor) => {
          $updateToolbar();
          return false;
        },
        LowPriority,
      )
    );
  }, [editor, $updateToolbar]);

  return (
    <div className="toolbar" ref={toolbarRef}>
      <button
        onClick={() => {
          editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
        }}
        className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
        aria-label="Format Bold">
        <i className="format bold" />
      </button>
    </div>
  );
}
  • 今回は文字を太字にする機能を実装してみます
  • stateでボタンの状態を管理し、lexicalの機能で入力文字にスタイルを適用させています

/src/App.tsx

import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';

import ExampleTheme from './ExampleTheme.ts';
import ToolbarPlugin from './plugins/ToolbarPlugin';

const placeholder = 'Enter some rich text...';

const editorConfig = {
  namespace: 'React.js Demo',
  nodes: [],
  onError(error: Error) {
    throw error;
  },
  theme: ExampleTheme,
};

export default function App() {
  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-container">
        <ToolbarPlugin />
        <div className="editor-inner">
          <RichTextPlugin
            contentEditable={
              <ContentEditable
                className="editor-input"
                aria-placeholder={placeholder}
                placeholder={
                  <div className="editor-placeholder">{placeholder}</div>
                }
              />
            }
            ErrorBoundary={LexicalErrorBoundary}
          />
          <HistoryPlugin />
          <AutoFocusPlugin />
        </div>
      </div>
    </LexicalComposer>
  );
}
  • LexicalComposer配下に各種プラグインを配置します
  • 場合によってeditorConfigに値を設定します

/src/index.css

body {
  margin: 0;
  background: #eee;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, '.SFNSText-Regular',
    sans-serif;
  font-weight: 500;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.App {
  font-family: sans-serif;
  text-align: center;
}

.editor-container {
  margin: 20px auto 20px auto;
  border-radius: 2px;
  max-width: 600px;
  color: #000;
  position: relative;
  line-height: 20px;
  font-weight: 400;
  text-align: left;
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
}

.editor-inner {
  background: #fff;
  position: relative;
}

.editor-input {
  min-height: 150px;
  resize: none;
  font-size: 15px;
  caret-color: rgb(5, 5, 5);
  position: relative;
  tab-size: 1;
  outline: 0;
  padding: 15px 10px;
  caret-color: #444;
}

.editor-placeholder {
  color: #999;
  overflow: hidden;
  position: absolute;
  text-overflow: ellipsis;
  top: 15px;
  left: 10px;
  font-size: 15px;
  user-select: none;
  display: inline-block;
  pointer-events: none;
}

.editor-text-bold {
  font-weight: bold;
}

.editor-paragraph {
  margin: 0;
  margin-bottom: 8px;
  position: relative;
}

.editor-paragraph:last-child {
  margin-bottom: 0;
}

.toolbar {
  display: flex;
  margin-bottom: 1px;
  background: #fff;
  padding: 4px;
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
  vertical-align: middle;
}

.toolbar button.toolbar-item {
  border: 0;
  display: flex;
  background: none;
  border-radius: 10px;
  padding: 8px;
  cursor: pointer;
  vertical-align: middle;
}

.toolbar button.toolbar-item:disabled {
  cursor: not-allowed;
}

.toolbar button.toolbar-item.spaced {
  margin-right: 2px;
}

.toolbar button.toolbar-item i.format {
  background-size: contain;
  display: inline-block;
  height: 18px;
  width: 18px;
  margin-top: 2px;
  vertical-align: -0.25em;
  display: flex;
  opacity: 0.6;
}

.toolbar button.toolbar-item:disabled i.format {
  opacity: 0.2;
}

.toolbar button.toolbar-item.active {
  background-color: rgba(223, 232, 250, 0.3);
}

.toolbar button.toolbar-item.active i {
  opacity: 1;
}

.toolbar .toolbar-item:hover:not([disabled]) {
  background-color: #eee;
}

.toolbar .toolbar-item .text {
  display: flex;
  line-height: 20px;
  width: 200px;
  vertical-align: middle;
  font-size: 14px;
  color: #777;
  text-overflow: ellipsis;
  width: 70px;
  overflow: hidden;
  height: 20px;
  text-align: left;
}

.toolbar .toolbar-item .icon {
  display: flex;
  width: 20px;
  height: 20px;
  user-select: none;
  margin-right: 8px;
  line-height: 16px;
  background-size: contain;
}

i.bold {
  background-image: url(icons/type-bold.svg);
}

/src/ExampleTheme.ts

export default {
    paragraph: 'editor-paragraph',
    placeholder: 'editor-placeholder',
    text: {
      bold: 'editor-text-bold',
    },
};
  • 公式が用意しているスタイルです

/public/icons/type-bold.svg

<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-bold" viewBox="0 0 16 16">
  <path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
</svg>
  • アイコンです

こんな感じになるかと思います。文字もちゃんと太字になってますね。 公式ドキュメントにはもっと機能がいっぱい乗ったテンプレートコードがあります。
是非ご自身の環境で使ってみてください。 lexical.dev

■終わりに

今回はリッチテキストエディタフレームワークlexicalについて簡単に紹介させて頂きました。
とても便利なライブラリなので、記事投稿サイトや、コミュニケーションツールなどの作成で入力機能を豪華にしたい場合は是非使ってみてください!
それでは最後までお読みいただきありがとうございました👍


アドグローブでは、さまざまなポジションで一緒に働く仲間を募集しています!
詳細については下記からご確認ください。みなさまからのご応募お待ちしております。

採用情報