Home About
lexical , React , Note Taking

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

前々回 および 前回から引き続いて 今回は、 lexical の Editor で変更があったら index.html にそれを反映する 方法を調べます。

小手調

Editor で変更があったらコンソールにその内容を出すように変更してみます。

src/Editor.js の変更差分。

   const [editor] = useLexicalComposerContext();
   useEffect(() => {
     return editor.registerUpdateListener(({editorState}) => {
+      editor.update(()=>{
+        const newMarkdownValue = $convertToMarkdownString(TRANSFORMERS);
+        console.log(newMarkdownValue);
+      });
+
       onChange(editorState);
     });
   }, [editor, onChange]);

要するに OnChangePlugin 内でエディタの内容に変更があったらコールバックされる部分で、 現在の editorState を マークダウンテキストに変換する処理を入れています。

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

editor.update(()=>{}) の関数内では $なんとか スペシャル関数が使えるとかなんとか。 このページに説明があります。

// Inside the `editor.update` you can use special $ prefixed helper functions.

よくわからないのだが、とりあえずこれで内容に変更がはいるたびに コンソールに変更後の内容を書き出すことはできるようになりました。

あとはこれを dist/index.html で把握できるようにする必要があります。

コールバック

変更が発生したらそれを受け取ることができるように、 リスナー(コールバック)関数を用意して、追加します。

コールバックするための関数は、 onTextChange という名前にします。

最初に src/index.js に onTextChange 関数を定義して、それを Editor へ渡します。

onTextChange 内では、変更された内容を受け取ったら(コールバックされてきたら)、 mytextarea の id の textarea へそれを反映します。

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

...

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

src/Editor.js ではこの onTextChange 関数を受け取って OnChangePlugin に渡します。

function Editor( { initMarkdownValue='', onTextChange=(f)=>f } ) {
  ...

  return (
    <LexicalComposer initialConfig={initialConfig}>
      ...
      <OnChangePlugin onChange={onChange} onTextChange={onTextChange}/>
    </LexicalComposer>
  );
}

OnChangePluginonTextChange 関数を受け取って 変更が発生したときに、 コンソールに出す代わりに onTextChange(newMarkdownValue) としてコールバックします。

function OnChangePlugin({ onChange, onTextChange }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerUpdateListener(({editorState}) => {
      editor.update(()=>{
        const newMarkdownValue = $convertToMarkdownString(TRANSFORMERS);
        onTextChange(newMarkdownValue);
        //console.log(newMarkdownValue);
      });

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

さらに textarea 要素を display:none; にしていたものを display:block; にして、表示を復活させておきます。

この状態で実行すると、次のように変更がそのまま textarea に反映できるようになりました。

lexical 1

まとめ

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

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:block;">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 onTextChange = (newMarkdownValue)=>{
  document.getElementById('mytextarea').value = newMarkdownValue;
};

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

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, onTextChange }) {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerUpdateListener(({editorState}) => {
      editor.update(()=>{
        const newMarkdownValue = $convertToMarkdownString(TRANSFORMERS);
        onTextChange(newMarkdownValue);
        //console.log(newMarkdownValue);
      });

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

function Editor( { initMarkdownValue='', onTextChange=(f)=>f } ) {
  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} onTextChange={onTextChange}/>
    </LexicalComposer>
  );
}

export default Editor;

以上です。

次回は、 エディタの初期値を与える部分で 手抜きして既存の markdown モジュールを使っていたが、 問題が実はあったので、その修正を試みます。

Liked some of this entry? Buy me a coffee, please.