Merge the Research Gallery shell's view address (shareable pages P1)

This commit is contained in:
Doxa Maps 2026-10-03 05:27:33 +00:00
commit dfed65c8af

View file

@ -67,7 +67,7 @@
:header="showHeader"
:groups="navGroups"
:active="activeKey"
@update:active="onSelect">
@update:active="onRailSelect">
<!-- #brand: the DX-Maps logo mark, left of the title (a compass-star glyph, self-drawn SVG — no
external asset, offline/F-Droid safe; the accent green matches the admin shell). -->
@ -216,6 +216,7 @@ import { helpFor } from './src/experimentalHelp.js'
import { TAB_INFO } from './src/tabInfo.js'
import MapInfo from '@map/components/info/MapInfo.vue'
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf } from '@map/composables/useViewAddress.js'
// THE HOST PAGE'S LANGUAGE, by the same ladder every nested map already uses (library/i18n
// detectLocale): ?lang= / ?locale= on the page URL, then <html lang>. '' when the page says nothing.
@ -1130,6 +1131,11 @@ export default defineComponent({
// A map's own "?" asks for its help with a composed doxa:help event (library HelpButton).
this._onHostedHelp = (e) => this.onHostedHelp(e)
this.$el.addEventListener('doxa:help', this._onHostedHelp)
// Back / Forward re-open the tab the address names (useViewAddress), and never write. Only where a page OWNS
// its address: on a partner's page a popstate is the partner's own navigation, and re-reading the URL there
// would snap a host's fixed `tab` attribute back to the default. The owner is read per event: a host may set
// profile-config after mount.
this._offAddress = onAddressChange(({ tab }) => { if (urlOwnerOf(this.cfg)) this.openFromAddress(tab) })
},
// Release the media-query listener. This element is a custom element a host can add and REMOVE at
// will (a cap-5 SPA is mounted and unmounted by shells, tabs and the studio), and a live listener
@ -1137,6 +1143,7 @@ export default defineComponent({
beforeUnmount() {
if (this._langObs) { this._langObs.disconnect(); this._langObs = null }
if (this._onHostedHelp) { this.$el.removeEventListener('doxa:help', this._onHostedHelp); this._onHostedHelp = null }
if (this._offAddress) { this._offAddress(); this._offAddress = null }
if (!this._mql || !this._onMedia) return
if (this._mql.removeEventListener) this._mql.removeEventListener('change', this._onMedia)
else if (this._mql.removeListener) this._mql.removeListener(this._onMedia)
@ -1440,10 +1447,11 @@ export default defineComponent({
this._honouredTab = true
},
// The tab a HOST asked to open first: the `tab` attribute, then profile-config.tab, then ?tab= on
// the page URL (so an iframe src or a shared link can deep-link). '' when nobody asked.
// the page URL (so an iframe src or a shared link can deep-link). '' when nobody asked. The URL is read
// through useViewAddress: the first ?tab= wins, and a value that is not a plain id reads as absent.
requestedTab() {
let q = ''
try { q = new URLSearchParams(location.search).get('tab') || '' } catch { /* non-browser */ }
try { q = readViewKeys(location.search).tab } catch { /* non-browser */ }
return String(this.tab || this.cfg.tab || q || '').trim()
},
// The index to open on mount: the requested tab when it exists and is enabled at this width,
@ -1496,6 +1504,26 @@ export default defineComponent({
const i = this.tabs.findIndex(t => t.id === key)
if (i !== -1) this.select(i)
},
// ★ THE PAGE ADDRESS (shareable pages, useViewAddress). A READER's pick in the rail is the one switch that
// writes ?tab=, as a push, so a reload keeps the tab, a sent link opens it, and Back returns to the previous one.
// Who writes is the page's urlOwner: 'app' (our own leaf page) writes history, 'host' (DX Life) gets a
// dxm-address event and its router writes, and a partner's page is never written. Every other switch is not a
// choice and writes nothing: the manifest's re-select, the device fallback, a host's `tab` attribute, Back and
// Forward. Nor is there a first-paint write: a bare URL means the device's default tab, and pinning a phone's
// default into the address would send a desktop reader to it.
onRailSelect(key) {
const before = this.activeKey
this.onSelect(key)
if (this.activeKey !== before) writeViewKeys({ tab: this.activeKey }, { owner: urlOwnerOf(this.cfg), push: true, el: this.$el })
},
// Back / Forward: the tab the address names, else the page's configured first tab (profile-config.tab), else the
// device default. Not the `tab` attribute: a host with a router re-points it from this same popstate, and until
// it does, it still names the tab being left.
openFromAddress(tab) {
const want = tab || String(this.cfg.tab || '').trim()
const i = want ? this.tabs.findIndex(t => t.id === want) : -1
this.select(i >= 0 && !this.isDesktopOnly(want) ? i : this.firstVisibleIndex())
},
// Resolve a tree-relative ref ("./x" / "x") against the tree base. If base is '' we keep it
// relative to THIS document — but this bundle's own folder is <base>/doxa-maps-spa/, so a
// co-located leaf at <base>/big-picture/... is one level UP: prefix '../'.