[Javascript] Summernote Codeview 整合 Monaco Editor,每次開啟自動 Format HTML

  • 20
  • 0

最近常常要幫客戶改後台裡面的 HTML,不過實際上大部分客戶根本不會自己去碰 Code

但如果直接把後台換成純 HTML Editor 也不太實際,客戶看到一堆 HTML 應該只會更不想改,而且像貼文字、改段落、上傳圖片這些操作,Summernote 這種 WYSIWYG 還是比較符合直覺 所以 Summernote 我還是想留著

 

只是換成我自己要進去改 HTML 的時候,Summernote 原本那個 Codeview 就真的有點難用,沒有 Syntax Highlight,有時候 HTML 又全部擠在一起,看久了很痛苦..

這次就來測看看,平常還是讓客戶繼續用 Summernote,只有切到 Codeview 的時候改成 Monaco Editor,這應該是我目前想到最好的解決方案

Summernote 本身切換 Codeview 時就會觸發

onCodeviewToggled

所以不用自己去綁那顆按鈕

onCodeviewToggled: function () {
    var $noteEditor =
        $('#summernote').next('.note-editor');

    var isCodeView =
        $noteEditor.hasClass('codeview');

    if (isCodeView) {
        showMonaco();
    } else {
        hideMonaco();
    }
}

進 Codeview 就建立 Monaco,切回去就 Dispose

Monaco 裡面的 HTML 直接拿 Summernote 原本的

.note-codable
var $codeview =
    $noteEditor.find('.note-codable');

monacoEditor = monaco.editor.create(host, {
    value: $codeview.val(),
    language: 'html',
    theme: 'vs-dark',
    automaticLayout: true,
    minimap: {
        enabled: false
    },
    wordWrap: 'on',
    tabSize: 2
});

樣 Summernote 原本的 Codeview 還是在,只是 Monaco 蓋在上面負責編輯

有改內容的話再寫回 .note-codable

monacoEditor.onDidChangeModelContent(function () {
    $codeview.val(
        monacoEditor.getValue()
    );
});

不然 Monaco 裡面改得很開心,切回 Summernote 內容一定要同步這點有點被卡到..

另外一個我自己比較想要的是,每次按 Codeview 都直接幫我重新排版 HTML

所以 Monaco 建立完之後直接跑:

setTimeout(function () {
    var action =
        monacoEditor.getAction(
            'editor.action.formatDocument'
        );

    if (action) {
        action.run();
    }
}, 100);

像這種:

<h3>Hello</h3><p>Test</p><ul><li>A</li><li>B</li></ul>

切進去之後就會直接整理,不用每次看到一坨 Code 再自己慢慢排

目前完整測試版本放下面,之後要搬到專案再直接從這份改

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Summernote + Monaco Editor Test</title>

    <link
        rel="stylesheet"
        href="https://cdn.jsdelivr.net/npm/summernote@0.8.20/dist/summernote-lite.min.css">

    <style>
        body {
            max-width: 1000px;
            margin: 30px auto;
            padding: 0 20px;
            font-family: Arial, sans-serif;
        }

        .note-editing-area {
            position: relative;
        }

        #monaco-host {
            position: absolute;
            inset: 0;
            z-index: 20;
            display: none;
        }
    </style>
</head>
<body>

    <h2>Summernote + Monaco Editor</h2>

    <div id="summernote">
        <h3>Hello Summernote</h3>
        <p>這是一段故意沒有排版好的 HTML。</p>
        <ul>
            <li>Item 1</li>
            <li>Item 2</li>
        </ul>
    </div>

    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/summernote@0.8.20/dist/summernote-lite.min.js"></script>

    <script>
        var monacoEditor = null;
        var monacoReady = false;

        $(function () {
            $('#summernote').summernote({
                height: 400,
                toolbar: [
                    ['style', ['bold', 'italic', 'underline']],
                    ['para', ['ul', 'ol', 'paragraph']],
                    ['insert', ['link']],
                    ['view', ['fullscreen', 'codeview']]
                ],
                callbacks: {
                    onInit: function () {
                        loadMonaco();
                    },

                    onCodeviewToggled: function () {
                        var $noteEditor =
                            $('#summernote').next('.note-editor');

                        var isCodeView =
                            $noteEditor.hasClass('codeview');

                        if (isCodeView) {
                            showMonaco();
                        } else {
                            hideMonaco();
                        }
                    }
                }
            });
        });

        function loadMonaco() {
            var script = document.createElement('script');

            script.src =
                'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js';

            script.onload = function () {
                require.config({
                    paths: {
                        vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs'
                    }
                });

                require(['vs/editor/editor.main'], function () {
                    monacoReady = true;
                });
            };

            document.body.appendChild(script);
        }

        function showMonaco() {
            if (!monacoReady) {
                return;
            }

            var $noteEditor =
                $('#summernote').next('.note-editor');

            var $codeview =
                $noteEditor.find('.note-codable');

            var host =
                document.getElementById('monaco-host');

            if (!host) {
                host = document.createElement('div');
                host.id = 'monaco-host';

                $noteEditor
                    .find('.note-editing-area')
                    .append(host);
            }

            host.style.display = 'block';

            if (monacoEditor) {
                monacoEditor.dispose();
            }

            monacoEditor = monaco.editor.create(host, {
                value: $codeview.val(),
                language: 'html',
                theme: 'vs-dark',
                automaticLayout: true,
                minimap: {
                    enabled: false
                },
                wordWrap: 'on',
                tabSize: 2
            });

            monacoEditor.onDidChangeModelContent(function () {
                $codeview.val(
                    monacoEditor.getValue()
                );
            });

            setTimeout(function () {
                var action =
                    monacoEditor.getAction(
                        'editor.action.formatDocument'
                    );

                if (action) {
                    action.run();
                }
            }, 100);
        }

        function hideMonaco() {
            var host =
                document.getElementById('monaco-host');

            if (host) {
                host.style.display = 'none';
            }

            if (monacoEditor) {
                monacoEditor.dispose();
                monacoEditor = null;
            }
        }
    </script>

</body>
</html>

這樣改完基本上既有後台不用大動,客戶繼續用他的 Summernote,我要進去修 HTML 也不用再看那坨擠成一團的 Code

目前先測了 Summernote + Monaco Editor 單一編輯器跟 Codeview 自動格式化,之後如果要搬進正式專案,可能還得補測 Fullscreen 切換跟同頁面多個 Editor Instance 的狀況,不過目前版本已經很夠用了,如果懶得自己寫,你就把這網址貼給 AI 他會自動幫你施工的..

 

---

The bug existed in all possible states.
Until I ran the code.