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.
58 lines
2.9 KiB
JavaScript
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 }
|
|
}
|