Merge Plugin Studio's view address and the generated pages' urlOwner (shareable pages P1)
This commit is contained in:
commit
1118219bd5
2 changed files with 51 additions and 8 deletions
|
|
@ -925,7 +925,10 @@ function mapPageHtml(opts) {
|
|||
? `\n <script>window.MAP_APP_API_URL = ${JSON.stringify(apiBaseUrl)};</script>`
|
||||
: ''
|
||||
// Bake the QA corner widget onto the host platform's embed surface (opt-out via build-config qaWidget:false).
|
||||
const pageConfig = DEST_CONFIG.qaWidget ? { ...config, qa: true } : config
|
||||
// urlOwner 'app' (library/composables/useViewAddress.js): this generated page is OURS, so its shell may write the
|
||||
// view address (?tab=…) for a reload and a sent link. Only the page's own config gets it, never an embed snippet
|
||||
// (embedCfg), so a partner's page is never written to; and a FRAMED page drops it at runtime (below).
|
||||
const pageConfig = { ...(DEST_CONFIG.qaWidget ? { ...config, qa: true } : config), urlOwner: 'app' }
|
||||
// PROVIDER-AWARE PAGE (see library/composables/useMapProvider.js): a profile or
|
||||
// instance whose config says provider:'maplibre' gets the MapLibre peer (token-
|
||||
// free, no Mapbox account) and SKIPS the token ladder entirely — the map mounts
|
||||
|
|
@ -1099,6 +1102,9 @@ ${apiScript}
|
|||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = ${JSON.stringify(pageConfig)};
|
||||
// Framed (the iframe embed on a partner's page): READ the address, never write it. A push inside a frame adds
|
||||
// entries to the partner page's own Back button (the joint session history).
|
||||
try { if (window.self !== window.top) delete BASE_CONFIG.urlOwner } catch (e) { delete BASE_CONFIG.urlOwner }
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ import StudioTree from './src/StudioTree.vue'
|
|||
import { LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
||||
import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapToken.js'
|
||||
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
|
||||
import { readViewKeys, writeViewKeys, onAddressChange, urlOwnerOf, commitUrl } from '@map/composables/useViewAddress.js'
|
||||
|
||||
// The reader's own map token / token-service URL. localStorage only: never written into the build,
|
||||
// never sent anywhere but Mapbox (the token) or the URL the reader typed (the service).
|
||||
|
|
@ -762,11 +763,20 @@ export default defineComponent({
|
|||
.catch(() => {})
|
||||
}
|
||||
this.loadManifest()
|
||||
// Back / Forward re-open the tab the address names; they never write (useViewAddress.js says why).
|
||||
this._offAddress = onAddressChange(({ tab }) => {
|
||||
const key = this.keyOfTab(tab)
|
||||
if (key) this.select(key, { fromHistory: true })
|
||||
else if (this.rows.length) this.select(this.rows[0].key, { fromHistory: true })
|
||||
})
|
||||
},
|
||||
// The corner-widget hosts live on window.__doxaStudioWidgets so a HOT UPDATE remounts Studio onto
|
||||
// the same hosts and a live widget (Rudder) keeps its state. A real unmount must release them, or
|
||||
// the hosts and every widget element in them stay pinned on the page after Studio is gone.
|
||||
beforeUnmount() {
|
||||
// FIRST, before the hot-update return below: a remounted Studio adds its own listener, and a kept one would
|
||||
// re-apply every Back twice.
|
||||
if (this._offAddress) { this._offAddress(); this._offAddress = null }
|
||||
if (import.meta.hot) return
|
||||
const W = window.__doxaStudioWidgets
|
||||
if (W && W.hosts) for (const h of Object.values(W.hosts)) { try { h.remove() } catch (_) {} }
|
||||
|
|
@ -1003,20 +1013,45 @@ export default defineComponent({
|
|||
// list" instead of a tab. It opens the first tab, remembers in localStorage the tab you were on,
|
||||
// and a reload goes back to that tab.
|
||||
//
|
||||
// 1. the URL hash — an explicit, shareable request always wins over a remembered one
|
||||
// 2. localStorage — what you were last looking at, so a reload is not a fresh start
|
||||
// 3. the first row — never leave the pane empty
|
||||
// 1. ?tab= — the view address (useViewAddress.js): a reload and a SENT link open it
|
||||
// 2. the URL hash — the old shareable form (#bundle/profile); rewritten to ?tab= on first paint
|
||||
// 3. localStorage — what you were last looking at, so a reload is not a fresh start
|
||||
// 4. the first row — never leave the pane empty
|
||||
//
|
||||
// The first select() is a REPLACE (first paint); a reader's click is a PUSH (one Back step), written only
|
||||
// where this page owns its address (urlOwner 'app', baked into our own leaf page; a partner's page and a
|
||||
// framed leaf only read).
|
||||
//
|
||||
// Called once after the manifest lands, so `rows` is populated and every candidate can be
|
||||
// checked against it. A remembered key whose bundle was renamed or removed simply fails that
|
||||
// check and falls through to the first row — a stale key must never render an empty pane, which
|
||||
// is the very state this replaces.
|
||||
route() {
|
||||
const first = { firstPaint: true }
|
||||
const asked = this.keyOfTab(readViewKeys(typeof location !== 'undefined' ? location.search : '').tab)
|
||||
if (asked) { this.select(asked, first); return }
|
||||
const h = (typeof location !== 'undefined' && location.hash || '').replace(/^#\/?/, '')
|
||||
if (h && this.rows.some(r => r.key === h)) { this.select(h); return }
|
||||
if (h && this.rows.some(r => r.key === h)) {
|
||||
this.select(h, first)
|
||||
// The old #bundle/profile link now reads as ?tab=; its hash goes, so the address has ONE form.
|
||||
if (urlOwnerOf(this.cfg) === 'app') commitUrl({ hash: '' })
|
||||
return
|
||||
}
|
||||
const remembered = this.readLastTab()
|
||||
if (remembered && this.rows.some(r => r.key === remembered)) { this.select(remembered); return }
|
||||
if (this.rows.length) this.select(this.rows[0].key)
|
||||
if (remembered && this.rows.some(r => r.key === remembered)) { this.select(remembered, first); return }
|
||||
if (this.rows.length) this.select(this.rows[0].key, first)
|
||||
},
|
||||
// ★ A ROW'S TAB ID. A row key is "<bundle>/<profile>", and the address takes plain ids only (no "/"), so the tab
|
||||
// WRITTEN is "<bundle>__<profile>": stable, since a profile name another bundle adopts later cannot make a sent
|
||||
// link mean a different row. READ: that form, or a bare profile name while it names exactly one row (a typed
|
||||
// ?tab=poster-styles), else '' (the default tab opens).
|
||||
tabIdOf(key) { return String(key || '').replace('/', '__') },
|
||||
keyOfTab(id) {
|
||||
if (!id) return ''
|
||||
const full = id.includes('__') ? id.replace('__', '/') : ''
|
||||
if (full && this.rows.some(r => r.key === full)) return full
|
||||
const byProfile = this.rows.filter(r => r.key.split('/')[1] === id)
|
||||
return byProfile.length === 1 ? byProfile[0].key : ''
|
||||
},
|
||||
// Storage is keyed per HOST (pathname), because two cap-5 shells served from one origin would
|
||||
// otherwise fight over a single key and each reload would yank the other's tab.
|
||||
|
|
@ -1034,7 +1069,7 @@ export default defineComponent({
|
|||
writeLastTab(key) {
|
||||
try { window.localStorage.setItem(this.storageKey(), key) } catch (_) { /* storage unavailable */ }
|
||||
},
|
||||
select(key) {
|
||||
select(key, { fromHistory = false, firstPaint = false } = {}) {
|
||||
// ★ BAIL BEFORE TOUCHING STATE if `key` names no real row. A composition-folder's TOP row can be
|
||||
// a SYNTHETIC head (see compositionTrees) for a composite that cannot list itself as one of its own
|
||||
// tabs (plugin-studio nesting <plugin-studio> would be infinite regress — same reasoning
|
||||
|
|
@ -1062,6 +1097,8 @@ export default defineComponent({
|
|||
// Remember it AFTER confirming the row exists, so a bad key is never written back — otherwise
|
||||
// one stray select() would poison the next reload with a key that fails route()'s check.
|
||||
this.writeLastTab(key)
|
||||
// The view address: a reader's choice is a push, the first paint a replace, Back/Forward never writes.
|
||||
if (!fromHistory) writeViewKeys({ tab: this.tabIdOf(key) }, { owner: urlOwnerOf(this.cfg), push: !firstPaint, el: this.$el })
|
||||
// A heavy row's slot only exists after the v-if subtree renders, so wait a tick before
|
||||
// mounting into it — on the first activation the ref is null at this point.
|
||||
if (!row.mounted) this.$nextTick(() => { if (this.activeKey === row.key) this.mountElement(row) })
|
||||
|
|
|
|||
Loading…
Reference in a new issue