Files
pagerite/frontend/src/main.js
T
LeoVasanko 4dac16a9ac Unify admin editors into a tabbed EditorShell (site, structure, article, banner)
- One mounted shell with four kept-alive tabs; pens open/switch tabs,
  closing only hides the shell so unsaved page-editor state survives
  until a real reload; admin panels never reload the page, regions are
  refreshed in place via the shared swapdoc.js helper
- SiteEditor split: site settings vs. the new StructureEditor tree tab;
  BannerEditor split out of the old site editor
- Article editor format bar (bold/italic/code/link/table/image) with a
  table size picker and Ctrl/Cmd-B/I/S bindings; fenced code block on an
  empty line; icon-only save button disabled (saturate(0)) when clean
- Media uploads use icon buttons; favicon upload by clicking the preview
  tile; placeholders reserved for actual defaults in effect
- Banner/site pens and auth buttons grouped in an .editor-pens container;
  page pen no longer shown to anonymous visitors
- While editing, window scroll is locked, the panel exactly fills the
  available window height, and only #main scrolls; editor scroll drives
  the article
- Banner design inherit label names the design and its true source;
  backend banner_design_source excludes the node's own setting and the
  doc payload carries banner_design_inherited
2026-08-19 00:14:05 +00:00

99 lines
3.7 KiB
JavaScript

// Pagerite editor entry. A single tabbed EditorShell is mounted in a
// dynamically created host div inside the static document. The shell hosts
// four tabs: PageEditor (Markdown + preview), BannerEditor (per-page banner
// HTML/design), SiteEditor (site-wide config), and StructureEditor (the
// site structure tree).
//
// Individual pens are shorthands that open the shell on a particular tab;
// once the shell is open, pens switch tabs instead of closing/remounting.
// Closing hides the shell but keeps the Vue app mounted, so editor state —
// including unsaved page text — survives and editing can continue on the
// next pen click; it is lost only on a real page reload.
if (import.meta.env.DEV) {
// Base styles only; the theme CSS is imported by pagerite.js (which
// always runs first — the editor opens from public pages).
import("./assets/pagerite.css");
}
import { createApp } from 'vue'
import EditorShell from './EditorShell.vue'
let host = null
let app = null
let savedTitle = null
let visible = false
export function openEditor(path, { mode = 'page' } = {}) {
if (app) {
// Shell already mounted: re-show it if hidden, switch to the requested
// tab and retarget the editors to the current page (the shell survives
// fetch-navigation).
if (!visible) showEditor()
document.body.dataset.editorMode = mode
dispatchEvent(new CustomEvent('pagerite:switch-editor', { detail: { mode, path } }))
return
}
savedTitle = document.title
host = document.createElement('div')
host.className = 'editor-host'
// Docked inside #content: below the banner, next to the article only.
document.getElementById('content').prepend(host)
document.body.classList.add('editing')
// Which tab is active; pagerite.js uses this to decide whether a pen click
// closes the panel or switches tabs.
document.body.dataset.editorMode = mode
visible = true
app = createApp(EditorShell, {
pagePath: path,
initialMode: mode,
onClose: closeEditor,
})
app.mount(host)
// The slide-in (editor-slide-in in pagerite.css) is a one-shot open
// effect; once finished, drop it so that later stylesheet swaps (theme
// change re-creating @keyframes) cannot restart it.
const root = host.firstElementChild
root?.addEventListener('animationend', function done(e) {
if (e.target !== root) return
root.removeEventListener('animationend', done)
root.style.animation = 'none'
})
}
function showEditor() {
savedTitle = document.title
host.style.display = ''
host.firstElementChild?.classList.remove('closing')
document.body.classList.add('editing')
visible = true
dispatchEvent(new CustomEvent('pagerite:editor-shown'))
}
export function closeEditor() {
if (!visible) return
visible = false
document.body.classList.remove('editing')
// dataset.editorMode is kept while hidden: the tabs use it to tell whether
// a pagerite:editor-shown event targets them.
// Slide the panel out in sync with the page shifting back, then hide it.
host.firstElementChild?.classList.add('closing')
const h = host
setTimeout(() => { h.style.display = 'none' }, 250)
dispatchEvent(new CustomEvent('pagerite:editor-hidden'))
// Restore the server-rendered title for the current URL. Re-fetching makes
// sure a brand change in the site editor or an in-place navigation leaves
// the correct public title behind.
const restoreTitle = savedTitle
savedTitle = null
fetch(location.pathname)
.then((r) => r.text())
.then((html) => {
if (visible) return // reopened meanwhile; the editor owns the title
const doc = new DOMParser().parseFromString(html, 'text/html')
if (doc.title) document.title = doc.title
})
.catch(() => {
if (!visible && restoreTitle != null) document.title = restoreTitle
})
}