Files
pagerite/docs/editing.md
T
LeoVasanko 9adc48479f 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.
2026-08-29 05:08:52 +00:00

7.1 KiB

Editing interface

The Vue editor is a single tabbed EditorShell.vue mounted in a host div created inside the static document.

Tabs

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, 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.
  • 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.

Media uploads everywhere use the image icon buttons (pasting into the editor works too). The article, banner and site-settings pens are shorthands that open the shell on the matching tab; once open, clicking a pen switches tabs (and retargets the editors to the current page) instead of closing/remounting. The close button in the tab bar closes the shell (deliberately NOT Escape — it fired too easily by accident); tabs have no close buttons of their own. Closing only HIDES the shell — the Vue app stays mounted, so page-editor state (unsaved text included) survives until a real page reload; the editor always follows the URL, so fetch-navigating with the shell open (or before re-opening it) retargets it to the new page — unsaved text is stashed per path for the session and restored when returning, cleared on save. Saving there is explicit (Ctrl+S) and refreshes the page regions in place. Admin panels never reload the page.

In-place page re-rendering shared by the banner/site/structure tabs lives in swapdoc.js (runScripts/loadPlain: fetch a page, swap the dynamic regions, replaceState). It also exports dropPageCache, which the editor tabs call after any save that can alter the rendered HTML of other pages (theme, headings, structure, banners, site brand/CSS, favicon). Dropping the cache while editing avoids re-fetching every page immediately; the public runtime re-preloads visible links once the editor panel closes.

Saving behavior

Everything saves immediately as you edit (brand/title/CSS debounced, slug on commit since it renames the path), theme change swaps the stylesheet in place, tree rows navigate in place without transitions when focused, and the front page is a root-only row whose empty slug is editable like any other. Saves that can affect other pages drop the prefetch cache; the cache is rebuilt when the editor panel closes so navigation stays instant.

Every non-empty list (and the root) ends with a non-draggable plus footer row (vuedraggable #footer slot): clicking it starts a new pending page at that level (its slug placeholder shows the slug derived live from the title being typed), and while dragging it is the list's "end of list" drop target. Committing a pending page PUTs it with empty markdown (creates an empty page that renders with its title — saving never deletes; deletion is the page editor's explicit choice: saving trimmed-empty text issues a REST DELETE), then switches to the page editor tab for the actual writing.

Dropping ON the lower part of a row moves the page under that row (the child list's container invisibly overlaps its own row's bottom via negative margin — Sortable inserts it as the first child natively), while a row's exposed top edge inserts a sibling before it. Row indentation is structural (each nested list margin-indents itself), so a dragged row previews its whole subtree at the target list's depth.

The shell is dynamic-imported onto the content page by pagerite.js when an edit pen is clicked (the pens are injected by pagerite.js after the session validates; they carry data-editor-src/data-editor-css/data-editor-mode). In dev, modules load from the Vite dev server (PAGERITE_VITE_URL), in prod from the hashed build assets resolved via frontend-build/.vite/manifest.json.

vite.config.js sets appType: 'mpa' (no SPA fallback) and builds with manifest: true, assetsDir: '_/assets' (so the build mirrors the URL space; frontend/public/favicon.ico lands at the build root and is served at /favicon.ico). JS inputs are src/main.js and src/pagerite.js, plus src/assets/pagerite.css as a separate stylesheet entry; theme, banner-design and transition CSS are NOT built — they live in pagerite/themes/{name}/ and are served by the backend. There is no index.html source (it would shadow / and turn missing dev paths into an empty Vue shell). All outputs are ES modules. The build sets preserveEntrySignatures: 'exports-only' because main.js is consumed via dynamic import() for its openEditor/closeEditor exports — Vite app builds otherwise strip unused entry exports, leaving dead edit pens. In dev the backend links theme/banner-design stylesheets like in prod (/_themes/...); only the base CSS is Vite-injected from JS, and pagerite.js then re-appends the #pagerite-theme/#pagerite-banner/#pagerite-transition/#pagerite-user elements to restore the canonical order (base < theme < design < transition < custom CSS). In production all page assets are inlined instead (styles as <style id="pagerite-…"> in <head>, scripts at the end of the body). Theme switches in the site editor swap the #pagerite-theme element in place — the link href in dev, the inline style's text (fetched from /_themes/...) in prod.

vite-plugin-fastapi.js has an auto-upgrade marker — edit vite.config.js, not the plugin.