feat(cafe24): HTML 편집기 Ctrl+/ 주석 토글

- 선택이 있으면 선택이 걸친 줄 전체가 대상. 반쯤 걸친 줄이 잘려 태그가
  깨지지 않게 하기 위함. 선택이 없으면 커서가 있는 줄 하나.
- 대상 안에 주석 기호가 하나라도 있으면 제거, 없으면 블록 전체를 <!-- -->
  로 감싼다. HTML 주석은 중첩이 안 되므로 이미 주석인 부분을 또 감싸지
  않는다.
- 선택이 개행에서 끝나면 다음 줄은 제외(줄 끝까지 드래그했을 때 아래 줄이
  딸려오지 않게).
- execCommand("insertText") 로 넣어 Ctrl+Z 이력에 남긴다. 미지원 브라우저는
  value 를 직접 바꾼다.
- 되돌린 뒤 커서/선택 위치를 복원한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 22:10:42 +09:00
parent 3537e058b0
commit 233c8db631
2 changed files with 69 additions and 3 deletions
@@ -78,7 +78,7 @@
<input type="hidden" name="list_query" value="{{ list_query }}" />
<div class="cf24-editor-bar">
<span class="cf24-muted">상세설명 HTML · {{ desc.description | length }}자 · PC/모바일 공통</span>
<span class="cf24-muted">상세설명 HTML · {{ desc.description | length }}자 · PC/모바일 공통 · Ctrl+/ 주석</span>
<span class="cf24-editor-bar-right">
<input class="cf24-memo" type="text" name="memo" maxlength="200"
placeholder="변경 메모 (버전 이력에 남습니다)" />
@@ -99,7 +99,8 @@
<div class="cf24-code-body">
<pre class="cf24-code-hl" id="cf24-hl-pc" aria-hidden="true"></pre>
<textarea id="cf24-html-pc" class="cf24-code-input" name="html" wrap="off"
spellcheck="false" autocapitalize="off" autocorrect="off">{{ html_pc }}</textarea>
spellcheck="false" autocapitalize="off" autocorrect="off"
title="Tab 들여쓰기 · Ctrl+/ 주석 토글(선택한 줄 전체, 선택이 없으면 커서 줄)">{{ html_pc }}</textarea>
</div>
</div>
</form>
@@ -186,10 +186,75 @@
timer = setTimeout(repaint, 80);
}
/* Ctrl+/ (Mac ⌘+/) 로 HTML 주석 토글.
- 선택이 있으면 그 선택이 걸친 **줄 전체**가 대상이다(반쯤 걸친 줄이
잘려 태그가 깨지지 않게).
- 선택이 없으면 커서가 있는 줄 하나.
- 대상 안에 주석 기호가 하나라도 있으면 **제거**, 없으면 블록 전체를
<!-- --> 로 감싼다. HTML 주석은 중첩이 안 되므로 "이미 주석인 부분을
또 감싸기"를 피하는 것이 이 규칙의 이유다. */
var COMMENT_MARK = /<!--[ \t]?|[ \t]?-->/g;
function toggleComment() {
var value = ta.value;
var start = ta.selectionStart, end = ta.selectionEnd;
// 선택이 개행에서 끝나면 그 다음 줄은 대상이 아니다(드래그로 줄 끝까지
// 끌었을 때 아래 줄까지 주석되는 것을 막는다).
if (end > start && value.charAt(end - 1) === "\n") end -= 1;
var lineStart = value.lastIndexOf("\n", start - 1) + 1;
var lineEnd = value.indexOf("\n", end);
if (lineEnd === -1) lineEnd = value.length;
var block = value.slice(lineStart, lineEnd);
if (!block.trim()) return; // 빈 줄에서는 아무 것도 하지 않는다
var caretIn = start - lineStart; // 블록 안에서의 커서 위치
var shift = 0, out;
COMMENT_MARK.lastIndex = 0;
if (COMMENT_MARK.test(block)) {
COMMENT_MARK.lastIndex = 0;
out = block.replace(COMMENT_MARK, function (mark, offset) {
if (offset < caretIn) shift -= mark.length; // 커서 앞이 줄어든 만큼
return "";
});
} else {
var indent = block.match(/^[ \t]*/)[0];
out = indent + "<!-- " + block.slice(indent.length) + " -->";
shift = caretIn >= indent.length ? 5 : 0; // "<!-- " 길이
}
// execCommand 로 넣어야 브라우저의 실행취소(Ctrl+Z) 이력에 남는다.
// 지원하지 않으면 value 를 직접 바꾼다(그때는 되돌리기가 안 될 뿐 동작은 같다).
ta.selectionStart = lineStart;
ta.selectionEnd = lineEnd;
var inserted = false;
try { inserted = document.execCommand("insertText", false, out); } catch (err) { inserted = false; }
if (!inserted) ta.value = value.slice(0, lineStart) + out + value.slice(lineEnd);
if (start === end) {
var pos = lineStart + Math.min(out.length, Math.max(0, caretIn + shift));
ta.selectionStart = ta.selectionEnd = pos;
} else {
ta.selectionStart = lineStart; // 바꾼 범위를 계속 선택해 둔다
ta.selectionEnd = lineStart + out.length;
}
dirty = true;
refresh();
}
ta.addEventListener("scroll", sync);
ta.addEventListener("input", function () { dirty = true; refresh(); });
// Tab 은 포커스 이동이 아니라 들여쓰기로 쓴다.
ta.addEventListener("keydown", function (e) {
// Ctrl+/ · ⌘+/ — 자판에 따라 key 가 "/" 가 아닐 수 있어 code 도 함께 본다.
if ((e.ctrlKey || e.metaKey) && !e.altKey &&
(e.key === "/" || e.code === "Slash" || e.code === "NumpadDivide")) {
e.preventDefault();
toggleComment();
return;
}
// Tab 은 포커스 이동이 아니라 들여쓰기로 쓴다.
if (e.key !== "Tab") return;
e.preventDefault();
var start = ta.selectionStart, end = ta.selectionEnd;