#274: align editor line numbers with wrapped text rows
With wrap on, a logical line occupies several visual rows in the textarea but the gutter showed one number per logical line, so every number after the first wrapped line drifted off its text (the paste view fixed this in #167; the editor gutter did not). Measure the wrapped row count per logical line with a hidden mirror div sharing the editor's font and wrapping rules, and render one .gutline block per visual row with the number on the first row of its logical line. Re-measure on input, wrap toggle and resize. Verified: gutter scrollHeight == textarea scrollHeight with zero diff at 1400x900 and 375x812, wrap on and off.
This commit is contained in:
@@ -319,8 +319,10 @@ html[data-wrap] .float { overflow-x: hidden; }
|
|||||||
.codebody { padding: 0 18px; white-space: pre; overflow-x: auto; }
|
.codebody { padding: 0 18px; white-space: pre; overflow-x: auto; }
|
||||||
/* #167: each logical line is its own block so offsetTop identifies its first visual row */
|
/* #167: each logical line is its own block so offsetTop identifies its first visual row */
|
||||||
.codeline { display: block; }
|
.codeline { display: block; }
|
||||||
/* #167 rev: gutter number spans must stack one per visual row (wrap on) */
|
/* #167: gutter number spans must stack one per visual row (wrap on).
|
||||||
.code .gutter .gutline { display: block; }
|
#274: the /new editor gutter uses the same .gutline blocks when its own
|
||||||
|
wrap toggle is on, so scope the rule to any gutter, not just .code. */
|
||||||
|
.code .gutter .gutline, .editor-wrap .gutter .gutline { display: block; }
|
||||||
/* #194: codeline blocks are adjacent (no '\n' text between them), so an
|
/* #194: codeline blocks are adjacent (no '\n' text between them), so an
|
||||||
empty block (blank source line) needs its own line box to stay one row */
|
empty block (blank source line) needs its own line box to stay one row */
|
||||||
.codeline:empty::before { content: "\200B"; }
|
.codeline:empty::before { content: "\200B"; }
|
||||||
|
|||||||
@@ -2,13 +2,71 @@
|
|||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const content = $('content'), gutter = $('gutter');
|
const content = $('content'), gutter = $('gutter');
|
||||||
|
|
||||||
|
// #274: with wrap on, a logical line occupies several VISUAL rows in the
|
||||||
|
// textarea, so one number per logical line drifts off its text (same bug the
|
||||||
|
// paste view fixed in #167). A textarea can't be split into spans, so the
|
||||||
|
// wrapped row count per logical line is measured with a hidden mirror div
|
||||||
|
// that shares the editor's font, line metrics and wrapping rules, and the
|
||||||
|
// gutter renders one .gutline block per visual row with the number on the
|
||||||
|
// FIRST row of its logical line (fillers elsewhere).
|
||||||
|
let mirror = null;
|
||||||
|
function measureRows(lines) {
|
||||||
|
if (!mirror) {
|
||||||
|
mirror = document.createElement('div');
|
||||||
|
mirror.style.position = 'absolute';
|
||||||
|
mirror.style.visibility = 'hidden';
|
||||||
|
mirror.style.top = '0';
|
||||||
|
mirror.style.left = '-9999px';
|
||||||
|
document.body.appendChild(mirror);
|
||||||
|
}
|
||||||
|
const cs = getComputedStyle(content);
|
||||||
|
mirror.style.font = cs.font;
|
||||||
|
mirror.style.lineHeight = cs.lineHeight;
|
||||||
|
mirror.style.whiteSpace = 'pre-wrap';
|
||||||
|
mirror.style.overflowWrap = 'anywhere';
|
||||||
|
mirror.style.wordBreak = 'break-all';
|
||||||
|
mirror.style.width = (content.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight)) + 'px';
|
||||||
|
const lh = parseFloat(cs.lineHeight) || 1;
|
||||||
|
const starts = [];
|
||||||
|
let total = 0;
|
||||||
|
const n = Math.max(lines.length, 1);
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
// A trailing newline yields an empty last line: it still occupies one row.
|
||||||
|
mirror.textContent = lines[i] + '\n';
|
||||||
|
let rows = Math.max(1, Math.round(mirror.getBoundingClientRect().height / lh));
|
||||||
|
starts.push(total);
|
||||||
|
total += rows;
|
||||||
|
}
|
||||||
|
return { starts, total };
|
||||||
|
}
|
||||||
|
|
||||||
function updateGutter() {
|
function updateGutter() {
|
||||||
const lines = content.value.split('\n').length;
|
const lines = content.value.split('\n');
|
||||||
let s = '';
|
const n = Math.max(lines.length, 1);
|
||||||
for (let i = 1; i <= Math.max(lines, 1); i++) s += i + '\n';
|
if (!document.documentElement.hasAttribute('data-wrap')) {
|
||||||
gutter.textContent = s;
|
let s = '';
|
||||||
|
for (let i = 1; i <= n; i++) s += i + '\n';
|
||||||
|
gutter.textContent = s.slice(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { starts, total } = measureRows(lines);
|
||||||
|
gutter.textContent = '';
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const spans = [];
|
||||||
|
for (let r = 0; r < total; r++) {
|
||||||
|
const c = document.createElement('span');
|
||||||
|
c.className = 'gutline';
|
||||||
|
c.textContent = '\u00a0';
|
||||||
|
spans.push(c);
|
||||||
|
frag.appendChild(c);
|
||||||
|
}
|
||||||
|
gutter.appendChild(frag);
|
||||||
|
for (let j = 0; j < starts.length; j++) spans[starts[j]].textContent = String(j + 1);
|
||||||
}
|
}
|
||||||
content.addEventListener('input', updateGutter);
|
content.addEventListener('input', updateGutter);
|
||||||
|
// #274: the wrap toggle and width changes re-wrap the textarea; re-measure.
|
||||||
|
new MutationObserver(updateGutter).observe(document.documentElement, { attributes: true, attributeFilter: ['data-wrap'] });
|
||||||
|
window.addEventListener('resize', updateGutter);
|
||||||
// #259: the editor scrolls itself; keep the gutter's numbers in step with it.
|
// #259: the editor scrolls itself; keep the gutter's numbers in step with it.
|
||||||
content.addEventListener('scroll', () => { gutter.scrollTop = content.scrollTop; });
|
content.addEventListener('scroll', () => { gutter.scrollTop = content.scrollTop; });
|
||||||
updateGutter();
|
updateGutter();
|
||||||
|
|||||||
Reference in New Issue
Block a user