CodeMirrorでブラウザにコードエディタを実装
前回ブラウザ上のコードエディタを自力で作成しようと試みました。結果は何とか動くようになったものの問題点が多そうです。
そこで、今回はMDN Web Docsなどでも用いられている、CodeMirrorというライブラリを使ってみようと思います。
前回に引き続き、HTMLのハイライトと、検索だけではなく、今回は、ライン番号やサジェスチョンまで実装することができました。
環境はCodeMirror6.01、開発環境はWindows11+GoogleChromeです。
開発環境のインストール
CodeMirrorはいくつかのパッケージに分割されて開発されています。最初はどのようなものがあるのかわかりづらいのでまとめてダウンロードしてしまいます。
CodeMirror周辺のパッケージをまとめてインストールするためのパッケージをgitでcloneするか、ブラウザでダウンロードして展開します。
コアパッケージのルートフォルダ(dev)に移動して、PowerShellなどのターミナルから次のコマンドを実行します。
インストール後初回のビルドまでしてくれますが、以降は次のようにしてビルドする事が可能です。
npm経由でもビルドすることができます。アプリケーションのビルドではなく開発環境のビルドなのでbuildというキーワードはつかっていないようです。
サンプルを稼働させる
ダウンロードしたライブラリの中にはサンプルコードも含まれていますので、初期状態から起動させることができます。
とすることで、デバッグサーバーが起動します。ローカルホストの8090番ポートにアクセスすると次のようなコードエディタが表示されます。
この時のHTMLソースコードは次のようになっています。
index.html
<!doctype html>
<meta charset=utf8>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CM6 demo</title>
<style>
.cm-editor { height: 300px; border: 1px solid #ddd}
.cm-scroller { overflow: auto; }
</style>
<h1>CM6</h1>
<div id=editor></div>
<script type=module src="_m/demo.js"></script>
モジュールとして参照しているdemo.jsのソースは次のようになっています。
demo.js
import { EditorView, basicSetup } from "/_m/__/codemirror/dist/index.js";
import { javascript } from "/_m/__/lang-javascript/dist/index.js";
window.view = new EditorView({
doc: 'console.log("Hello world")',
extensions: [
basicSetup,
javascript(),
],
parent: document.querySelector("#editor")
});
これらのコードはプロジェクトルートのdemoフォルダに入っています。demo.jsはデバッグ実行している際とはimportのパスが少し違います。
詳しくは公式のドキュメントを参照してもらえばと思いますが、簡単に説明をすると、 EditorViewのコンストラクタに、EditorViewConfigオブジェクト(オプション)を渡しています。
このオブジェクト中のdocとextensionsフィールドは、継承しているEditorStateConfigのものです。EditorViewインスタンス生成時の処理ではstateフィールドに値が存在しない場合、自身のdoc,selection,exteinsionsフィールドをEditorState.createに渡してEditorStateを作成してstateフィールドにセットします。
このような仕組みになっているのは、おそらくEditorStateオブジェクトがイミュータブルで直接編集してはいけないものだからだと思います。
extenstionsフィールドに拡張ライブラリを渡します。basicSetupを指定することで、ハイライト、検索・置換(Ctrl+Fキーで表示)、ラインナンバー付与等の一般的に必要な機能のほとんどを設定できます。言語はサンプルではJavaScrip()tが渡されていました。
parentフィールドにはエディタを設定する要素を渡します。
HTMLエディタへ変更
JavaScripエディタををHTMLに変えるのと、readOnly属性を付与できるようにしたコードが次のものです。HTML側から呼び出せるようにビューのそのもをwindowに設定するのではなく、ビューを返すメソッドを設定するように変更しました。
demo.js
import { EditorView, basicSetup } from "codemirror";
import { html } from "@codemirror/lang-html";
import { EditorState } from "@codemirror/state";
window.makeView = (text = "", blnReadOnly = false, selector = "#editor") => {
return new EditorView({
doc: text,
extensions: [basicSetup, html(), EditorState.readOnly.of(blnReadOnly)],
parent: document.querySelector(selector),
});
};
html側では、読み出しと保存機能を付けました。コードのみの実装となりますが、スクリプトから編集する際のコードも書いています。エディタ内のドキュメントはイミュータブルなEditorStateの中に含まれます。EditorStateオブジェクトはview.stateで取得できますが、直接編集してはいけません。
index.html
<!DOCTYPE html>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Code Mirror HTML sample</title>
<style>
.cm-editor {
height: 300px;
border: 1px solid #ddd;
}
.cm-scroller {
overflow: auto;
}
</style>
<h1>HTML EDITOR</h1>
<div id="editor"></div>
<div class="control" style="margin-top: 1em">
<button onclick="readData();" style="width: 30%">読出</button>
<button onclick="writeData();" style="width: 30%; margin-left: 1em">
保存
</button>
</div>
<script type="module" src="_m/demo.js"></script>
<script>
let view = null;
const changeData = () => {
// コードからの編集時
if (view) {
let max = view.state.doc.toString().length;
view.dispatch({
changes: {
from: 0,
to: max,
insert: "<span>read<span>\n",
},
});
//view.state.readOnly = false;
} else {
console.log("read-error");
}
};
const readData = () => {
// 読み出し時は一度ビューを破棄して再生成
if (view) {
view.destroy();
}
view = makeView("<span>read<span>\n");
};
const writeData = () => {
// 他のファイルへ書き込む
if (view) {
console.log(view.state.doc.toString());
} else {
console.log("write-error");
}
};
window.onload = () => {
view = makeView();
};
</script>
バンドル
CodeMirror周辺のライブラリは、ECMAScriptやCommonJSのモジュールなのでそのままブラウザで利用する事はできません。
そのため、bundlerかloaderが必要なのですが、ここではwebpackでバンドルします。
npmでwebpackをインストールします。後で必要になるのでwebpack-cliも合わせてインストールします。直接アプリでは使用しないので開発環境にインストールします。
webpack用の設定ファイル(webpack.config.js)をプロジェクトのルートへ作成して記述します。
webpack.config.js
const path = require("path");
module.exports = {
entry: "./demo/demo.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
mode: "production",
};
entryフィールドにdemo.jsへのパス、outputフィールドには出力先のフォルダと、ファイル名を設定します。
modeの値を「production」にすると、コードを圧縮した本番用の.jsファイル。「development」を指定するとデバッグ用の.jsファイルとなります。
今回のコードはpackage.jsonファイルに「"private": true」のエントリーが設定してありますが、もしないものを用いる場合は「"main": "xxx.js"」のエントリーを消して、「"private": true」を追加します。
ファイルが設定できたら、npxからwebpackを実行します。
package.jsonの設定をするなら、scriptsに「"build": "webpack"」のエントリーを加え「npm run build」からビルドするようにします。
バンドルされたファイル(bundle.js)と共にindex.htmlをコピーして、本番環境に配置しHTML側のdemo.jsの読み込みの部分をbundle.jsと変更する(この時type="module"の指定は不要です)ことで本番環境が構築できます。
ここではWebpackの使い方の説明を省きましたが、もう少し丁寧にWebpackでJavaScriptの開発環境を構築したい場合は、リンク先の記事を参照していただけると幸いです。