🐛 fix layout shift on new lines
This commit is contained in:
@@ -437,11 +437,24 @@ const hrPlugin = ViewPlugin.fromClass(
|
|||||||
{ decorations: v => v.decorations }
|
{ decorations: v => v.decorations }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Paragraph indent plugin — adds first-line indent to paragraph lines.
|
// Paragraph indent plugin — inserts a zero-height 2em widget at the start of
|
||||||
// Paragraphs in the manuscript use single newlines (no blank separators),
|
// each paragraph line. Widgets are accounted for in CodeMirror's cursor
|
||||||
// so every text line is a paragraph. We indent all of them except the first
|
// positioning, so the cursor on a blank line already sits at the indent
|
||||||
// paragraph after a heading, HR, or document start (traditional typography).
|
// position before any text is typed — eliminating the layout shift.
|
||||||
const noIndentDeco = Decoration.line({ class: 'cm-no-indent' })
|
// Being an inline element at position 0, it only indents the first visual
|
||||||
|
// line of each wrapped paragraph (true first-line indent, not block indent).
|
||||||
|
class IndentWidget extends WidgetType {
|
||||||
|
eq(other: WidgetType): boolean { return other instanceof IndentWidget }
|
||||||
|
toDOM(): HTMLElement {
|
||||||
|
const span = document.createElement('span')
|
||||||
|
span.style.cssText = 'display:inline-block;width:2em;height:0'
|
||||||
|
span.setAttribute('aria-hidden', 'true')
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
ignoreEvent() { return true }
|
||||||
|
}
|
||||||
|
|
||||||
|
const indentWidgetDeco = Decoration.widget({ widget: new IndentWidget(), side: -1 })
|
||||||
|
|
||||||
function buildParagraphDecos(view: EditorView): DecorationSet {
|
function buildParagraphDecos(view: EditorView): DecorationSet {
|
||||||
const builder = new RangeSetBuilder<Decoration>()
|
const builder = new RangeSetBuilder<Decoration>()
|
||||||
@@ -452,13 +465,10 @@ function buildParagraphDecos(view: EditorView): DecorationSet {
|
|||||||
const text = line.text
|
const text = line.text
|
||||||
const isHeading = /^#{1,6} /.test(text)
|
const isHeading = /^#{1,6} /.test(text)
|
||||||
const isHR = /^---+$/.test(text.trim())
|
const isHR = /^---+$/.test(text.trim())
|
||||||
const isSpecial = /^[-*>]/.test(text) // list / blockquote / blank
|
const isSpecial = /^[-*>]/.test(text) // list / blockquote
|
||||||
|
|
||||||
// Only suppress indent for structural lines — never for regular text.
|
if (!isHeading && !isHR && !isSpecial) {
|
||||||
// This ensures no text line ever changes its decoration status based on
|
builder.add(line.from, line.from, indentWidgetDeco)
|
||||||
// neighboring lines, eliminating cascade-induced layout shifts.
|
|
||||||
if (isHeading || isHR || isSpecial) {
|
|
||||||
builder.add(line.from, line.from, noIndentDeco)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return builder.finish()
|
return builder.finish()
|
||||||
@@ -504,8 +514,6 @@ function buildTheme(fontSize: number, dark: boolean, focusMode = false): ReturnT
|
|||||||
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
|
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
|
||||||
'.cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
'.cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
||||||
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
||||||
'.cm-line': { textIndent: '2em' },
|
|
||||||
'.cm-no-indent': { textIndent: '0' },
|
|
||||||
'.cm-gutters': { display: 'none' },
|
'.cm-gutters': { display: 'none' },
|
||||||
'.cm-activeLine': { backgroundColor: 'transparent' },
|
'.cm-activeLine': { backgroundColor: 'transparent' },
|
||||||
'.cm-activeLineGutter': { backgroundColor: 'transparent' },
|
'.cm-activeLineGutter': { backgroundColor: 'transparent' },
|
||||||
|
|||||||
Reference in New Issue
Block a user