Vue.jsとAce.jsで、ブラウザで動作するマークダウンエディタを作ってみた

f:id:sho_ya:20190116131614p:plain

ブラウザ上で動くVue.jsマークダウンエディタはVueのチュートリアルにもあるが、Aceエディタ化されたものが欲しかったので、githubで似たようなものを見つけて、改造した。 エディタの方に行番号がついていて、それをAce側で制御している。

本当はキャプションの画像にGIFを入れて、動いている所を見せたかったのだが、何故かgifのアップロードできないので、今後でき次第差し替える。

module.exports = {
    render: function (h) {
        var height = this.height ? this.px(this.height) : '100%'
        var width = this.width ? this.px(this.width) : '100%'
        return h('div',{
            attrs: {
                style: "height: " + height  + '; width: ' + width,
            }
        })
    },
    props:{
        value:{
            type:String,
            required:true
        },
        lang:String,
        theme:String,
        height:true,
        width:true,
        options:Object
    },
    data: function () {
        return {
            editor:null,
            contentBackup:""
        }
    },
    methods: {
        px:function (n) {
            if( /^\d*$/.test(n) ){
                return n+"px";
            }
            return n;
        }
    },
    watch:{
        value:function (val) {
            if(this.contentBackup !== val){
                this.editor.setValue(val,1);
                this.contentBackup = val;
            }
        },
        theme:function (newTheme) {
            this.editor.setTheme('ace/theme/'+newTheme);
        },
        lang:function (newLang) {
            this.editor.getSession().setMode('ace/mode/'+newLang);
        },
        options:function(newOption){
            this.editor.setOptions(newOption);
        },
        height:function(){
            this.$nextTick(function(){
                this.editor.resize()
            })
        },
        width:function(){
            this.$nextTick(function(){
                this.editor.resize()
            })
        }
    },
    beforeDestroy: function() {
        this.editor.destroy();
        this.editor.container.remove();
    },
    mounted: function () {
        var vm = this;
        var lang = this.lang||'text';
        var theme = this.theme||'monokai';

        require('brace/ext/emmet');
        var editor = vm.editor = ace.edit(this.$el);

        this.$emit('init',editor);

        editor.$blockScrolling = Infinity;
        editor.setOption("enableEmmet", true);
        editor.getSession().setMode('ace/mode/'+lang);
        editor.setTheme('ace/theme/'+theme);
        editor.setValue(this.value,1);
        this.contentBackup = this.value;

        editor.on('change',function () {
            var content = editor.getValue();
            vm.$emit('input',content);
            vm.contentBackup = content;
            vm.$root.editorValue = content;
        });
        if(vm.options)
            editor.setOptions(vm.options);
    }
}


document.querySelector('body').append(document.createElement('div'))
window.aceVue = new Vue({
    el: '#app',
    template:'<div><editor v-model="content" lang="html" height="500" @init="initEditor"></editor><div id="preview" v-html="cmpledmkdown"></div></div>',
    data:{
        content:"",
        editorValue: ''
    },
    components:{
        editor:require('vue2-ace-editor')
    },
    computed: {
        cmpledmkdown: function () {
            // console.log(this.value);
            return this.editorValue ? marked(this.editorValue, { sanitize: false }) : '';
        }
    },
    methods:{
        initEditor:function (editor) {
            require('brace/mode/html');
            require('brace/theme/monokai');
        }
    }
})