- 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.
206 lines
6.0 KiB
Vue
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>
|