#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:
@@ -2,13 +2,71 @@
|
||||
const $ = id => document.getElementById(id);
|
||||
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() {
|
||||
const lines = content.value.split('\n').length;
|
||||
let s = '';
|
||||
for (let i = 1; i <= Math.max(lines, 1); i++) s += i + '\n';
|
||||
gutter.textContent = s;
|
||||
const lines = content.value.split('\n');
|
||||
const n = Math.max(lines.length, 1);
|
||||
if (!document.documentElement.hasAttribute('data-wrap')) {
|
||||
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);
|
||||
// #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.
|
||||
content.addEventListener('scroll', () => { gutter.scrollTop = content.scrollTop; });
|
||||
updateGutter();
|
||||
|
||||
Reference in New Issue
Block a user