🖼️

【Next.js】MarkdownエディタにD&Dしたらプレビューに表示させたい

2022/07/16に公開1

こんにちは、Zenn初投稿な者です。
react-md-editorでD&Dしたら画像をプレビューに表示するのに結構ハマったので、備忘録的にまとめます。

完成形

react-md-editor is 何?

react-md-editorは、多機能かつ高い拡張性を持つReact用のMarkdownエディタを提供します。
Next.jsもサポートされているため、今回はその設定方法も見ていきましょう。

yarn install

Next.jsのプロジェクトが立ち上がっていることを前提に、追加で必要なパッケージをインストールします。

yarn install @uiw/react-markdown-preview @uiw/react-md-editor next-remove-imports

next.config.jsのカスタマイズ

お次に、react-md-editorをNext.jsで使用できるように、next.config.jsを編集します。

const removeImports = require("next-remove-imports");

module.exports = removeImports({
  reactStrictMode: true
});

ここでは、node_modulesにあるすべてのパッケージから、すべての .less/.css/.scss/.sass/.stylのimportを削除しています。
確かこれをしないと「node_modulesからのCSSのimportがあるんですけど!?」って感じでnextに怒られた気がします(ガバ

さて、実はもう一つ設定しなければいけないことがあるんですが、それは今は一旦置いておきます。

Markdownエディタを作る

さて、では実際にreact-md-editorを使っていきましょう。
と、その前に今回使うディレクトリ構成を先に載せておきます。

root
 |
 ├─ components
 |   |
 |   └─ MdEditor
 |       |   index.tsx
 |       |
 |       └─ utils
 |           fileUploader.tsx
 |           insertToTextArea.tsx
 |           onImagePasted.tsx
 |
 └─ pages
     index.tsx

ではまず、./components/MdEditor/index.tsxを作りましょう。

./components/MdEditor/index.tsx
import MDEditor from "@uiw/react-md-editor";
import { useState } from "react";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

const MdEditor = () => {
  const [markdown, setMarkdown] = useState<string | undefined>();

  return (
    <div data-color-mode="light">
      <MDEditor
        value={markdown}
        onChange={(value) => {
          setMarkdown(value);
        }}
        height={440}
        textareaProps={{
          placeholder: "Fill in your markdown for the coolest of the cool."
        }}
        hideToolbar
      />
    </div>
  );
};

export default MdEditor;

そうしたら、./pages/index.tsxを次のように編集します。

./pages/index.tsx
import dynamic from "next/dynamic";

const MdEditor = dynamic(import("../components/MdEditor"), {
  ssr: false,
  loading: () => <div>now loading</div>
});

const IndexPage = () => <MdEditor />;

export default IndexPage;

これでyarn devをしてみると、Markdownエディタがルートに表示されるはずです。

ここでのポイントはdynamic()です。
ここで<MdEditor>をSSRによってレンダリングしないように設定することで、react-md-editor/MDEditorをNext.jsのSSR下でも使用できるようにしています。

D&D時にファイルをプレビューに表示する

さて、それではここにD&D時の処理を追加していきましょう。
react-md-editoreasyMDEのようにimageUploadFunction()のようなものはないので、onPasteonDropを併用して実装していきます。

onImagePasted.tsxinsertToTextArea.tsxfileUploader.tsxを以下のように編集してください。

./components/MdEditor/utils/onImagePasted.tsx
import type { SetStateAction } from "react";
import fileUpload from "./fileUploader";
import insertToTextArea from "./insertToTextArea";

const onImagePasted = async (
  dataTransfer: DataTransfer,
  setMarkdown: (value: SetStateAction<string | undefined>) => void
) => {
  const files: File[] = [];
  for (let index = 0; index < dataTransfer.items.length; index += 1) {
    const file = dataTransfer.files.item(index);

    if (file) {
      files.push(file);
    }
  }

  await Promise.all(
    files.map(async (file) => {
      const url = await fileUpload(file);
      const insertedMarkdown = insertToTextArea(`![](${url})`);
      if (!insertedMarkdown) {
        return;
      }
      setMarkdown(insertedMarkdown);
    })
  );
};

export default onImagePasted;
./components/MdEditor/utils/insertToTextArea.tsx
const insertToTextArea = (intsertString: string) => {
  const textarea = document.querySelector("textarea");
  if (!textarea) {
    return null;
  }

  let sentence = textarea.value;
  const len = sentence.length;
  const pos = textarea.selectionStart;
  const end = textarea.selectionEnd;

  const front = sentence.slice(0, pos);
  const back = sentence.slice(pos, len);

  sentence = front + intsertString + back;

  textarea.value = sentence;
  textarea.selectionEnd = end + intsertString.length;

  return sentence;
};

export default insertToTextArea;
./components/MdEditor/utils/fileUploader.tsx
const fileUploader = (file: File) => {
  const imageURL = URL.createObjectURL(file);

  return imageURL;
};

export default fileUploader;

各関数の役割は、以下の通りです。

  • onImagePasted()
    • Fileの抽出とリスト化、アップロードの実行
  • insertToTextArea()
    • 元あったカーソルの位置に![](url)を挿入する
  • fileUploader()
    • ファイルのアップロード

今回はお試しということで、fileUploader()はオンラインストレージサービスと接続したりはしていません。
実際に本番で使用するときは、ここの関数を適宜変更してください。

そうしたら、./components/MdEditor/index.tsxを再度以下のように編集してください。

./components/MdEditor/index.tsx
import MDEditor from "@uiw/react-md-editor";
import { useState } from "react";
+ import onImagePasted from "./utils/onImagePasted";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

const MdEditor = () => {
  const [markdown, setMarkdown] = useState<string | undefined>();

  return (
    <div data-color-mode="light">
      <MDEditor
        value={markdown}
        onChange={(value) => {
          setMarkdown(value);
        }}
+         onPaste={async (event) => {
+           await onImagePasted(event.clipboardData, setMarkdown);
+         }}
+         onDrop={async (event) => {
+           await onImagePasted(event.dataTransfer, setMarkdown);
+         }}
        height={440}
        textareaProps={{
          placeholder: "Fill in your markdown for the coolest of the cool."
        }}
        hideToolbar
      />
    </div>
  );
};

export default MdEditor;

これで実装は完了です。
再びブラウザを確認すると、D&DとCopy&Pasteで画像が表示できるMarkdownエディタが表示されているはずです。

おわりに

今回はreact-md-editorをNext.jsのSSR下で使えるようにし、D&D・Copy&Pasteで画像を表示させる処理を実装しました。
個人的に、SSR関連で何か怒られたときはdynamic()ssr: falseをしておけば、大体直るもんだと信じています。
でも一度に全てのコンポーネントがマウントされないのは、それはそれでUXが良いわけではないので、安易にdynamic()に頼らない生き方をしたいと思った今日この頃でした。

またね。

Discussion

はなむ++はなむ++

素敵な記事をありがとうございます。
私もこの記事を見ながらマークダウンエディタを作っているところです。

さて、実はもう一つ設定しなければいけないことがあるんですが、それは今は一旦置いておきます。

next.config.jsのカスタマイズにあるこの文がとても気になりますので、教えていただけないでしょうか。
記事の中に書かれていましたらどの部分を指すのか教えていただけると幸いです。