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.
This commit is contained in:
2026-09-03 01:47:24 +00:00
parent 43793ef9a4
commit 2a430247e0
13 changed files with 411 additions and 223 deletions
+32 -1
View File
@@ -8,6 +8,8 @@ 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: '' },
@@ -18,6 +20,25 @@ 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 = [
@@ -62,15 +83,25 @@ function onSwitchEvent(ev) {
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">
<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" />