From f0c97577e3e866306705f6cd9d58f5b3ec86551f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 05:25:14 +0000 Subject: [PATCH] Research Gallery: the tab is in the page address (shareable pages P1) A reader's pick in the rail writes ?tab= through useViewAddress as a push, so a reload keeps the tab, a sent link opens it, and Back returns to the previous tab. Only the address owner writes: urlOwner 'app' writes history, 'host' gets a dxm-address event for its router, and a partner's page is never written. Programmatic switches (the manifest's re-select, the device fallback, a host's tab attribute, Back/Forward) write nothing, and there is no first-paint write. Back/Forward re-open the tab the address names on a page that owns its address. ?tab= is read through readViewKeys. --- .../doxa-maps-spa/doxa-maps-spa.vue | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue index 80f87c9f6..27afbf32a 100644 --- a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue +++ b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue @@ -67,7 +67,7 @@ :header="showHeader" :groups="navGroups" :active="activeKey" - @update:active="onSelect"> + @update:active="onRailSelect"> @@ -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 . '' 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 /doxa-maps-spa/, so a // co-located leaf at /big-picture/... is one level UP: prefix '../'.