Fix document editor scrollbar and line-number sync
Fixes #1501 Fixes #1496
This commit is contained in:
@@ -6322,13 +6322,170 @@ import * as Modals from './modalManager.js';
|
||||
}
|
||||
|
||||
/** Update the line number gutter */
|
||||
function updateLineNumbers(text) {
|
||||
let _lineNumberResizeObserver = null;
|
||||
let _lineNumberObservedTextarea = null;
|
||||
let _lineNumberResizeRaf = null;
|
||||
|
||||
function _lineNumberContentEl(gutter) {
|
||||
let inner = gutter.querySelector('.doc-line-number-content');
|
||||
if (!inner) {
|
||||
inner = document.createElement('div');
|
||||
inner.className = 'doc-line-number-content';
|
||||
gutter.textContent = '';
|
||||
gutter.appendChild(inner);
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
|
||||
function _lineNumberStyleSignature(style) {
|
||||
return [
|
||||
style.fontFamily,
|
||||
style.fontSize,
|
||||
style.fontWeight,
|
||||
style.fontStyle,
|
||||
style.lineHeight,
|
||||
style.letterSpacing,
|
||||
style.tabSize,
|
||||
style.fontFeatureSettings,
|
||||
style.fontVariantLigatures,
|
||||
style.fontKerning,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function _textareaTextWidth(textarea, style) {
|
||||
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
||||
const paddingRight = parseFloat(style.paddingRight) || 0;
|
||||
return Math.max(0, textarea.clientWidth - paddingLeft - paddingRight);
|
||||
}
|
||||
|
||||
function _lineHeightPx(style) {
|
||||
const parsed = parseFloat(style.lineHeight);
|
||||
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
||||
const fontSize = parseFloat(style.fontSize) || 11;
|
||||
return fontSize * 1.45;
|
||||
}
|
||||
|
||||
function _lineNumberMeasureEl(textarea) {
|
||||
const wrap = document.getElementById('doc-editor-wrap') || textarea.parentElement || document.body;
|
||||
let probe = wrap.querySelector('.doc-line-number-measure');
|
||||
if (!probe) {
|
||||
probe = document.createElement('textarea');
|
||||
probe.className = 'doc-line-number-measure';
|
||||
probe.setAttribute('aria-hidden', 'true');
|
||||
probe.tabIndex = -1;
|
||||
probe.readOnly = true;
|
||||
probe.wrap = 'soft';
|
||||
wrap.appendChild(probe);
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
function _syncLineNumberMeasureStyle(probe, style, textWidth) {
|
||||
probe.style.width = textWidth + 'px';
|
||||
probe.style.fontFamily = style.fontFamily;
|
||||
probe.style.fontSize = style.fontSize;
|
||||
probe.style.fontWeight = style.fontWeight;
|
||||
probe.style.fontStyle = style.fontStyle;
|
||||
probe.style.lineHeight = style.lineHeight;
|
||||
probe.style.letterSpacing = style.letterSpacing;
|
||||
probe.style.tabSize = style.tabSize;
|
||||
probe.style.fontFeatureSettings = style.fontFeatureSettings;
|
||||
probe.style.fontVariantLigatures = style.fontVariantLigatures;
|
||||
probe.style.fontKerning = style.fontKerning;
|
||||
probe.style.textRendering = style.textRendering;
|
||||
probe.style.whiteSpace = style.whiteSpace;
|
||||
probe.style.wordWrap = style.wordWrap;
|
||||
probe.style.overflowWrap = style.overflowWrap;
|
||||
}
|
||||
|
||||
function _measureLineNumberHeights(textarea, lines, textWidth, style) {
|
||||
const probe = _lineNumberMeasureEl(textarea);
|
||||
_syncLineNumberMeasureStyle(probe, style, textWidth);
|
||||
const lineHeight = _lineHeightPx(style);
|
||||
return lines.map(line => {
|
||||
probe.value = line || ' ';
|
||||
const visualRows = Math.max(1, Math.round(probe.scrollHeight / lineHeight));
|
||||
return visualRows * lineHeight;
|
||||
});
|
||||
}
|
||||
|
||||
function _renderLineNumberRows(inner, heights) {
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'doc-line-number-row';
|
||||
row.style.height = `${heights[i]}px`;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'doc-line-number-label';
|
||||
label.textContent = String(i + 1);
|
||||
row.appendChild(label);
|
||||
frag.appendChild(row);
|
||||
}
|
||||
inner.textContent = '';
|
||||
inner.appendChild(frag);
|
||||
}
|
||||
|
||||
function _scheduleLineNumberRerender() {
|
||||
if (_lineNumberResizeRaf) return;
|
||||
const run = () => {
|
||||
_lineNumberResizeRaf = null;
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
if (textarea) updateLineNumbers(textarea.value, true);
|
||||
};
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
_lineNumberResizeRaf = requestAnimationFrame(run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureLineNumberResizeObserver(textarea) {
|
||||
if (typeof ResizeObserver === 'undefined') return;
|
||||
if (!_lineNumberResizeObserver) {
|
||||
_lineNumberResizeObserver = new ResizeObserver(_scheduleLineNumberRerender);
|
||||
}
|
||||
if (_lineNumberObservedTextarea === textarea) return;
|
||||
if (_lineNumberObservedTextarea) {
|
||||
_lineNumberResizeObserver.unobserve(_lineNumberObservedTextarea);
|
||||
}
|
||||
_lineNumberObservedTextarea = textarea;
|
||||
_lineNumberResizeObserver.observe(textarea);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', _scheduleLineNumberRerender);
|
||||
}
|
||||
|
||||
function updateLineNumbers(text, force = false) {
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
const gutter = document.getElementById('doc-line-numbers');
|
||||
if (!gutter) return;
|
||||
const count = (text || '').split('\n').length;
|
||||
let html = '';
|
||||
for (let i = 1; i <= count; i++) html += i + '\n';
|
||||
gutter.textContent = html;
|
||||
if (!textarea || !gutter) return;
|
||||
|
||||
const value = text || '';
|
||||
const lines = value.split('\n');
|
||||
const inner = _lineNumberContentEl(gutter);
|
||||
const style = getComputedStyle(textarea);
|
||||
const textWidth = _textareaTextWidth(textarea, style);
|
||||
const styleSig = _lineNumberStyleSignature(style);
|
||||
|
||||
_ensureLineNumberResizeObserver(textarea);
|
||||
if (
|
||||
!force &&
|
||||
inner._lineNumberText === value &&
|
||||
inner._lineNumberWidth === textWidth &&
|
||||
inner._lineNumberStyleSig === styleSig
|
||||
) {
|
||||
syncGutterScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
const heights = _measureLineNumberHeights(textarea, lines, textWidth, style);
|
||||
_renderLineNumberRows(inner, heights);
|
||||
inner._lineNumberText = value;
|
||||
inner._lineNumberWidth = textWidth;
|
||||
inner._lineNumberStyleSig = styleSig;
|
||||
syncGutterScroll();
|
||||
}
|
||||
|
||||
/** Sync line number gutter scroll with textarea */
|
||||
@@ -6336,7 +6493,7 @@ import * as Modals from './modalManager.js';
|
||||
const textarea = document.getElementById('doc-editor-textarea');
|
||||
const gutter = document.getElementById('doc-line-numbers');
|
||||
if (textarea && gutter) {
|
||||
gutter.scrollTop = textarea.scrollTop;
|
||||
_lineNumberContentEl(gutter).style.transform = `translateY(${-textarea.scrollTop}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12075,10 +12075,47 @@ textarea.memory-add-input {
|
||||
background: var(--bg);
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
font-variant-ligatures: none !important;
|
||||
font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
|
||||
font-kerning: none !important;
|
||||
text-rendering: geometricPrecision !important;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
.doc-line-number-content {
|
||||
display: block;
|
||||
will-change: transform;
|
||||
}
|
||||
.doc-line-number-row {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.doc-line-number-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 36px;
|
||||
text-align: right;
|
||||
}
|
||||
.doc-line-number-measure {
|
||||
position: absolute !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
left: -9999px !important;
|
||||
top: 0 !important;
|
||||
height: 0 !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
border: 0 !important;
|
||||
resize: none !important;
|
||||
box-sizing: content-box !important;
|
||||
color: transparent !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Find marks live in the syntax-highlight overlay, which sits at
|
||||
z-index:0 under a transparent textarea — so they're always visible
|
||||
through the text layer. The previous color-mix variant could
|
||||
@@ -12209,11 +12246,11 @@ mark.doc-find-mark.current {
|
||||
area — caret stays right, but typed text appears on a different row
|
||||
than the caret. */
|
||||
scrollbar-gutter: stable;
|
||||
/* The highlight overlay hides its scrollbar, so the textarea must too —
|
||||
otherwise the scrollbar shrinks the textarea's text-area width and
|
||||
wraps lines earlier than the overlay, putting the caret on the wrong
|
||||
line entirely. */
|
||||
scrollbar-width: none;
|
||||
/* Show a real scrollbar for long documents. scrollbar-gutter above keeps
|
||||
the text column stable so the gutter, textarea, and find overlay stay
|
||||
metrically aligned while the scrollbar is present. */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--fg) 28%, transparent) transparent;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
tab-size: 4;
|
||||
white-space: pre-wrap;
|
||||
@@ -12231,7 +12268,15 @@ mark.doc-find-mark.current {
|
||||
font-kerning: none !important;
|
||||
text-rendering: geometricPrecision !important;
|
||||
}
|
||||
.doc-editor-textarea::-webkit-scrollbar { display: none; }
|
||||
.doc-editor-textarea::-webkit-scrollbar { width: 8px; }
|
||||
.doc-editor-textarea::-webkit-scrollbar-track { background: transparent; }
|
||||
.doc-editor-textarea::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--fg) 24%, transparent);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.doc-editor-textarea::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--fg) 36%, transparent);
|
||||
}
|
||||
.doc-editor-textarea:hover,
|
||||
.doc-editor-textarea:focus,
|
||||
.doc-editor-textarea:active {
|
||||
|
||||
Reference in New Issue
Block a user