Home About Contact
lexical , React , Note Taking

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

前回 から引き続いて 今回は、 エディタへの初期値のセットおよび エディタの内容が更新されたときに現在のエディタの内容を取得する部分を 改良します。

ボールドにレンダリングされる

エディタへの初期値を渡す方法として Editor.js 内で次のような記述で値を渡しています。

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

この方法を使うと、 initMarkdownValueHello! **World** だった場合、 現状のコードでは、 次のようにレンダリングされてしまいました。

lexical 1

$convertFromMarkdownString(initMarkdownValue, TRANSFORMERS) が実行されることで markdown テキストから EditorState インスタンスが作り出されるのですが、 ** で囲った部分が太字になっています。

RichText エディタをつくるのであれば、これは好ましい振る舞いですが、 今は PlainText エディタをつくりたいので、そのまま ** を出力してほしい。

自分で EditorState をつくる

結局、EditorState インスタンスを自分でつくり出す必要がある。

$convertFromPlainString のような関数が用意されていないか調べたが見つけられなかった。

この editorState にセットしている無名の関数

() => $convertFromMarkdownString(initMarkdownValue, TRANSFORMERS)

これはいったい何をやっているのかソースをたどってみたのですが、 $convertFromMarkdownString(initMarkdownValue, TRANSFORMERS) が実行された結果、 EditorState インスタンスが返ると思っていたのですが、どうもそうではないようです。

ざっとコードを読んだだけなので間違っている可能性もあります。 この情報を信用しないで必要ならソースを読んでください。

とりあえず動いたのが次のコードです。

  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
    editorState: () => {
      const rootNode = $getRoot();
      rootNode.clear();

      const parts = initMarkdownValue.split(/(\r?\n|\t)/);
      const elementNode = $createParagraphNode();

      parts.map((part)=> {
        if (part === '\n' || part === '\r\n') {
          return $createLineBreakNode();
        } else if (part === '\t') {
          return $createTabNode();
        } else {
          return $createTextNode(part);
        }
      }).forEach((node)=> {
          elementNode.append(node);
      });

      rootNode.append(elementNode);
      rootNode.selectEnd();
    }
    //editorState: () => $convertFromMarkdownString(initMarkdownValue, TRANSFORMERS),
  };

忘れずに必要な関数のインポートもしましょう。

import {
  $createTabNode,
  $createLineBreakNode,
  $createTextNode,
  $createParagraphNode,
} from 'lexical';

実行すると、次のようになります。

lexical 2

意図通りの結果になりました。

editorState にセットしている無名関数では、$getRoot() して rootNode を取得して、 この rooNodeinitMarkdownValue の内容を使って EditorState DOM インスタンスを 構築しているだけです。

改行やタブなどを考慮しているのでややこしくなっていますが、もし単なる Hello, World! という文字列だけを使って構築するのであれば、次のようになります。

const textNode = $createTextNode('Hello, World!');
const elementNode = $createParagraphNode();
elementNode.append(textNode);

const rootNode = $getRoot();
rootNode.append(elementNode);

変更内容をコールバックする方の修正

エディタの内容が変更になったときに変更された内容をコールバックする方は、 現状のままでも機能します。

      editor.update(()=>{
        const newMarkdownValue = $convertToMarkdownString(TRANSFORMERS);
        onTextChange(newMarkdownValue);
      });

なのでこのままでも良いのですが、一応 $convertToMarkdownString を使わない方法で実装します。

      /*
      editor.update(()=>{
        const newMarkdownValue = $convertToMarkdownString(TRANSFORMERS);
        onTextChange(newMarkdownValue);
      });
      */
      
      const stringifiedEditorState = JSON.stringify(editorState.toJSON());
      const parsedEditorState = editor.parseEditorState(stringifiedEditorState);
      const newMarkdownValue = parsedEditorState.read(() => $getRoot().getTextContent());
      onTextChange(newMarkdownValue);

editorState.toJSON() して EditorState の DOM を JSON として書き出してからなんやかんやして、プレーンテキストにします。

まとめ

今回の修正を反映したコードを掲載します。

initMarkdownValue, newMarkdownValue としていた変数名は initTextValue, newTextValue に変更しました。

src/index.js

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

const initTextValue = document.getElementById('mytextarea').value;
const onTextChange = (newTextValue)=>{
  document.getElementById('mytextarea').value = newTextValue;
};

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

src/Editor.js

import React from "react";
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 {
  $getRoot,
  $getSelection,
  $createTabNode,
  $createLineBreakNode,
  $createTextNode,
  $createParagraphNode,
} from 'lexical';


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, onTextChange }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerUpdateListener(({editorState}) => {
      const stringifiedEditorState = JSON.stringify(editorState.toJSON());
      const parsedEditorState = editor.parseEditorState(stringifiedEditorState);
      const newTextValue = parsedEditorState.read(() => $getRoot().getTextContent());
      onTextChange(newTextValue);

      onChange(editorState);
    });
  }, [editor, onChange]);
}

function Editor( { initTextValue='', onTextChange=(f)=>f } ) {
  const initialConfig = {
    namespace: 'MyEditor',
    theme,
    onError,
    editorState: () => {
      const rootNode = $getRoot();
      rootNode.clear();

      const parts = initTextValue.split(/(\r?\n|\t)/);
      const elementNode = $createParagraphNode();

      parts.map((part)=> {
        if (part === '\n' || part === '\r\n') {
          return $createLineBreakNode();
        } else if (part === '\t') {
          return $createTabNode();
        } else {
          return $createTextNode(part);
        }
      }).forEach((node)=> {
          elementNode.append(node);
      });

      rootNode.append(elementNode);
      rootNode.selectEnd();
    }
  };

  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} onTextChange={onTextChange}/>
    </LexicalComposer>
  );
}

export default Editor;

以上です。