journal: real markdown editor (EasyMDE) with live preview toggle, fix default template
- Replace the plain textarea with EasyMDE (vendored locally, no CDN) for markdown authoring: syntax highlighting, smart list continuation, and a custom text-based toolbar (built-in EasyMDE toolbar icons require Font Awesome from a CDN, which this app doesn't use). unorderedListStyle is set to "-" to match the app's own template convention. - Add a preview/edit toggle button that swaps the editor for the exact same server-rendered markdown (via /journal/preview) shown after saving, instead of always showing both. - Fix create/edit entry routes to verify the submitted category_id actually belongs to the current user before inserting -- every other write path in this app already checked ownership; this one didn't (found while manually testing the new editor with a typo'd category id that happened to belong to someone else's category, which surfaced as an IntegrityError 500 instead of a clean 404-equivalent). - Fix the default "일상" category template: bare "-" bullet lines don't parse as list items in the markdown renderer (they need a trailing space), and the content_template validator was silently stripping that trailing space off on every save. Backfill migration updates any category still holding the old, broken template text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+122
-1
@@ -902,8 +902,129 @@ label {
|
||||
}
|
||||
|
||||
.journal-entry-content {
|
||||
white-space: pre-wrap;
|
||||
margin-top: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.journal-preview {
|
||||
margin-top: 6px;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-bg);
|
||||
min-height: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 마크다운 에디터 툴바 (EasyMDE는 toolbar:false로 끄고 여기서 자체 버튼으로 대체 —
|
||||
EasyMDE 기본 툴바는 Font Awesome CDN을 전제로 해서 이 앱의 "CDN 금지" 원칙과 안 맞는다) */
|
||||
.markdown-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.md-tool-btn {
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-tool-btn:active {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* EasyMDE(CodeMirror) 컨테이너를 이 앱의 입력 필드 톤에 맞춘다 */
|
||||
.markdown-editor + .EasyMDEContainer .CodeMirror {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.markdown-editor + .EasyMDEContainer .CodeMirror-cursor {
|
||||
border-left-color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown-editor + .EasyMDEContainer .editor-statusbar {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.journal-entry-content > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content p,
|
||||
.journal-entry-content ul,
|
||||
.journal-entry-content ol,
|
||||
.journal-entry-content blockquote,
|
||||
.journal-entry-content pre {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.journal-entry-content h1,
|
||||
.journal-entry-content h2,
|
||||
.journal-entry-content h3,
|
||||
.journal-entry-content h4 {
|
||||
margin: 12px 0 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.journal-entry-content h1 { font-size: 19px; }
|
||||
.journal-entry-content h2 { font-size: 17px; }
|
||||
.journal-entry-content h3,
|
||||
.journal-entry-content h4 { font-size: 15px; }
|
||||
|
||||
.journal-entry-content ul,
|
||||
.journal-entry-content ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.journal-entry-content blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--color-accent);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.journal-entry-content code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
.journal-entry-content pre {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.journal-entry-content pre code {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.journal-entry-tags {
|
||||
|
||||
+7
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
|
||||
(function () {
|
||||
// 내장 툴바(toolbar: false)는 안 쓴다 — EasyMDE 기본 툴바 아이콘은 Font Awesome CDN을 전제로
|
||||
// 하는데, 이 앱은 CDN을 안 쓰는 게 원칙이라 대신 journal.html/journal_day_detail.html에서
|
||||
// 직접 만든 텍스트 버튼이 아래 EDITOR_ACTIONS로 EasyMDE 인스턴스 메서드를 호출한다.
|
||||
function initEditors(root) {
|
||||
if (typeof EasyMDE === "undefined") return;
|
||||
var scope = root instanceof Element ? root : document;
|
||||
var textareas = scope.querySelectorAll("textarea.markdown-editor:not([data-easymde-initialized])");
|
||||
|
||||
textareas.forEach(function (textarea) {
|
||||
textarea.setAttribute("data-easymde-initialized", "true");
|
||||
|
||||
var easymde = new EasyMDE({
|
||||
element: textarea,
|
||||
toolbar: false,
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
status: false,
|
||||
placeholder: textarea.getAttribute("placeholder") || "",
|
||||
minHeight: (textarea.getAttribute("rows") || 6) * 24 + "px",
|
||||
// 글머리 목록 버튼/Enter 자동 이어쓰기가 기본 "*" 대신 "-"를 쓰게 한다.
|
||||
// 서버 렌더링(app/markdown_utils.py)은 -/*/+ 전부 동일하게 처리하니 렌더링과는 무관하고
|
||||
// 순수하게 에디터가 새로 만들어주는 글머리 기호에 대한 취향 설정이다.
|
||||
unorderedListStyle: "-",
|
||||
});
|
||||
|
||||
// CodeMirror는 원본 textarea와 실시간으로 값이 동기화되지 않는다(.save()를 명시적으로
|
||||
// 불러야 함) — 매 변경마다 저장하고, htmx 미리보기(hx-trigger="input ...")와
|
||||
// Alpine x-model이 둘 다 반응하도록 input 이벤트를 합성해서 던진다.
|
||||
easymde.codemirror.on("change", function () {
|
||||
easymde.codemirror.save();
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
textarea._easymde = easymde;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initEditors(document);
|
||||
});
|
||||
document.body.addEventListener("htmx:afterSwap", function (evt) {
|
||||
initEditors(evt.target);
|
||||
});
|
||||
})();
|
||||
Vendored
+7
File diff suppressed because one or more lines are too long
@@ -1,13 +1,16 @@
|
||||
const CACHE_NAME = "habit-tracker-v5";
|
||||
const CACHE_NAME = "habit-tracker-v6";
|
||||
const APP_SHELL = [
|
||||
"/static/css/style.css",
|
||||
"/static/css/vendor/easymde.min.css",
|
||||
"/static/js/app.js",
|
||||
"/static/js/push-register.js",
|
||||
"/static/js/habit-reorder.js",
|
||||
"/static/js/journal-category-reorder.js",
|
||||
"/static/js/journal-editor.js",
|
||||
"/static/js/vendor/htmx.min.js",
|
||||
"/static/js/vendor/alpine.min.js",
|
||||
"/static/js/vendor/sortable.min.js",
|
||||
"/static/js/vendor/easymde.min.js",
|
||||
"/static/icons/icon-192.png",
|
||||
"/static/icons/icon-512.png",
|
||||
"/static/icons/icon-apple-180.png",
|
||||
|
||||
Reference in New Issue
Block a user