From 8b34ed86dca5cf7c9f0c7c2a783301b17177056e Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 07:53:36 +0000 Subject: [PATCH] Research Gallery nested in an app that owns the address: its tab is that app's subtab segment (shareable pages P2b) Not the owner: a rail pick announces the tab with this shell's depth (claimView), and an outer owner such as Plugin Studio records it as its subtab segment; on a partner's page nobody listens and nothing is written. Once the tab is settled after the manifest (never at mount, so a link's Experimental tab is honoured), the shell names it once with a replace, so a deeper shell's claim always has this segment to sit under. On load it reads its own subtab segment when it names one of its tabs, then tab when it does (a partner's page), else its default. Back follows the segment only once the shell knows an owner records it. Children are told their depth: 1 under an owner, else this shell's depth + 1. As the owner (its own leaf, DX Life) the code path is unchanged. --- .../doxa-maps-spa/doxa-maps-spa.vue | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue index 2632b4426..ba23db109 100644 --- a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue +++ b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue @@ -216,7 +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, ownAddress } from '@map/composables/useViewAddress.js' +import { readViewKeys, onAddressChange, urlOwnerOf, ownAddress, claimView, subtabAt, depthOf } 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. @@ -1135,7 +1135,11 @@ export default defineComponent({ // 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) }) + this._offAddress = onAddressChange((k) => { + if (urlOwnerOf(this.cfg)) this.openFromAddress(k.tab) + // nested in an app that records this shell's tab as its `subtab` (Plugin Studio): Back re-opens that one + else if (this._underOwner) this.openFromAddress(subtabAt(location.search, depthOf(this.cfg))) + }) // A nested composite's choice (its open pane, a selected place) reaches the address through this shell, the // page's one writer, while the page owns its address; a nested element can never switch this shell's tab. this._offOwn = ownAddress(this.$el, () => urlOwnerOf(this.cfg)) @@ -1454,13 +1458,27 @@ export default defineComponent({ if (w >= 0 && !this.isDesktopOnly(want) && !this._honouredTab) this.select(w) else this.select(i >= 0 && !this.isDesktopOnly(activeId) ? i : this.firstVisibleIndex()) this._honouredTab = true + // NESTED (not the owner): name the settled tab in the owner's address once, as a replace, so a deeper shell's + // claim always has this shell's segment to sit under. Here, not at mount: a link's Experimental tab is only + // honoured once the manifest is in, and a claim at mount would overwrite it with the default. + if (!urlOwnerOf(this.cfg)) this.claimNested(false) }, // 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 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 = readViewKeys(location.search).tab } catch { /* non-browser */ } + // NOT the owner: inside an app that owns the address (Plugin Studio) this shell's tab is that app's `subtab`, at + // this shell's depth (profile-config addressDepth); `tab` is the outer app's own (a Studio row). So the subtab + // segment first when it names one of these tabs, then `tab` when it does (a partner's page, one level), else + // the default. + try { + const k = readViewKeys(location.search), sub = subtabAt(location.search, depthOf(this.cfg)) + const mine = (id) => !!id && this.tabs.some(t => t.id === id) + if (urlOwnerOf(this.cfg)) q = k.tab + else if (mine(sub)) { q = sub; this._underOwner = true } + else q = mine(k.tab) ? k.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, @@ -1523,7 +1541,19 @@ export default defineComponent({ 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 }) + if (this.activeKey === before) return + // The owner writes ?tab=. Not the owner: the pick is announced (claimNested). + const owner = urlOwnerOf(this.cfg) + if (owner) claimView({ tab: this.activeKey }, { owner, el: this.$el, push: true }) + else this.claimNested(true) + }, + // Not the owner: an outer owner (Plugin Studio) records this shell's tab as its `subtab` segment at this shell's + // depth; on a partner's page nobody listens and nothing is written. Seeing it recorded is how this shell learns + // that Back should follow the address. + claimNested(push) { + const depth = depthOf(this.cfg) + claimView({ tab: this.activeKey }, { owner: '', el: this.$el, push, depth }) + try { if (subtabAt(location.search, depth) === this.activeKey) this._underOwner = true } catch { /* no window */ } }, // 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 @@ -1641,6 +1671,8 @@ export default defineComponent({ // #anchor and retitled the host's browser tab "Regions". A child that syncs its state into the // URL standalone should read this and stand down; children that never write the URL ignore it. pc.urlSync = false + // ...and how deep they sit below the address owner: a shell nested here records its tab as `subtab` at that depth + pc.addressDepth = urlOwnerOf(this.cfg) ? 1 : depthOf(this.cfg) + 1 // The MAP's own name for its print header (mapPrintTemplate.js): this tab's localized label. A plugin that // does not print ignores it. pc.mapTitle = this.tabLabel(t)