#274: align editor line numbers with wrapped text rows #276
@@ -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');
|
||||||
|
const n = Math.max(lines.length, 1);
|
||||||
|
if (!document.documentElement.hasAttribute('data-wrap')) {
|
||||||
let s = '';
|
let s = '';
|
||||||
for (let i = 1; i <= Math.max(lines, 1); i++) s += i + '\n';
|
for (let i = 1; i <= n; i++) s += i + '\n';
|
||||||
gutter.textContent = s;
|
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