Files
pagerite/frontend/src/EditorShell.vue
T
LeoVasanko 2a430247e0 Shared editor language selection; paced WebSocket reconnects
- LangSelect.vue: one language selector for the page and structure tabs
  (small flag, clean dropdown), v-modeled on the shell-wide editorLang —
  switching in either panel switches both AND the page preview. While the
  panel is open the selection overrides the normal language preferences
  (swapdoc.setLangOverride: loadPlain and pagerite.js fetches/prefetches
  pin ?lang=, the primary language by its own code); closing restores.
- reconnect.js: every socket (page/banner editors, analytics view, the
  activity channel) now connects through a staggered slot — simultaneous
  attempts at page load (Vite's HMR socket plus ours, repeated on every
  refresh) trip the browser's WebSocket throttling, leaving all sockets
  to the host "pending" for minutes, which was the recurring empty
  editor. A watchdog closes sockets stuck CONNECTING so they reschedule
  through the policy (jittered exponential backoff, reset only by a
  healthy connection) instead of hanging forever; a reconnected editor
  re-opens its document when the previous open died with its socket.
2026-09-03 01:47:24 +00:00

206 lines
6.0 KiB
Vue

<script setup>
// Tabbed shell for the four admin editors. The individual pens are shorthands
// that open the shell on a given tab; once open, tabs switch instantly without
// closing the panel. Tabs are kept alive so switching preserves state.
import { onMounted, onUnmounted, provide, ref, watch } from 'vue'
import PageEditor from './PageEditor.vue'
import BannerEditor from './BannerEditor.vue'
import SiteEditor from './SiteEditor.vue'
import StructureEditor from './StructureEditor.vue'
import LocalizationEditor from './LocalizationEditor.vue'
import { editorLang } from './editorLang'
import { loadPlain, setLangOverride } from './swapdoc'
const props = defineProps({
pagePath: { type: String, default: '' },
initialMode: { type: String, default: 'page' },
})
const emit = defineEmits(['close'])
const currentPath = ref(props.pagePath)
const activeMode = ref(props.initialMode)
// The shared language selection (./editorLang, v-modeled by the tabs'
// LangSelects) also drives the page preview: while the shell is open it
// overrides the normal language preferences (?lang= / Accept-Language),
// so the page renders in the language being edited; closing restores.
const primaryLang = ref('en')
let pinned = false
function pinPreviewLang() {
pinned = true
setLangOverride(editorLang.value || primaryLang.value)
loadPlain(currentPath.value)
}
function unpinPreviewLang() {
if (!pinned) return
pinned = false
setLangOverride(null)
loadPlain(currentPath.value)
}
watch(editorLang, () => { if (pinned) pinPreviewLang() })
// Tab order: site-wide settings first (site, structure, localization), then
// — after a visual break — the per-page editors (article, banner).
const MODES = [
{ key: 'site', label: 'site', component: SiteEditor },
{ key: 'structure', label: 'structure', component: StructureEditor },
{ key: 'localization', label: 'lang', component: LocalizationEditor },
{ key: 'page', label: 'article', component: PageEditor, breakBefore: true },
{ key: 'banner', label: 'banner', component: BannerEditor },
]
function switchMode(mode) {
if (MODES.some((m) => m.key === mode)) activeMode.value = mode
}
function onPathChange(path) {
currentPath.value = path
}
function close() {
emit('close')
}
provide('editorShell', { switchMode })
watch(activeMode, (mode) => {
document.body.dataset.editorMode = mode
}, { immediate: true })
// A pen click while the shell is open (main.js) switches tabs, and also
// retargets the editors when the user fetch-navigated with the shell open.
function onSwitchEvent(ev) {
if (ev.detail?.path != null) currentPath.value = ev.detail.path
if (ev.detail?.mode) switchMode(ev.detail.mode)
}
// Closing the shell hides it but keeps it mounted (main.js); the tabs stay
// cached in KeepAlive the whole time, so no state is ever lost until a real
// page reload. On re-show each active tab re-applies its window title and
// preview via its own pagerite:editor-shown listener. No Escape-to-close:
// it fired too easily by accident (e.g. dismissing an editor popup).
onMounted(() => {
document.body.dataset.editorMode = activeMode.value
addEventListener('pagerite:switch-editor', onSwitchEvent)
addEventListener('pagerite:editor-shown', pinPreviewLang)
addEventListener('pagerite:editor-hidden', unpinPreviewLang)
// The shell mounts visible (openEditor), so pin immediately. The primary
// language's code (for pinning it explicitly) comes from the settings.
pinPreviewLang()
fetch('/_api/settings').then((r) => r.json()).then((s) => {
primaryLang.value = s.primary_lang || 'en'
}).catch(() => { /* keep the default */ })
})
onUnmounted(() => {
removeEventListener('pagerite:switch-editor', onSwitchEvent)
removeEventListener('pagerite:editor-shown', pinPreviewLang)
removeEventListener('pagerite:editor-hidden', unpinPreviewLang)
})
</script>
<template>
<div class="editor-root overlay" lang="en" dir="ltr">
<header class="editor-tabs">
<template v-for="m in MODES" :key="m.key">
<span v-if="m.breakBefore" class="tab-break" />
<button
type="button"
class="tab"
:class="{ active: activeMode === m.key }"
@click="switchMode(m.key)"
>
{{ m.label }}
</button>
</template>
<button type="button" class="close" title="close" @click="close"></button>
</header>
<div class="editor-tab-body">
<KeepAlive>
<component
:is="MODES.find((m) => m.key === activeMode).component"
:key="activeMode"
:page-path="currentPath"
@close="close"
@path-change="onPathChange"
/>
</KeepAlive>
</div>
</div>
</template>
<style scoped>
/* Docked-overlay positioning (sticky, height, slide-in) comes from the
global pagerite.css (.editor-root.overlay); the shell is a flex column so
the tab body fills what the tab bar leaves. */
.editor-root {
display: flex;
flex-direction: column;
}
.editor-tabs {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.4rem 1rem;
border-bottom: 1px solid var(--line);
background: var(--surface);
flex-shrink: 0;
}
.editor-tabs .tab {
padding: 0.25rem 0.8rem;
font: inherit;
font-size: 0.9rem;
color: var(--muted);
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.editor-tabs .tab:hover {
color: var(--text);
}
/* Visual break between the site-wide tabs and the per-page tabs. */
.editor-tabs .tab-break {
align-self: stretch;
margin: 0.2rem 0.5rem;
border-left: 1px solid var(--line);
}
.editor-tabs .tab.active {
color: var(--text);
border-bottom-color: var(--accent);
}
.editor-tabs .close {
margin-left: auto;
padding: 0 0.3rem;
background: none;
border: none;
color: var(--muted);
font-size: 1.05rem;
cursor: pointer;
}
.editor-tabs .close:hover {
color: var(--text);
}
.editor-tab-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* The active tab component's root fills the body. */
.editor-tab-body > * {
flex: 1;
min-height: 0;
}
</style>