ブログ記事の投稿画面をReactで書いたときの導入手順について。

日頃から文章をマークダウン記法で書くことが多かったので、同じ感覚で書けるエディタになるように実装した。

1からエディタを作ることも考えたが、react-simplemde-editorというライブラリが良さそうだったので今回はそちらを採用した。

環境

導入

$ yarn add react-simplemde-editor
index.tsimport React from "react";
import SimpleMDE from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";

const MarkdownEditor = () => {
    return <SimpleMDE id="simple-mde" />;
}
export default MarkdownEditor;

起動してブラウザを開くとエディタが表示される。

img.png

初期値を表示

SimpleMDEコンポーネントのvalue属性に文字列や変数を指定すると、初期値としてエディタに表示される。

index.tsconst MarkdownEditor = () => {
    const [value, setValue] = useState("Initial value");
    
    return <SimpleMDE id="simple-mde" value={value} />;
}

入力値を変数に代入

SimpleMDEコンポーネントのonChangeに指定したコールバック関数がエディタの内容が変更されるたびに実行される。コールバック関数は引数にエディタ内の文字列を持つので、更新用関数に渡してstate変数に展開させる。

index.tsconst MarkdownEditor = () => {
    const [value, setValue] = useState("Initial value");

    const onChange = useCallback((value: string) => {
        setValue(value);
    }, []);

    return <SimpleMDE id="simple-mde" value={value} onChange={onChange} />;
}

画像をクリップボード経由またはドラッグ&ドロップで貼り付ける

Qiitaの記事投稿画面のように画像をクリップボード経由かドラッグ&ドロップで貼り付けられるようにした。

SimpleMDEには複数のイベントリスナーが標準で用意されており、SimpleMDEコンポーネントのevents属性に任意のイベント種別とコールバック関数を指定することで利用できる。
今回は、クリップボード貼り付けとドラッグ&ドロップを利用したかったのでdropとpasteを指定した。

コールバック関数内では対象が画像ファイルであるかのチェックとファイルのBLOBを取得し、画像アップロードを非同期で行わせる。
画像アップロード処理は下記コードでは割愛。画像アップロードが成功すると画像URLが返却されるようにした。

画像は![](画像URL)の形式で本文中に挿入したかったのでreplaceSelection関数を利用。replaceSelectionはインスタンス関数なのでSimpleMDEのインスタンスを予め用意する必要がある。SimpleMDEコンポーネントのgetMdeInstance属性にコールバック関数を指定することでインスタンスが取得できるのでブロック変数に代入して利用する。

index.tsconst MarkdownEditor = () => {

    let simpleMde: EasyMDE;
    const getInstance = (instance: EasyMDE) => {
        simpleMde = instance;
    };

    const [value, setValue] = useState("Initial value");

    const onChange = useCallback((value: string) => {
        setValue(value);
    }, []);
    
    const uploadImage = async () => {
        try {
            // 画像アップロード処理を実行
        } catch (error) {
        }
    }

    const handleDrop = (data, e) => {
        if (e.clipboardData.files === undefined || e.clipboardData.files.length === 0) {
            return;
        }

        const files = e.dataTransfer?.files;
        const file = files[0];

        if (
            file.type === "image/png" ||
            file.type === "image/jpeg" ||
            file.type === "image/gif"
        ) {
            const uploadedImageUrl = uploadImage();
            simpleMde.codemirror.replaceSelection("![](" + uploadedImageUrl + ")");
        }
    };

    const handlePaste = (data, e) => {
        if (e.clipboardData.files === undefined || e.clipboardData.files.length === 0) {
            return;
        }

        const files = e.clipboardData.files;
        const file = files[0];

        if (file.type === "image/png") {
            const uploadedImageUrl = uploadImage();
            simpleMde.codemirror.replaceSelection("![](" + uploadedImageUrl + ")");
        }
    };

    return<SimpleMDE
        id="simple-mde"
        getMdeInstance={getInstance}
        value={value}
        onChange={onChange}
        events={{ drop: handleDrop, paste: handlePaste }} />;
}

あとがき

react-simplemde-editorを採用したおかげで手間をかけることなく要件が満たせたエディタが作れたと思う。
公式ドキュメントを見ると他にも色々な機能がありそうなので今後試していきたい。