fix(cafe24): 편집기 커서 위치가 어긋나던 문제 — 스크롤 동기화로 전환
증상: 줄 끝에 커서를 두고 엔터를 치면 커서보다 앞쪽에서 줄이 나뉘었다. 원인은 두 층의 크기를 맞추는 방식이었다. 색칠된 <pre> 가 상자 크기를 정하고 textarea 가 그 위를 덮게 했는데, flex 자식에서 `width: max-content` 가 기대대로 적용되지 않아 화면보다 긴 줄이 있으면 textarea 만 내부 스크롤이 가능한 상태가 됐다 (실측 scrollWidth 706 vs clientWidth 686). 캐럿을 따라 textarea 가 내부적으로 스크롤되면 색칠 층은 제자리에 남아, 보이는 글자와 실제 문자 오프셋이 어긋난다. 그래서 커서를 둔 곳과 다른 위치에 개행이 들어갔다. 크기를 맞추려는 시도를 버리고 스크롤 주체를 textarea 로 두고 색칠 층과 줄 번호를 transform 으로 같은 양만큼 이동시킨다. 크기 계산이 아예 없으므로 어긋날 여지가 없다. 줄 번호 칸도 코드 영역 왼쪽의 독립 박스로 바꿔(sticky 제거) 가로 스크롤과 무관해졌다. 검증(브라우저 실측): 가로 스크롤 33px·세로 200px 에서 색칠 층과 줄 번호가 정확히 같은 양만큼 이동, 스크롤 0 에서 두 층의 텍스트 원점 일치, 줄 번호 개수가 줄 수와 일치(10/10, 70/70). 유닛테스트 45개 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,13 +69,11 @@
|
||||
줄바꿈을 하지 않고(wrap=off) 가로로 스크롤하므로 줄 번호가 항상 맞는다.
|
||||
두 요소의 폰트·여백이 다르면 글자가 어긋난다 — CSS 에서 함께 관리한다. #}
|
||||
<div class="cf24-code" id="cf24-code-pc">
|
||||
<div class="cf24-code-rows">
|
||||
<div class="cf24-gutter" id="cf24-gutter-pc" aria-hidden="true"></div>
|
||||
<div class="cf24-code-body">
|
||||
<pre class="cf24-code-hl" aria-hidden="true"><code id="cf24-hl-pc"></code></pre>
|
||||
<textarea id="cf24-html-pc" class="cf24-code-input" name="html" wrap="off"
|
||||
spellcheck="false" autocapitalize="off" autocorrect="off">{{ html_pc }}</textarea>
|
||||
</div>
|
||||
<div class="cf24-gutter" aria-hidden="true"><div class="cf24-gutter-inner" id="cf24-gutter-pc"></div></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814h" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -161,24 +161,30 @@
|
||||
gutter.dataset.lines = String(count);
|
||||
}
|
||||
|
||||
// 스크롤 주체는 textarea 다. 색칠 층과 줄 번호를 같은 양만큼 이동시켜
|
||||
// 글자 위치를 맞춘다(크기를 계산해 맞추는 방식은 긴 줄에서 어긋났다).
|
||||
function sync() {
|
||||
var x = ta.scrollLeft, y = ta.scrollTop;
|
||||
hl.style.transform = "translate(" + -x + "px," + -y + "px)";
|
||||
if (gutter) gutter.style.transform = "translateY(" + -y + "px)";
|
||||
}
|
||||
|
||||
function repaint() {
|
||||
// 마지막 줄이 잘리지 않게 개행을 하나 덧붙인다(<pre> 특성).
|
||||
if (ta.value.length > HL_LIMIT) hl.textContent = ta.value + "\n";
|
||||
else hl.innerHTML = paintHtml(ta.value) + "\n";
|
||||
renderGutter(ta.value.split("\n").length);
|
||||
// 크기는 <pre> 가 정하고 textarea 가 그 위를 덮는다. 다시 칠하는 사이
|
||||
// 내부 스크롤이 생겼으면 되돌려 두 층의 정렬을 유지한다.
|
||||
ta.scrollTop = 0;
|
||||
ta.scrollLeft = 0;
|
||||
sync();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// 짧은 소스는 바로 칠해야 줄 번호·상자 크기가 즉시 따라온다.
|
||||
// 짧은 소스는 바로 칠해야 줄 번호가 즉시 따라온다.
|
||||
if (ta.value.length < 50000) { repaint(); return; }
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(repaint, 80);
|
||||
}
|
||||
|
||||
ta.addEventListener("scroll", sync);
|
||||
ta.addEventListener("input", function () { dirty = true; refresh(); });
|
||||
// Tab 은 포커스 이동이 아니라 들여쓰기로 쓴다.
|
||||
ta.addEventListener("keydown", function (e) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814h" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814h" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
Reference in New Issue
Block a user