Django Admin 整合 EasyMDE(markdown 編輯器)

models.py
content_html 用來存放 EasyMDE 從 content 產生的 HTML,這樣做有兩個好處。可以快取產生的 HTML,另外是格式不會跑掉。用其他 markdown 轉 HTML 格式會稍微與 EasyMDE 產生的 HTML 不一樣,要精確排版就會很頭痛。

class Post(models.Model):
    content = models.TextField("內容")
    content_html = models.TextField(blank=True, null=True)  # 自動轉成 HTML 儲存

widgets.py

from django import forms
from django.utils.safestring import mark_safe

class EasyMDEWidget(forms.Textarea):
    class Media:
        css = {
            'all': ('https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css',
                    'post.css')
        }
        js = (
            'https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js',
        )

    def render(self, name, value, attrs=None, renderer=None):
        textarea = super().render(name, value, attrs, renderer)
        return mark_safe(f"""
            {textarea}
            <script>
                document.addEventListener("DOMContentLoaded", function() {{
                    const easyMDE = new EasyMDE({{ element: document.getElementById("id_{name}") }});                    
    
                    const textarea = document.getElementById('id_content');
                    const form = textarea.closest('form');
                    
                    form.addEventListener('submit', function () {{
                        const html = easyMDE.options.previewRender(easyMDE.value());
                        
                        const parser = new DOMParser();
                    const doc = parser.parseFromString(html, 'text/html');
                    const cleanHtml = doc.body.innerHTML;
                    
                        const htmlField = document.getElementById('id_content_html');
                        if (htmlField) {{            
                            htmlField.value = cleanHtml.trim();
                        }} else {{
                            alert('no id_content_html found')
                        }}
                    }});
                }});
            </script>
        """)

form

class PostAdminForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'content': EasyMDEWidget(),
            'content_html': forms.HiddenInput(),
        }

admin.py

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    form = PostAdminForm

post.css 有需要可以調整一下 EasyMDE 顏色

/* 編輯器本體 */
.EasyMDEContainer .CodeMirror {
    background-color: #1e1e1e;
    color: #e0e0e0;
}

/* 工具列背景與按鈕 */
.editor-toolbar {
    background-color: #2a2a2a !important;
    border: 1px solid #444;
}

/* 工具列 icon */
.editor-toolbar a {
    color: #ccc !important;
}

.editor-toolbar li, .editor-toolbar button {
    color: #ccc !important;
}

/* 工具列 icon hover 效果 */
.editor-toolbar a:hover {
    background-color: #3a3a3a !important;
    color: white !important;
}

/* Side-by-Side 預覽 */
.editor-preview-side {
    background-color: #1e1e1e;
    color: #e0e0e0;
    border-left: 1px solid #444;
}

/* 全螢幕預覽 */
.editor-preview {
    background-color: #1e1e1e;
    color: #e0e0e0;
}

/* 全螢幕模式強制覆蓋 */
.CodeMirror-fullscreen,
.editor-toolbar.fullscreen {
    background-color: #1e1e1e !important;
    color: #e0e0e0 !important;
}

/* 工具列在全螢幕時 */
.editor-toolbar.fullscreen {
    border-bottom: 1px solid #444;
}

/* CodeMirror 滾動條背景 */
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter {
    background-color: #1e1e1e;
}

/* 被啟用的按鈕(例如全螢幕、Side-by-Side) */
.editor-toolbar a.active,
.editor-toolbar a.active:hover {
    background-color: #444 !important;
    color: #fff !important;
}

/* 全螢幕狀態下的工具列也要套用暗色 */
.editor-toolbar.fullscreen a.active {
    background-color: #444 !important;
    color: #fff !important;
}


/* 在 Side by Side 或全螢幕模式下,稍微往右移 */
.CodeMirror.cm-s-easymde.CodeMirror-fullscreen {
    margin-left: 10px !important;
}

/* Side by Side 模式編輯區向右推一點 */
.editor-preview-side {
    margin-left: 10px !important;
}

/* 針對 EasyMDE / CodeMirror 編輯器游標改成白色 */
.CodeMirror-cursor {
  border-left: 2px solid white !important;
}