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)