CodeMirror などで実装した Markdown エディタのリアルタイムプレビューにスクロール位置同期を実装する

StackEdit のようなマークダウンエディタには、リアルタイムタイムプレビューが実装されていて、実際に文章を書いているとリアルタイムにプレビュー画面に反映されます。機能が豊富なエディタには大体備わってますね。

さて、これをCodeMirrorやACEなどブラウザベースIDEで実装しようとすると、リアルタイムプレビューまではググってサンプルなどを動かしてるとすぐできます。が、現在編集している箇所へスクロールして追随する、「スクロール位置同期」 機能を実装しようとすると、日本語ではほとんど情報が見つかりません。一応こーいう記事はありますが。

これをどうにかする方法です。

まずはコード(CodeMirrorの例)

今回は、markedCodeMirror を用いた例です。基本的な使い方は別途調べてね。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/codemirror.min.css">
<style>
    #editorWrapper {
        float:left;
        width:50%;
    }
    #editorWrapper .CodeMirror {
        height: 500px;
    }
    #previewWrapper {
        float:right;
        width:50%;
        height: 500px;
        overflow: auto;
        background-color: #ccc;
    }
</style>
<div id="editorWrapper" style="">
    <textarea id="editor">
    </textarea>
</div>
<div id="previewWrapper">
    <div id="preview"></div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/addon/mode/overlay.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/mode/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/mode/gfm/gfm.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.18.2/addon/edit/continuelist.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
<script>
    var preview = document.getElementById("preview");
    var previewWrapper = document.getElementById("previewWrapper");
    var editor = CodeMirror.fromTextArea(document.getElementById('editor'), {
        mode:  "gfm",
        lineNumbers: true,
        matchBrackets: true,
        lineWrapping: true,
        extraKeys: {
            "Enter": "newlineAndIndentContinueMarkdownList"
        }
    });

    editor.on("change" , function(e){
        preview.innerHTML = marked(e.getValue());
    });

    editor.on("scroll" , function(e){
        // http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
        var scrollInfo = e.getScrollInfo();

        // get line number of the top line in the page
        var lineNumber = e.lineAtHeight(scrollInfo.top, 'local');
        // get the text content from the start to the target line
        var range = e.getRange({line: 0, ch: null}, {line: lineNumber, ch: null});
        var parser = new DOMParser();
        var doc = parser.parseFromString(marked(range), 'text/html');
        var totalLines = doc.body.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, pre, blockquote, hr, table');

        // shouldPreviewScroll(length)
        var body = document.getElementById("preview");
        var elems = body.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, pre, blockquote, hr, table');
        if (elems.length > 0) {
            previewWrapper.scrollTop = elems[totalLines.length].offsetTop;
        }
    });
</script>

これを適当なHTMLファイルとして保存して、適当にマークダウンで文章を書いてみてください。画像や連続改行が混ざっていたとしても、プレビューとエディタ部分のスクロールがいい感じに同期されるかと思います。ソースコードにもURL書いてますが、元ネタは「The Sync Scroll of Markdown Editor in Javascript」です。個人的は良い感じ。

他に参考になりそうな記事

不具合があるとか、CodeMirrorを使っていないとか色々あると思いますので参考程度に。

あと、できれば CommonMark に準拠したライブラリの方が色々良いですよ!