- Banner-corner auth link is now a button opening paskia-js's profile() dialog (handles login too); auth re-probed when the dialog closes. - Editor/analytics components call /_api via apiFetch/apiJson: an expired session opens the login dialog and the request retries. - pagerite.js: task-checkbox toggle uses apiJson (explicit edit attempt, reverts on any failure incl. cancelled login); auth probes use fetchJson (never a dialog); page-cache/navigation stay on plain fetch. - Add the paskia npm dependency; document the convention.
167 lines
7.7 KiB
JavaScript
167 lines
7.7 KiB
JavaScript
// Shared in-place page re-rendering for the editor tabs: fetch a page
|
|
// without transitions and swap its dynamic regions into the live document.
|
|
// Used by BannerEditor (banner design changes), SiteEditor (theme changes)
|
|
// and StructureEditor (tree navigation).
|
|
|
|
import { apiFetch } from 'paskia'
|
|
|
|
// Drop the public page runtime's in-memory prefetch cache. Editors call this
|
|
// whenever a site-wide or page change invalidates the cached HTML of other
|
|
// pages (theme, headings, structure, banner, etc.). The cache is rebuilt by
|
|
// re-preloading visible links once the editor panel closes.
|
|
export function dropPageCache() {
|
|
dispatchEvent(new CustomEvent('pagerite:drop-page-cache'))
|
|
}
|
|
|
|
// The editor's language override (set by EditorShell): while the panel is
|
|
// open, its language selection wins over the normal preferences — every
|
|
// in-place re-render asks for that language explicitly, and pagerite.js
|
|
// applies it to its own fetches and prefetches (pagerite:session-lang).
|
|
// The primary selection pins by its code: ?lang=<primary> selects the
|
|
// original explicitly (i18n.select_language). Panel closed, the session's
|
|
// chosen language (window.__pageriteLang) takes over — the pick stays.
|
|
let overrideLang = null // the ?lang= value in force, null = the session's
|
|
|
|
export function setLangOverride(queryLang) {
|
|
overrideLang = queryLang || null
|
|
dispatchEvent(new CustomEvent('pagerite:session-lang', { detail: { lang: overrideLang } }))
|
|
}
|
|
|
|
export function runScripts(root) {
|
|
// Scripts injected via innerHTML do not execute; re-create them.
|
|
if (!root) return
|
|
for (const old of root.querySelectorAll('script')) {
|
|
const s = document.createElement('script')
|
|
for (const a of old.attributes) s.setAttribute(a.name, a.value)
|
|
s.textContent = old.textContent
|
|
old.replaceWith(s)
|
|
}
|
|
}
|
|
|
|
function swapRegions(doc) {
|
|
for (const id of ['page-banner', 'nav', 'main']) {
|
|
const fresh = doc.getElementById(id)
|
|
const el = document.getElementById(id)
|
|
if (fresh && el) el.replaceWith(document.importNode(fresh, true))
|
|
}
|
|
// #sidebar is omitted entirely when the section has no sub-navigation,
|
|
// so it may be absent on either side: replace, insert, or remove.
|
|
const freshSidebar = doc.getElementById('sidebar')
|
|
const curSidebar = document.getElementById('sidebar')
|
|
if (freshSidebar && curSidebar) {
|
|
curSidebar.replaceWith(document.importNode(freshSidebar, true))
|
|
} else if (freshSidebar) {
|
|
document.getElementById('main')?.before(document.importNode(freshSidebar, true))
|
|
} else if (curSidebar) {
|
|
curSidebar.remove()
|
|
}
|
|
// The brand lives in the header, outside the swappable regions, and is
|
|
// absent entirely when neither a brand nor custom brand HTML is set. A
|
|
// plain link keeps its element (text swap only, preserving the shrink-
|
|
// to-fit observers); anything else (custom HTML wrapper) is replaced.
|
|
const freshBrand = doc.getElementById('brand')
|
|
const curBrand = document.getElementById('brand')
|
|
if (freshBrand && curBrand && freshBrand.tagName === 'A' && curBrand.tagName === 'A') {
|
|
curBrand.textContent = freshBrand.textContent
|
|
} else if (freshBrand && curBrand) {
|
|
curBrand.replaceWith(document.importNode(freshBrand, true))
|
|
runScripts(document.getElementById('brand'))
|
|
} else if (curBrand) {
|
|
curBrand.remove()
|
|
} else if (freshBrand) {
|
|
document.getElementById('nav')?.before(document.importNode(freshBrand, true))
|
|
runScripts(document.getElementById('brand'))
|
|
}
|
|
// Site-wide custom CSS is in <head> and must be swapped too.
|
|
const freshUserStyle = doc.getElementById('pagerite-user')
|
|
const curUserStyle = document.getElementById('pagerite-user')
|
|
if (freshUserStyle && curUserStyle) {
|
|
curUserStyle.textContent = freshUserStyle.textContent
|
|
} else if (freshUserStyle) {
|
|
document.head.appendChild(document.importNode(freshUserStyle, true))
|
|
} else if (curUserStyle) {
|
|
curUserStyle.remove()
|
|
}
|
|
// Theme and other public stylesheets live in <head>, rendered with stable
|
|
// ids by the backend (links in dev, inline <style> elements in prod);
|
|
// sync them positionally so the custom CSS (rendered last) always keeps
|
|
// winning by order. Diff-based: unchanged sheets keep their elements, so
|
|
// their @keyframes are never torn down (re-creating keyframes would
|
|
// replay the editor's slide-in animation).
|
|
const sel = 'link[rel="stylesheet"][id], style[id]'
|
|
const freshEls = [...doc.head.querySelectorAll(sel)]
|
|
const freshIds = new Set(freshEls.map((el) => el.id))
|
|
for (const el of [...document.head.querySelectorAll(sel)]) {
|
|
if (!freshIds.has(el.id)) el.remove()
|
|
}
|
|
// Insert missing sheets in the fresh document's order, each right after
|
|
// its predecessor's element. The first sheet rendered is always the base
|
|
// CSS, so its element doubles as the fallback anchor when nothing matched
|
|
// yet (e.g. no theme was selected before and the position is otherwise
|
|
// lost).
|
|
let anchor = null
|
|
for (const el of freshEls) {
|
|
const cur = el.id && document.getElementById(el.id)
|
|
if (cur && cur.outerHTML === el.outerHTML) {
|
|
anchor = cur
|
|
continue
|
|
}
|
|
const imported = document.importNode(el, true)
|
|
// Same id, new content (theme switch): replace in place, keeping position.
|
|
if (cur) cur.replaceWith(imported)
|
|
else if (anchor) anchor.after(imported)
|
|
else {
|
|
const base = document.getElementById('pagerite-base')
|
|
if (base) base.after(imported)
|
|
else document.head.append(imported)
|
|
}
|
|
anchor = imported
|
|
}
|
|
// The served language rides on <html> (lang + dir, rtl for e.g. Arabic):
|
|
// follow the swapped page. The editor panel carries its own lang="en"
|
|
// dir="ltr", so it is unaffected.
|
|
document.documentElement.lang = doc.documentElement.lang
|
|
document.documentElement.dir = doc.documentElement.dir
|
|
// The editor keeps its own title while open; only inherit the server title
|
|
// when navigating outside the editor (e.g. fetch-navigation swaps).
|
|
if (!document.body.classList.contains('editing')) {
|
|
document.title = doc.title
|
|
}
|
|
}
|
|
|
|
// Fetch /p, swap its regions into the live page and replaceState to it.
|
|
// Returns the final URL (after redirects), or null when the fetch did not
|
|
// yield a page. Category and missing URLs render a placeholder 404 page —
|
|
// fine to swap in (new pages are created by editing them). The fetch pins
|
|
// the editor's language override, or — panel closed — the session's chosen
|
|
// language (window.__pageriteLang).
|
|
export async function loadPlain(p) {
|
|
let doc
|
|
let finalUrl = `/${p}`
|
|
let html
|
|
try {
|
|
const pin = overrideLang || window.__pageriteLang
|
|
const res = await apiFetch(pin ? `${finalUrl}?lang=${pin}` : finalUrl)
|
|
const type = res.headers.get('content-type') || ''
|
|
if (!type.includes('text/html')) return null
|
|
if (res.redirected) finalUrl = res.url
|
|
html = await res.text()
|
|
doc = new DOMParser().parseFromString(html, 'text/html')
|
|
} catch { return null }
|
|
if (!doc.getElementById('main')) return null
|
|
swapRegions(doc)
|
|
// The address bar keeps the pretty URL: a language query is a fetch
|
|
// detail, never shown (pagerite.js's initial ?lang= works the same).
|
|
const pretty = new URL(finalUrl, location.href)
|
|
pretty.searchParams.delete('lang')
|
|
history.replaceState(history.state, '', pretty)
|
|
runScripts(document.getElementById('page-banner'))
|
|
runScripts(document.getElementById('main'))
|
|
// Keep pagerite.js's in-memory page cache in sync with the fresh copy.
|
|
// The URL is announced as fetched: a language-pinned copy caches under
|
|
// its own ?lang= key, where navigation with the same pin finds it.
|
|
dispatchEvent(new CustomEvent('pagerite:page-fetched', { detail: { url: finalUrl, html } }))
|
|
dispatchEvent(new CustomEvent('pagerite:preview')) // re-inject + re-tuck the edit pens
|
|
return finalUrl
|
|
}
|