超簡単にMarkdownエディタをWebページに実装可能な"SimpleMDE"


もう平成も終わろうとしているのに未だ神Excel依存な弊社で、せめて所属部署、所属課だけでも良いのでMarkdownを使ってくれ...と思い、色々考えてました。

まず「Markdownは難しくない」と感じてもらう為に、エディタ/ビューアを用意しなければなりません。
しかし、いきなりVSCodeだのStackblitzだの言っても確実にコケると直感しました。

この手のは、とっつきで「難しそう」と感じられてしまったら確実に失敗するんですよね。

イントラにアクセスするだけで誰でも使える、Webページ埋込タイプのエディタが無いか探してたら、丁度良い物があるではないですか。

SimpleMDE

JavaScript/CSSの2ファイルをロードするだけで使える、お手軽Markdownエディタです。

つかいかた

SimpleMDE.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simplemde@latest/dist/simplemde.min.css">
        <script src="https://cdn.jsdelivr.net/npm/simplemde@latest/dist/simplemde.min.js"></script>
    </head>
    <body>
        <textarea id="mde"></textarea>
    </body>
    <script>
        const mde = new SimpleMDE({
            element: document.getElementById("mde")
        });
    </script>
</html>

たったこれだけで立派なMarkdownエディタが実装出来ます。
ちょうど公式サイトの一番上のデモと同じ感じになります。

機能の追加/削除

素の状態でも十分使えるスグレモノですが .md ファイルの読込/保存やDataURLで軽量バイナリ埋込など、痒い所に手を届かせる自作の機能も追加可能です。

インスタンス生成時にオプション指定可能です。

new SimpleMDE({
    element: document.getElementById("mde"),
    toolbar: [
    ...,
    {
        name: "save", // 機能名
        action: saveHandler, // アクションハンドラ
        className: "fa fa-download", // アイコン
        title: "Save Markdown" // 説明
    }]
});

function saveHandler(editor){ // コンテキスト
    saveAs(new Blob([editor.value()]), "markdown.md");
    editor.codemirror.focus();
}

"ツールバー" が機能の実体なので、MixInを使って toolbar オプションに新しい機能を宣言することで追加可能です。
なお、MixInを使用しないと上書きになるので、BoldやItalicなどの基本的な機能も再宣言しなければなりません。

アイコンは FontAwesome の物が使えます。
ハンドラにはエディタのコンテキストが引数として渡されます。

エディタとプレビューのフォントを変えたい場合は、既存のCSSを上書きする事で可能です。

/* エディタ領域 */
.CodeMirror {
    font-family: "MigMix 1M";
}
/* プレビュー領域 */
.editor-preview,
.editor-preview-side {
    font-family: "M PLUS 1p";
}

/* プレビュー領域内コード */
.editor-preview code,
.editor-preview-side code {
    font-family: "Kosugi";
}

NotoSansやHiraginoなど、お好きなフォントを使ってください。
あ、僕はMigMixが好きです。

Tips: D&D(ドラッグアンドドロップ)対応

放り込みたくなりますよね。

// ウィンドウ全域でD&D禁止
addEventListener("dragover", (event)=>{
    event.preventDefault();
    event.dataTransfer.dropEffect = "none";
});
addEventListener("drop", (event)=>{
    event.preventDefault();
});

const dnd = document.getElementsByClassName("CodeMirror-scroll")[0];
// エディタ領域のみD&D許可
dnd.addEventListener("dragover", (event)=>{
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
});
dnd.addEventListener("drop", async(event)=>{
    event.preventDefault();
    mde.value(await fileReader(event.dataTransfer.files[0]));
    mde.codemirror.focus();
});

// ここら辺はお好みで
function fileReader(blob){
    return new Promise((res, rej)=>{
        const fr = new FileReader();
        fr.addEventListener("load", () => res(fr.result));
        fr.addEventListener("error", () => rej(fr.error));
        fr.readAsText(blob);
    });
}

これでエディタ領域のみD&Dが可能となります。

Tips: ファイルピッカー呼出

ツールバーにファイル入力を設けたいとき。
もはやHTML初心者講座になってる気がしないでもないですが、とりあえず書いときます。

// Toolbar アクションハンドラ
async function loadHandler(editor){
    editor.value(await filePicker().then(b => fileReader(b)));
    editor.codemirror.focus();
}

function filePicker(){
    return new Promise((res)=>{
        const input = document.createElement("input");
        input.type = "file";
        input.id = "file";
        input.style.display = "none";
        document.body.appendChild(input);

        const file = document.getElementById("file");

        file.addEventListener("change", function fn(event){
            input.removeEventListener("change", fn);
            document.body.removeChild(file);

            res(event.target.files[0]);
        });

        file.click();
    });
}

本当は once: true オプションを使いたいのですが、神Excelが横行してるような職場のブラウザシェアを考えるとね...