doxa-maps/library/utils/languageRequest.js
Doxa Maps 25982d717f Shell header: a phone bar and a rail-bottom language slot for header-off shells, and a shared language menu that asks the host first
VerticalTabsLayout, header off: on a phone a 44px bar (a real grid row) replaces the floating corner
circle, with the rail toggle at the inline start and a #phone-end slot at the end; on a desktop a
#rail-end slot sits at the bottom of the rail (it receives `collapsed`). Neither renders with a header
on, so header-on consumers and desktop layouts without a filler are unchanged. The --map-fab-* footprint
is no longer published; its readers fall back to their plain insets.

ShellLanguageMenu fills either slot: the switcher's languages plus the current one, a native select
under a globe face. A pick dispatches one cancelable, composed, bubbling doxa-language-request on the
host element (library/utils/languageRequest.js). A host that cancels keeps the language; otherwise the
menu emits 'switch' and writes ?lang= only on a page the tree generated.

check:shell-header (gates:pre, Vite SSR) asserts where the bar and the rail slot render, the event's
shape and count, cancel and no-cancel, the ?lang= rule and the language list; its self-test (gates:post)
refuses 11 planted defects. check:nav-reachable now presses the bar's toggle.
2026-09-30 23:28:52 +00:00

58 lines
2.9 KiB
JavaScript

/**
* languageRequest.js — how a shell asks its HOST PAGE before it changes language.
*
* A shell (the Research Gallery, Posterverse) shows its own language menu, but the page around it may
* own the language: DX Life keeps it in the URL path (/ar/deep-research), and a shell that switched on
* its own would fight the page. So a pick is a REQUEST, sent as one native event on the shell's host
* element:
*
* doxa-language-request detail: { language: 'ar' } bubbles, composed, cancelable
*
* - A host that owns the language calls preventDefault() and changes it itself (it then sets the
* shell's `language` attribute). dispatchEvent returns false, and the shell does NOT switch.
* - Nobody cancels (a standalone leaf, or a partner page that set `language="es"` once and never
* listens): dispatchEvent returns true, and the shell switches itself. That is why the rule keys on
* the cancel and not on whether `language` was passed: a static attribute would otherwise leave a
* dropdown that does nothing.
*
* ★ A NATIVE CustomEvent, NOT Vue's emit. Vue's custom-element emit wraps detail in an ARRAY
* (detail[0]) and is never cancelable, so the host could neither read detail.language nor say no.
*
* The host is found the way library/utils/hostElement.js finds it (one hop out of the shadow root);
* that helper takes a Vue instance, this takes the DOM node the pick happened on.
*/
export const LANGUAGE_REQUEST_EVENT = 'doxa-language-request'
/** The custom element a node is rendered inside, or the node itself outside any shadow root. */
export function hostOf(el) {
try { const rn = el && el.getRootNode && el.getRootNode(); if (rn && rn.host) return rn.host } catch (_) {}
return el || null
}
/**
* Ask the host page for `code`. Returns true when NOBODY cancelled (the shell should switch itself),
* false when a host took the request (the shell waits for the host to set `language`).
*/
export function requestLanguage(fromEl, code) {
const host = hostOf(fromEl)
if (!host || typeof host.dispatchEvent !== 'function') return true
return host.dispatchEvent(new CustomEvent(LANGUAGE_REQUEST_EVENT, {
detail: { language: code }, bubbles: true, composed: true, cancelable: true,
}))
}
/**
* Record an uncancelled pick in ?lang=, ONLY on a page this tree generated (window.MAP_OWNS_PAGE ===
* true, stamped by internal/generate-tree.mjs and honoured by library/i18n). A host's URL is the host's:
* a shell embedded anywhere else never writes it. Returns whether it wrote.
*/
export function writeOwnedLangParam(code, win = (typeof window !== 'undefined' ? window : null)) {
try {
if (!win || win.MAP_OWNS_PAGE !== true || !win.history || !win.location) return false
const url = new URL(win.location.href)
url.searchParams.set('lang', code)
win.history.replaceState(win.history.state, '', url.toString())
return true
} catch (_) { return false }
}