vue-codemirror を試す
CodeMirror っていう JavaScript 製のエディタコンポーネントの Vue.js 用コンポーネント vue-codemirror を触ってみたときの備忘録。
最終的なコードはここにある。
vue-codemirror をプロジェクトに読み込む
import Vue from 'vue' import VueCodemirror from 'vue-codemirror' import 'codemirror/lib/codemirror.css' Vue.use(VueCodemirror)
こんな感じ。
これを plugins/vue-codemirror.js
に切り出して、 main.js
で
import '@/plugins/vue-codemirror'
コンポーネントの利用
ドキュメント通りでできるけど、このあとのベースラインとして。
<template lang="pug"> #app codemirror(v-model="code" :options="cmOptions") </template> <script> export default { name: 'App', data() { return { code: '', cmOptions: { } } } } </script>
インデントまわりのカーソル移動をいい感じにする
これでだいたいエディタっぽいアプリをつくることはできるんだけど、インデントまわりに不満がある。個人的には以下のようにしたい。
- インデントはタブではなく、ホワイトスペースを利用してほしい
- インデントの先頭でバックスペースを押したとき、文字単位ではなくインデント単位で直前の空白を削除してほしい
CodeMirror のオプションを指定したらいい感じになるかなと思ったけど、うまくいかなかった。調べたら以下のような先達の tips があった。
Setting for using spaces instead of tabs · Issue #988 · codemirror/CodeMirror · GitHub
しかしこの通りにやってもうまくいかなかったので、ベースを利用しつつ、自分の好みになるように書いてみた。
Vue.use(VueCodemirror, { options: { // based on https://github.com/codemirror/CodeMirror/issues/988#issuecomment-549644684 extraKeys: { Tab: (cm) => { if (cm.getMode().name === 'null') { cm.execCommand('insertTab'); } else { if (cm.somethingSelected()) { cm.execCommand('indentMore'); } else { cm.execCommand('insertSoftTab'); } } }, Backspace: (cm) => { if (!cm.somethingSelected()) { let cursorsPos = cm.listSelections().map((selection) => selection.anchor); let indentUnit = cm.options.indentUnit; let shouldDelChar = false; for (let cursorPos of cursorsPos) { const { start, end, string, type } = cm.getTokenAt(cursorPos) if (type !== 'indent' && type !== null || start !== 0 || end !== cursorPos.ch || end === 0 || string.trimStart() !== '' || end % indentUnit > 0) { shouldDelChar = true break } } if (!shouldDelChar) { cm.execCommand('indentLess'); } else { cm.execCommand('delCharBefore'); } } else { cm.execCommand('delCharBefore'); } }, 'Shift-Tab': (cm) => cm.execCommand('indentLess') } } })
保存アクションで dirty flag を消す
CodeMirror には、 CMD-S (CTRL-S) で保存するキーバインドがデフォルトで定義されており、その際呼び出されるアクションとして save という (カラの) アクションが定義されている。
また、 CodeMirror コンポーネント自体にも save という (カラの) メソッドが定義されている (上記のアクションとは別物)。
これらをくみあわせて、保存アクションをしたときに dirty flag をリセットするようにしたい。
ちなみに CodeMirror には開発者が活用できる以下のような内部状況フラグがある。
- https://codemirror.net/doc/manual.html#markClean
- 最初は "clean" だが、なにか修正がくわわると not clean。そしてこの API で markClean するとふたたび "clean" となる。
- https://codemirror.net/doc/manual.html#changeGeneration
- clean flag は clean か clean ではないかの2値だが、より発展したものとして世代管理できるこちらの API もある。
まずは save action の上書きから。
// default save action の上書き VueCodemirror.CodeMirror.commands.save = (cm) => { // 保存されるときに "clean" 扱いにする cm.markClean() // 現状ではなにも内容の定義されていない、コンポーネントの save メソッドを呼び出しておく cm.save(cm) }
つぎはコンポーネント側。
codemirror#editor(ref="cmComponent" v-model="code" :options="cmOptions" @input="cmChanged")
export default { name: 'App', data() { return { code: '', isClean: true, cmOptions: { // ... } } }, computed: { cmComponent() { return this.$refs.cmComponent } }, mounted() { this.cmComponent.codemirror.save = (cm) => { this.isClean = cm.isClean() } }, methods: { cmChanged() { this.isClean = this.cmComponent.codemirror.isClean() } } }
コンポーネントの save メソッド側で、 手元のコンポーネントの clean フラグに CodeMirror の isClean
フラグの状況を反映させる。
また、 CodeMirror コンポーネントの change 時にもそのフラグの状況を反映させる。
そもそも CodeMirror の clean フラグを使わなくっても、 CMD-S を自前でハンドリングしたり、 change のときだけ not clean にしたりすればいいんじゃ?って思うかもだけど、実は CodeMirror 側では、 Undo 等して、編集差分が発生しなくなったときに clean になったりする。なので、 clean フラグ自体は CodeMirror 側に負わせるのがよい。
(とはいえ、 save action とコンポーネントの save method をどちらもオーバライドしているこのやりかたはすこしくどすぎるかも。 save action の上書きだけでよさそう。あと、 change イベントと save action をハンドリングして内部のフラグをもってきているから、いまいちリアクティブみがない。もっとよい方法ないのかな。)
集大成として Markdown editor もどき
以上のサンプルコードとして、 Markdown editor もどきをつくった。
再掲になるけど、
べつだんリアルタイムプレビューを出すのはそこまで難しくないんだけど、 clean フラグのとりまわしをやりたかったので、 あえて CMD-S を押さないとプレビューに反映されないようになっている。