Home About Contact
lexical , React , Note Taking

簡単なエディタをつくる試み lexical を調べる(その2)

前回から引き続いて lexical について調べていきます。 今回は、エディタの初期テキストを index.html で指定する方法です。

次のように textarea の値として設定してある内容を lexical のエディタの初期テキストを指定することにします。

dist/index.html

<textarea id="mytextarea">Hello, World!</textarea>

lexical のテキストエディタ部分の初期値の設定は initalConfig の editorState の値として設定すればよいようです。

src/index.js

  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
    editorState: 'Hello, World!',
  };

一度これで実行してみます。( npm run build して python3 でwebサーバを起動。 )

うまくいきません。 コンソールを見てみると、次のようなメッセージが。

 "Hello, World!" is not valid JSON

editorState の値には文字列を渡すのではなく、期待されている valid JSON を渡す必要があるとのこと。 valid JSON を作り出すのが簡単ではないかもしれないので、 ここは用意されている markdown から適切な JSON に変換してくれる関数を使いましょう。

このページ lexical-markdown を参考に実装していきます。

次のようにすればよいようです。

<LexicalComposer initialConfig={{
  editorState: () => $convertFromMarkdownString(markdown, TRANSFORMERS)
}}>

src/index.js を次のように修正しました。 (作動確認なので、直接 markdown 部分にテキストを入れている。)

  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
    editorState: () => $convertFromMarkdownString('Hello, World!', TRANSFORMERS),
  };

これらの関数を使うために src/index.js の先頭にインポートも忘れずに。

import {
  $convertFromMarkdownString,
  $convertToMarkdownString,
  TRANSFORMERS,
} from '@lexical/markdown';

これで実行して結果を確認します。

lexical 1

うまくいきました。

あとは、dist/index.html の該当部分のテキストを取り出して、ここにセットすればよい。

リファクタリング

先に進む前に、 この段階でコードをリファクタリングします。

方針は、 src/index.js に全部詰め込んでいたのをやめてファイル src/index.js と src/Editor.js とにわけます。

まず、 現在 src/index.js に書いている内容のほとんどを src/Editor.js に移します。 そして、 src/index.js の最後に書いていた dist/index.html の root に Editor を適用する部分のコードを src/index.js に残します。

リファクタリング後の src/index.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import Editor from "./Editor";

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Editor />);

src/Editor.js

import React from 'react';
//import {createRoot} from 'react-dom/client';

... 中略

export default Editor;

/*
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Editor />);
*/

src/Editor.js は、 src/index.js からまるまるコピーした状態から、 import {createRoot} from 'react-dom/client'; をコメントアウト、 そして、 root 部分に Editor を適用するコードは、src/index.js に移動したので、 src/Editor.js からはその部分のコードをコメントアウトしました。

この段階でのプロジェクトのファイル構成を確認しておきます。

.
├── babel.config.json
├── dist
│   ├── editor.js
│   └── index.html
├── node_modules/...
├── package.json
├── package-lock.json
├── src
│   ├── Editor.js
│   └── index.js
└── webpack.config.js

textarea 要素に書いた値を反映

dist/index.html の textarea に書いた値を src/Editor.js の initialConfig に反映させるために次のようにします。

dist/index.html

<textarea id="mytextarea">Hello, World!</textarea>

id="mytextarea" 指定してあるので、これを使って値を取得します。

src/index.js

const initMarkdownValue = document.getElementById('mytextarea').value;

この値を Editor に渡すために次のように書きかえます。

src/index.js

root.render(<Editor initMarkdownValue={initMarkdownValue} />);

Editor でこの値を受け取ることができるように、src/Editor.js の該当個所を次のように修正。

function Editor( { initMarkdownValue='' } ) {
  const initialConfig = {
    ...
    editorState: () => $convertFromMarkdownString(initMarkdownValue, TRANSFORMERS),
  };

これで実行してみます。

lexical 2

うまくいきました。

textarea 要素は、値を受け渡すために使っているだけなので、 表示されない方がよいかもしれません。 必要なら次のように display:none; 指定して隠しておけばよいでしょう。

<textarea id="mytextarea" style="display:none;">Hello, World!</textarea>

まとめ

完成したコードを掲載します。

dist/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>my text editor</title>
    <style>
body {
  margin:0;
  padding:0;
}
div {
  margin:0;
  padding:0;
}
#myeditor {
  margin: 30px;
  border: solid 1px #333;
}
.editor-paragraph {
  padding:0;
  margin:0;
  font-size:14pt;
}
.editor-placeholder {
  position: absolute;
  top: 32px;
  left: 32px;
  user-select: none;
  pointer-events: none;
  font-size:14pt;
  color: #cccccc;
  overflow: hidden;
}
    </style>
  </head>
  <body>
    <div id="myeditor">
      <div id="root"></div>
    </div>
    <textarea id="mytextarea" style="display:none;">Hello, World!</textarea>
    <script src="./editor.js"></script>
  </body>
</html>

src/index.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import Editor from "./Editor";

const initMarkdownValue = document.getElementById('mytextarea').value;

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Editor initMarkdownValue={initMarkdownValue} />);

src/Editor.js

import React from 'react';
//import {createRoot} from 'react-dom/client';

import {$getRoot, $getSelection} from 'lexical';
import {useEffect, useState} from 'react';

import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
//import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';

import {
  $convertFromMarkdownString,
  $convertToMarkdownString,
  TRANSFORMERS,
} from '@lexical/markdown';


const theme = {
  paragraph: 'editor-paragraph'
};

function MyCustomAutoFocusPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Focus the editor when the effect fires!
    editor.focus();
  }, [editor]);

  return null;
}

function onError(error) {
  console.error(error);
}

function OnChangePlugin({ onChange }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerUpdateListener(({editorState}) => {
      onChange(editorState);
    });
  }, [editor, onChange]);
}

function Editor( { initMarkdownValue='' } ) {
  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
    editorState: () => $convertFromMarkdownString(initMarkdownValue, TRANSFORMERS),
  };

  const [editorState, setEditorState] = useState();
  function onChange(editorState) {
    setEditorState(editorState);
  }

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={<ContentEditable style={{ outline: 'none' }} />}
        placeholder={<div className="editor-placeholder">Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
      <MyCustomAutoFocusPlugin />
      <OnChangePlugin onChange={onChange}/>
    </LexicalComposer>
  );
}

export default Editor;

次回は、lexical の Editor 上で変更した内容を監視して index.html に反映する方法を調査します。