Compare commits
6
Commits
46c412f8f3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74bfe34d3f | ||
|
|
6000de166f | ||
|
|
f524157326 | ||
|
|
ac98cf4f99 | ||
|
|
291813820b | ||
|
|
779bbb58aa |
@@ -979,7 +979,10 @@ label {
|
|||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* EasyMDE(CodeMirror) 컨테이너를 이 앱의 입력 필드 톤에 맞춘다 */
|
/* EasyMDE(CodeMirror) 컨테이너를 이 앱의 입력 필드 톤에 맞춘다. iOS에서는
|
||||||
|
app/static/js/journal-editor.js가 EasyMDE 대신 순수 <textarea>로 폴백하므로(한글 IME
|
||||||
|
자소분리 회피) 이 규칙은 그 경우 그냥 매칭되지 않는다 — .markdown-editor 자체는 위 공통
|
||||||
|
textarea 규칙을 그대로 물려받아 별도 스타일 없이도 정상적으로 보인다. */
|
||||||
.markdown-editor + .EasyMDEContainer .CodeMirror {
|
.markdown-editor + .EasyMDEContainer .CodeMirror {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-control);
|
border-radius: var(--radius-control);
|
||||||
|
|||||||
+252
-63
@@ -1,15 +1,243 @@
|
|||||||
(function () {
|
(function () {
|
||||||
// 내장 툴바(toolbar: false)는 안 쓴다 — EasyMDE 기본 툴바 아이콘은 Font Awesome CDN을 전제로
|
// 저널 본문 입력창(.markdown-editor)의 편집 엔진을 기기별로 나눈다.
|
||||||
// 하는데, 이 앱은 CDN을 안 쓰는 게 원칙이라 대신 journal.html/journal_day_detail.html에서
|
//
|
||||||
// 직접 만든 텍스트 버튼이 아래 EDITOR_ACTIONS로 EasyMDE 인스턴스 메서드를 호출한다.
|
// EasyMDE(CodeMirror 5 기반)는 타이핑 중 문법에 색을 입혀 보여주는 진짜 마크다운 에디터
|
||||||
function initEditors(root) {
|
// 경험을 주지만, iOS Safari에서 한글처럼 여러 keystroke를 조합해 한 글자를 완성하는 IME
|
||||||
if (typeof EasyMDE === "undefined") return;
|
// 입력 중에 CodeMirror가 화면을 다시 그리면서 조합 버퍼를 끊어버려 자소가 분리된 채로
|
||||||
var scope = root instanceof Element ? root : document;
|
// 남는 문제(자소분리)가 있다. inputStyle을 contenteditable로 강제해도 아이폰에서까지
|
||||||
var textareas = scope.querySelectorAll("textarea.markdown-editor:not([data-easymde-initialized])");
|
// 재현되는 걸 확인했다 — CodeMirror5 자체의 CJK IME 한계로 보고, iOS에서만 순수
|
||||||
|
// <textarea>로 폴백한다(브라우저 네이티브 입력 처리를 그대로 쓰면 IME가 깨질 이유가
|
||||||
|
// 없다). PC/안드로이드 등 iOS가 아닌 환경은 지금까지처럼 EasyMDE를 그대로 쓴다.
|
||||||
|
//
|
||||||
|
// 굵게/기울임 같은 툴바 버튼과 카테고리 템플릿 자동 채우기는 window.JournalEditor를
|
||||||
|
// 통해 호출되는데, 이 객체가 각 textarea에 EasyMDE가 붙어있는지(t._easymde) 보고
|
||||||
|
// EasyMDE 명령 또는 아래의 직접 선택 영역 조작 중 알맞은 쪽으로 위임한다 — 호출하는
|
||||||
|
// 템플릿 쪽(journal_editor_toolbar.html, journal.html)은 어느 엔진이 쓰이는지 몰라도 된다.
|
||||||
|
|
||||||
textareas.forEach(function (textarea) {
|
function isIOS() {
|
||||||
textarea.setAttribute("data-easymde-initialized", "true");
|
var ua = navigator.userAgent || "";
|
||||||
|
if (/iPad|iPhone|iPod/.test(ua)) return true;
|
||||||
|
// iPadOS 13+는 기본 설정에서 데스크톱 Safari인 척하는 UA를 보낸다("Macintosh"로 위장,
|
||||||
|
// "Request Mobile Website"를 켜지 않는 한). 이런 위장 아이패드를 잡아내는 표준적인
|
||||||
|
// 방법은 "Mac인데 멀티터치가 된다"는 조합을 보는 것이다(실제 맥은 터치스크린이 없음).
|
||||||
|
return navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireInput(textarea) {
|
||||||
|
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 순수 textarea 모드에서 툴바가 쓰는 선택 영역 조작 ----
|
||||||
|
|
||||||
|
function insertAtCursor(textarea, text) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var value = textarea.value;
|
||||||
|
textarea.value = value.slice(0, start) + text + value.slice(end);
|
||||||
|
var pos = start + text.length;
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = pos;
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceMarker(textarea, marker, replacement) {
|
||||||
|
var idx = textarea.value.indexOf(marker);
|
||||||
|
if (idx === -1) return;
|
||||||
|
var before = textarea.value.slice(0, idx);
|
||||||
|
var after = textarea.value.slice(idx + marker.length);
|
||||||
|
textarea.value = before + replacement + after;
|
||||||
|
var pos = before.length + replacement.length;
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = pos;
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapSelection(textarea, prefix, suffix) {
|
||||||
|
if (suffix === undefined) suffix = prefix;
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var value = textarea.value;
|
||||||
|
var selected = value.slice(start, end);
|
||||||
|
textarea.value = value.slice(0, start) + prefix + selected + suffix + value.slice(end);
|
||||||
|
textarea.selectionStart = start + prefix.length;
|
||||||
|
textarea.selectionEnd = start + prefix.length + selected.length;
|
||||||
|
textarea.focus();
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentLineRange(textarea) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var value = textarea.value;
|
||||||
|
var lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
||||||
|
var lineEnd = value.indexOf("\n", end);
|
||||||
|
if (lineEnd === -1) lineEnd = value.length;
|
||||||
|
return { lineStart: lineStart, lineEnd: lineEnd, block: value.slice(lineStart, lineEnd) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceBlock(textarea, range, newBlock) {
|
||||||
|
var value = textarea.value;
|
||||||
|
textarea.value = value.slice(0, range.lineStart) + newBlock + value.slice(range.lineEnd);
|
||||||
|
textarea.selectionStart = range.lineStart;
|
||||||
|
textarea.selectionEnd = range.lineStart + newBlock.length;
|
||||||
|
textarea.focus();
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLinePrefix(textarea, prefix) {
|
||||||
|
var range = currentLineRange(textarea);
|
||||||
|
var lines = range.block.split("\n");
|
||||||
|
var allPrefixed = lines.every(function (line) { return line.indexOf(prefix) === 0; });
|
||||||
|
var newLines = lines.map(function (line) {
|
||||||
|
if (allPrefixed) return line.slice(prefix.length);
|
||||||
|
return line.indexOf(prefix) === 0 ? line : prefix + line;
|
||||||
|
});
|
||||||
|
replaceBlock(textarea, range, newLines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOrderedListPlain(textarea) {
|
||||||
|
var range = currentLineRange(textarea);
|
||||||
|
var lines = range.block.split("\n");
|
||||||
|
var re = /^\d+\.\s/;
|
||||||
|
var allNumbered = lines.every(function (line) { return re.test(line); });
|
||||||
|
var newLines = lines.map(function (line, i) {
|
||||||
|
return allNumbered ? line.replace(re, "") : (i + 1) + ". " + line.replace(re, "");
|
||||||
|
});
|
||||||
|
replaceBlock(textarea, range, newLines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleHeadingPlain(textarea) {
|
||||||
|
var range = currentLineRange(textarea);
|
||||||
|
var re = /^#{1,6}\s/;
|
||||||
|
var newBlock = re.test(range.block) ? range.block.replace(re, "") : "### " + range.block;
|
||||||
|
replaceBlock(textarea, range, newBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCodePlain(textarea) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var selected = textarea.value.slice(start, end);
|
||||||
|
if (selected.indexOf("\n") === -1) {
|
||||||
|
wrapSelection(textarea, "`");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var value = textarea.value;
|
||||||
|
textarea.value = value.slice(0, start) + "```\n" + selected + "\n```" + value.slice(end);
|
||||||
|
textarea.selectionStart = start + 4;
|
||||||
|
textarea.selectionEnd = start + 4 + selected.length;
|
||||||
|
textarea.focus();
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertLinkPlain(textarea) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var selected = textarea.value.slice(start, end) || "링크 텍스트";
|
||||||
|
var url = window.prompt("링크 주소를 입력하세요", "https://");
|
||||||
|
if (!url) return;
|
||||||
|
var value = textarea.value;
|
||||||
|
var markdown = "[" + selected + "](" + url + ")";
|
||||||
|
textarea.value = value.slice(0, start) + markdown + value.slice(end);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + markdown.length;
|
||||||
|
textarea.focus();
|
||||||
|
fireInput(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
// journal_editor_toolbar.html의 버튼들과 journal.html의 카테고리 템플릿 자동 채우기가
|
||||||
|
// 호출하는 공개 API — 어느 엔진(EasyMDE/순수 textarea)이 붙어있는지는 t._easymde 유무로
|
||||||
|
// 판단해서 알맞은 쪽으로 위임한다.
|
||||||
|
window.JournalEditor = {
|
||||||
|
bold: function (t) { if (t) (t._easymde ? t._easymde.toggleBold() : wrapSelection(t, "**")); },
|
||||||
|
italic: function (t) { if (t) (t._easymde ? t._easymde.toggleItalic() : wrapSelection(t, "*")); },
|
||||||
|
strike: function (t) { if (t) (t._easymde ? t._easymde.toggleStrikethrough() : wrapSelection(t, "~~")); },
|
||||||
|
heading: function (t) { if (t) (t._easymde ? t._easymde.toggleHeadingSmaller() : toggleHeadingPlain(t)); },
|
||||||
|
quote: function (t) { if (t) (t._easymde ? t._easymde.toggleBlockquote() : toggleLinePrefix(t, "> ")); },
|
||||||
|
ul: function (t) { if (t) (t._easymde ? t._easymde.toggleUnorderedList() : toggleLinePrefix(t, "- ")); },
|
||||||
|
ol: function (t) { if (t) (t._easymde ? t._easymde.toggleOrderedList() : toggleOrderedListPlain(t)); },
|
||||||
|
code: function (t) { if (t) (t._easymde ? t._easymde.toggleCodeBlock() : toggleCodePlain(t)); },
|
||||||
|
link: function (t) { if (t) (t._easymde ? t._easymde.drawLink() : insertLinkPlain(t)); },
|
||||||
|
setValue: function (t, value) {
|
||||||
|
if (!t) return;
|
||||||
|
t.value = value;
|
||||||
|
if (t._easymde) t._easymde.value(value);
|
||||||
|
fireInput(t);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 클립보드 이미지 붙여넣기 ----
|
||||||
|
// 스크린샷/사진을 그대로 붙여넣기 대신 서버에 업로드하고 그 자리에 마크다운 이미지
|
||||||
|
// 문법()을 끼워넣는다. 두 엔진 다 업로드 로직은 같고, "지금 커서 위치를 어떻게
|
||||||
|
// 표시해뒀다가 나중에 찾아서 바꿔치기하는지"만 다르다.
|
||||||
|
|
||||||
|
function uploadPastedImage(file) {
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append("file", file, file.name || "pasted-image.png");
|
||||||
|
return fetch("/api/journal/paste-image", { method: "POST", body: formData }).then(function (res) {
|
||||||
|
if (!res.ok) return res.json().then(function (body) { throw new Error(body.detail || "업로드 실패"); });
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractImageFile(event) {
|
||||||
|
var items = event.clipboardData && event.clipboardData.items;
|
||||||
|
if (!items) return null;
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf("image/") === 0) return items[i].getAsFile();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 순수 textarea 모드: 업로드 중 표식을 값에서 찾아 최종 링크로 바꾼다. 고정된 커서
|
||||||
|
// 좌표 대신 텍스트 표식으로 위치를 추적하므로, 업로드가 끝나기 전에 사용자가 다른 곳을
|
||||||
|
// 계속 타이핑해도 자리를 잃지 않는다.
|
||||||
|
function handlePlainImagePaste(textarea, event) {
|
||||||
|
var file = extractImageFile(event);
|
||||||
|
if (!file) return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var marker = "![업로드 중… #" + Math.random().toString(36).slice(2, 8) + "]()";
|
||||||
|
insertAtCursor(textarea, marker);
|
||||||
|
|
||||||
|
uploadPastedImage(file)
|
||||||
|
.then(function (data) { replaceMarker(textarea, marker, ""); })
|
||||||
|
.catch(function (err) { replaceMarker(textarea, marker, "(이미지 붙여넣기 실패: " + err.message + ")"); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// EasyMDE 모드: CodeMirror 북마크로 위치를 추적한다(원본 로직 그대로).
|
||||||
|
function handleCodeMirrorImagePaste(cm, event) {
|
||||||
|
var file = extractImageFile(event);
|
||||||
|
if (!file) return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var doc = cm.getDoc();
|
||||||
|
var from = doc.getCursor();
|
||||||
|
var placeholder = "![업로드 중...]()";
|
||||||
|
doc.replaceRange(placeholder, from);
|
||||||
|
var to = { line: from.line, ch: from.ch + placeholder.length };
|
||||||
|
var startMark = doc.setBookmark(from);
|
||||||
|
var endMark = doc.setBookmark(to);
|
||||||
|
|
||||||
|
uploadPastedImage(file)
|
||||||
|
.then(function (data) {
|
||||||
|
var start = startMark.find();
|
||||||
|
var end = endMark.find();
|
||||||
|
if (start && end) doc.replaceRange("", start, end);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
var start = startMark.find();
|
||||||
|
var end = endMark.find();
|
||||||
|
if (start && end) doc.replaceRange("(이미지 붙여넣기 실패: " + err.message + ")", start, end);
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
startMark.clear();
|
||||||
|
endMark.clear();
|
||||||
|
cm.save();
|
||||||
|
cm.getTextArea().dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEasyMDE(textarea) {
|
||||||
|
// 내장 툴바(toolbar: false)는 안 쓴다 — EasyMDE 기본 툴바 아이콘은 Font Awesome CDN을
|
||||||
|
// 전제로 하는데, 이 앱은 CDN을 안 쓰는 게 원칙이라 대신 journal_editor_toolbar.html의
|
||||||
|
// 자체 버튼이 위 JournalEditor를 통해 EasyMDE 인스턴스 메서드를 호출한다.
|
||||||
var easymde = new EasyMDE({
|
var easymde = new EasyMDE({
|
||||||
element: textarea,
|
element: textarea,
|
||||||
toolbar: false,
|
toolbar: false,
|
||||||
@@ -29,70 +257,31 @@
|
|||||||
// Alpine x-model이 둘 다 반응하도록 input 이벤트를 합성해서 던진다.
|
// Alpine x-model이 둘 다 반응하도록 input 이벤트를 합성해서 던진다.
|
||||||
easymde.codemirror.on("change", function () {
|
easymde.codemirror.on("change", function () {
|
||||||
easymde.codemirror.save();
|
easymde.codemirror.save();
|
||||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
fireInput(textarea);
|
||||||
});
|
});
|
||||||
|
|
||||||
easymde.codemirror.on("paste", function (cm, event) {
|
easymde.codemirror.on("paste", function (cm, event) {
|
||||||
handleImagePaste(cm, event);
|
handleCodeMirrorImagePaste(cm, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
textarea._easymde = easymde;
|
textarea._easymde = easymde;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 클립보드에 이미지가 있으면(스크린샷/사진 복사 등) 그대로 붙여넣기 대신 서버에 업로드하고
|
function initPlainTextarea(textarea) {
|
||||||
// 그 자리에 마크다운 이미지 문법()을 끼워넣는다. 이미지가 아니면 그냥 통과시켜서
|
textarea.addEventListener("paste", function (event) { handlePlainImagePaste(textarea, event); });
|
||||||
// CodeMirror 기본 텍스트 붙여넣기가 그대로 동작하게 둔다.
|
|
||||||
function handleImagePaste(cm, event) {
|
|
||||||
var items = event.clipboardData && event.clipboardData.items;
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
var imageItem = null;
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].type.indexOf("image/") === 0) {
|
|
||||||
imageItem = items[i];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initEditors(root) {
|
||||||
|
var scope = root instanceof Element ? root : document;
|
||||||
|
var textareas = scope.querySelectorAll("textarea.markdown-editor:not([data-journal-editor-initialized])");
|
||||||
|
|
||||||
|
textareas.forEach(function (textarea) {
|
||||||
|
textarea.setAttribute("data-journal-editor-initialized", "true");
|
||||||
|
if (typeof EasyMDE !== "undefined" && !isIOS()) {
|
||||||
|
initEasyMDE(textarea);
|
||||||
|
} else {
|
||||||
|
initPlainTextarea(textarea);
|
||||||
}
|
}
|
||||||
if (!imageItem) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
var file = imageItem.getAsFile();
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
var doc = cm.getDoc();
|
|
||||||
var from = doc.getCursor();
|
|
||||||
var placeholder = "![업로드 중...]()";
|
|
||||||
doc.replaceRange(placeholder, from);
|
|
||||||
var to = { line: from.line, ch: from.ch + placeholder.length };
|
|
||||||
// 업로드가 끝나기 전에 사용자가 다른 곳에서 계속 타이핑해도(줄 추가 등) 자리를 잃지
|
|
||||||
// 않도록 고정 좌표 대신 CodeMirror 북마크로 추적한다.
|
|
||||||
var startMark = doc.setBookmark(from);
|
|
||||||
var endMark = doc.setBookmark(to);
|
|
||||||
|
|
||||||
var formData = new FormData();
|
|
||||||
formData.append("file", file, file.name || "pasted-image.png");
|
|
||||||
|
|
||||||
fetch("/api/journal/paste-image", { method: "POST", body: formData })
|
|
||||||
.then(function (res) {
|
|
||||||
if (!res.ok) return res.json().then(function (body) { throw new Error(body.detail || "업로드 실패"); });
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then(function (data) {
|
|
||||||
var start = startMark.find();
|
|
||||||
var end = endMark.find();
|
|
||||||
if (start && end) { doc.replaceRange("", start, end); }
|
|
||||||
})
|
|
||||||
.catch(function (err) {
|
|
||||||
var start = startMark.find();
|
|
||||||
var end = endMark.find();
|
|
||||||
if (start && end) { doc.replaceRange("(이미지 붙여넣기 실패: " + err.message + ")", start, end); }
|
|
||||||
})
|
|
||||||
.finally(function () {
|
|
||||||
startMark.clear();
|
|
||||||
endMark.clear();
|
|
||||||
cm.save();
|
|
||||||
cm.getTextArea().dispatchEvent(new Event("input", { bubbles: true }));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = "habit-tracker-v6";
|
const CACHE_NAME = "habit-tracker-v8";
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
"/static/css/style.css",
|
"/static/css/style.css",
|
||||||
"/static/css/vendor/easymde.min.css",
|
"/static/css/vendor/easymde.min.css",
|
||||||
|
|||||||
@@ -134,10 +134,7 @@
|
|||||||
if (this.content === this.lastAutoFilled) {
|
if (this.content === this.lastAutoFilled) {
|
||||||
this.content = tmpl;
|
this.content = tmpl;
|
||||||
this.lastAutoFilled = tmpl;
|
this.lastAutoFilled = tmpl;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => { JournalEditor.setValue(this.$refs.contentField, tmpl); });
|
||||||
if (this.$refs.contentField._easymde) { this.$refs.contentField._easymde.value(tmpl); }
|
|
||||||
this.$refs.contentField.dispatchEvent(new Event('input'));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() { this.onCategoryChange(this.$refs.categorySelect.value); },
|
init() { this.onCategoryChange(this.$refs.categorySelect.value); },
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<div class="markdown-toolbar">
|
<div class="markdown-toolbar">
|
||||||
<span x-show="!previewMode" style="display:flex; gap:4px; flex-wrap:wrap;">
|
<span x-show="!previewMode" style="display:flex; gap:4px; flex-wrap:wrap;">
|
||||||
<button type="button" class="md-tool-btn" title="굵게" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleBold()"><strong>B</strong></button>
|
<button type="button" class="md-tool-btn" title="굵게" @click="JournalEditor.bold($el.closest('.field').querySelector('textarea'))"><strong>B</strong></button>
|
||||||
<button type="button" class="md-tool-btn" title="기울임" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleItalic()"><em>I</em></button>
|
<button type="button" class="md-tool-btn" title="기울임" @click="JournalEditor.italic($el.closest('.field').querySelector('textarea'))"><em>I</em></button>
|
||||||
<button type="button" class="md-tool-btn" title="취소선" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleStrikethrough()"><s>S</s></button>
|
<button type="button" class="md-tool-btn" title="취소선" @click="JournalEditor.strike($el.closest('.field').querySelector('textarea'))"><s>S</s></button>
|
||||||
<button type="button" class="md-tool-btn" title="제목" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleHeadingSmaller()">H</button>
|
<button type="button" class="md-tool-btn" title="제목" @click="JournalEditor.heading($el.closest('.field').querySelector('textarea'))">H</button>
|
||||||
<button type="button" class="md-tool-btn" title="인용" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleBlockquote()">❝</button>
|
<button type="button" class="md-tool-btn" title="인용" @click="JournalEditor.quote($el.closest('.field').querySelector('textarea'))">❝</button>
|
||||||
<button type="button" class="md-tool-btn" title="글머리 목록" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleUnorderedList()">•</button>
|
<button type="button" class="md-tool-btn" title="글머리 목록" @click="JournalEditor.ul($el.closest('.field').querySelector('textarea'))">•</button>
|
||||||
<button type="button" class="md-tool-btn" title="번호 목록" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleOrderedList()">1.</button>
|
<button type="button" class="md-tool-btn" title="번호 목록" @click="JournalEditor.ol($el.closest('.field').querySelector('textarea'))">1.</button>
|
||||||
<button type="button" class="md-tool-btn" title="코드" @click="$el.closest('.field').querySelector('textarea')._easymde?.toggleCodeBlock()"></></button>
|
<button type="button" class="md-tool-btn" title="코드" @click="JournalEditor.code($el.closest('.field').querySelector('textarea'))"></></button>
|
||||||
<button type="button" class="md-tool-btn" title="링크" @click="$el.closest('.field').querySelector('textarea')._easymde?.drawLink()">🔗</button>
|
<button type="button" class="md-tool-btn" title="링크" @click="JournalEditor.link($el.closest('.field').querySelector('textarea'))">🔗</button>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user