Scroll page on cursor move only when cursor leaves viewport
Cursor-driven editor→page scroll sync pinned the cursor's page position at a fixed window height, so every cursor move dragged the page along. Now the page scrolls only when the cursor's mapped position crosses a viewport edge margin, and just enough to bring it back inside.
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ The Vue editor is a single tabbed `EditorShell.vue` mounted in a host div create
|
|||||||
|
|
||||||
The shell hosts four kept-alive tabs (ordered site-wide first — site, structure — then, after a visual break, the per-page tabs — article, banner):
|
The shell hosts four kept-alive tabs (ordered site-wide first — site, structure — then, after a visual break, the per-page tabs — article, banner):
|
||||||
|
|
||||||
- `PageEditor.vue` — CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor and article scrolls are linked piecewise-linearly, keyed on the section anchors' `data-line` (markdown source line the backend stamps on top-level anchored h1/h2s): the page follows the cursor (fractional, wrap-aware, anchored at a fixed window height), the editor follows page scroll with a progress-based viewport anchor, applied instantly (the window keeps scrolling normally while any editor is open — the panel is fixed to the viewport's left edge, its top tracking the banner's bottom edge until the banner scrolls away — and the panel scrolls internally); anchored h2s carry their own edit pens that open the editor scrolled to that section; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload (always with an empty `""` caption, cursor inside the quotes), toggling fences (` ``` ` code blocks and `::: aside` containers share the same machinery: clicked inside one they remove it and select the content, otherwise they wrap the selection or the cursor's line, keeping it selected), and `.left`/`.right`/`.wide`/`.margin` placement toggles plus `.small`/`.large`/`.huge` text-size toggles (brace attributes on the block at the cursor, mutually exclusive within each group; on `:::` containers a placement class replaces the container name instead — `::: aside` → `::: margin`), with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path.
|
- `PageEditor.vue` — CodeMirror + server-rendered preview over WebSocket `/_api/ws/editor`, previewing into the visible article; editor and article scrolls are linked piecewise-linearly, keyed on the section anchors' `data-line` (markdown source line the backend stamps on top-level anchored h1/h2s): the page follows the cursor (fractional, wrap-aware, scrolling only when the cursor's page position leaves the viewport, with an edge margin), the editor follows page scroll with a progress-based viewport anchor, applied instantly (the window keeps scrolling normally while any editor is open — the panel is fixed to the viewport's left edge, its top tracking the banner's bottom edge until the banner scrolls away — and the panel scrolls internally); anchored h2s carry their own edit pens that open the editor scrolled to that section; a format bar offers Markdown helpers — bold/italic/code/link/table/image upload (always with an empty `""` caption, cursor inside the quotes), toggling fences (` ``` ` code blocks and `::: aside` containers share the same machinery: clicked inside one they remove it and select the content, otherwise they wrap the selection or the cursor's line, keeping it selected), and `.left`/`.right`/`.wide`/`.margin` placement toggles plus `.small`/`.large`/`.huge` text-size toggles (brace attributes on the block at the cursor, mutually exclusive within each group; on `:::` containers a placement class replaces the container name instead — `::: aside` → `::: margin`), with Ctrl/Cmd-B/I/S bindings — for the hard-to-remember syntax. Edits content and title only, never the path.
|
||||||
- `BannerEditor.vue` — per-page banner HTML + banner design selector, previewed into `#page-banner`.
|
- `BannerEditor.vue` — per-page banner HTML + banner design selector, previewed into `#page-banner`.
|
||||||
- `SiteEditor.vue` — site brand + optional custom brand HTML with image/video upload + theme selector + page-transition selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into `<head id="pagerite-user">`.
|
- `SiteEditor.vue` — site brand + optional custom brand HTML with image/video upload + theme selector + page-transition selector + font picker + favicon upload — clicking the preview tile picks a new one — + site-wide custom CSS, CSS injected into `<head id="pagerite-user">`.
|
||||||
- `StructureEditor.vue` — the vue-draggable structure tree with always-editable title/slug inputs per row.
|
- `StructureEditor.vue` — the vue-draggable structure tree with always-editable title/slug inputs per row.
|
||||||
|
|||||||
@@ -664,9 +664,10 @@ function onEditorShown() {
|
|||||||
//
|
//
|
||||||
// Editor → page follows the CURSOR, not the editor viewport: the cursor's
|
// Editor → page follows the CURSOR, not the editor viewport: the cursor's
|
||||||
// fractional line (soft-wrap included, so moving inside a wrapped
|
// fractional line (soft-wrap included, so moving inside a wrapped
|
||||||
// paragraph tracks smoothly) maps to its page position, shown at a fixed
|
// paragraph tracks smoothly) maps to its page position. The page only
|
||||||
// anchor height in the window — the cursor on the last line lands at the
|
// scrolls when that position leaves the viewport (with an edge margin),
|
||||||
// end of the page, no ramping needed. Only cursor/selection changes drive
|
// and then just enough to bring it back inside — cursor movement within
|
||||||
|
// view never drags the page along. Only cursor/selection changes drive
|
||||||
// this direction: editor wheel-scrolling repositions the text, not the
|
// this direction: editor wheel-scrolling repositions the text, not the
|
||||||
// page, which removes the scroll→scroll echo entirely.
|
// page, which removes the scroll→scroll echo entirely.
|
||||||
// Page → editor anchors a viewport fraction that grows with page progress
|
// Page → editor anchors a viewport fraction that grows with page progress
|
||||||
@@ -719,8 +720,8 @@ function editorTopFor(line) {
|
|||||||
return Math.min(max, block.top + (line - n) * block.height)
|
return Math.min(max, block.top + (line - n) * block.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
//: Window height fraction where the cursor's page position is shown.
|
//: Edge margin (window height fraction) for cursor-driven page scrolls.
|
||||||
const CURSOR_ANCHOR = 1 / 3
|
const CURSOR_MARGIN = 1 / 8
|
||||||
|
|
||||||
function syncWindowToEditor() {
|
function syncWindowToEditor() {
|
||||||
if (syncingScroll || !view) return
|
if (syncingScroll || !view) return
|
||||||
@@ -728,7 +729,10 @@ function syncWindowToEditor() {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const pts = syncPoints()
|
const pts = syncPoints()
|
||||||
if (pts) {
|
if (pts) {
|
||||||
// The cursor's page position, shown at a fixed window height.
|
// Scroll the page only when the cursor's page position leaves the
|
||||||
|
// viewport (minus an edge margin): while it stays visible the page
|
||||||
|
// keeps its position, so cursor movement does not drag the page
|
||||||
|
// along; crossing an edge scrolls just enough to bring it back.
|
||||||
const pos = view.state.selection.main.head
|
const pos = view.state.selection.main.head
|
||||||
const coords = view.coordsAtPos(pos)
|
const coords = view.coordsAtPos(pos)
|
||||||
if (coords) {
|
if (coords) {
|
||||||
@@ -739,8 +743,14 @@ function syncWindowToEditor() {
|
|||||||
? Math.max(0, Math.min(1, (docY - block.top) / block.height))
|
? Math.max(0, Math.min(1, (docY - block.top) / block.height))
|
||||||
: 0
|
: 0
|
||||||
const line = view.state.doc.lineAt(pos).number + frac
|
const line = view.state.doc.lineAt(pos).number + frac
|
||||||
const y = interp(pts, line, 0, 1) - CURSOR_ANCHOR * innerHeight
|
const y = interp(pts, line, 0, 1)
|
||||||
if (Math.abs(scrollY - y) > 1) scrollTo({ top: Math.max(0, y), behavior: 'instant' })
|
const margin = CURSOR_MARGIN * innerHeight
|
||||||
|
let target = null
|
||||||
|
if (y < scrollY + margin) target = y - margin
|
||||||
|
else if (y > scrollY + innerHeight - margin) target = y - innerHeight + margin
|
||||||
|
if (target !== null && Math.abs(scrollY - target) > 1) {
|
||||||
|
scrollTo({ top: Math.max(0, target), behavior: 'instant' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestAnimationFrame(() => { syncingScroll = false })
|
requestAnimationFrame(() => { syncingScroll = false })
|
||||||
|
|||||||
Reference in New Issue
Block a user