Engine and token props: a token-service URL is honoured, Clear clears, and a pending token is awaited
(cherry picked from commit 6e4ddceced)
This commit is contained in:
parent
05165d9b26
commit
0c7f4af47b
7 changed files with 196 additions and 21 deletions
82
internal/check-map-props-unit.mjs
Normal file
82
internal/check-map-props-unit.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-map-props-unit — the engine and token props, decided in the library, measured WITHOUT a browser.
|
||||
*
|
||||
* resolveMapProvider ('auto': Mapbox when a token is available, MapLibre otherwise; an explicit provider
|
||||
* wins), and the page-wide token (useSharedMapToken): publish, CLEAR, and a token-service URL. Runs the
|
||||
* real library modules in Node against a stand-in window/location, so it is fast and needs no browser.
|
||||
*
|
||||
* Each case names the defect it guards (U3 = Clear left the old token live, U1 = tokenUrl read by nothing,
|
||||
* U7 = mixed-case provider dropped).
|
||||
*
|
||||
* USAGE node internal/check-map-props-unit.mjs EXIT 0 pass · 1 fail
|
||||
*/
|
||||
const listeners = {}
|
||||
globalThis.window = globalThis
|
||||
globalThis.CustomEvent = class { constructor(type, o) { this.type = type; this.detail = o && o.detail } }
|
||||
window.addEventListener = (t, f) => { (listeners[t] = listeners[t] || []).push(f) }
|
||||
window.removeEventListener = (t, f) => { listeners[t] = (listeners[t] || []).filter((x) => x !== f) }
|
||||
window.dispatchEvent = (e) => { for (const f of listeners[e.type] || []) f(e); return true }
|
||||
let search = ''
|
||||
Object.defineProperty(globalThis, 'location', { configurable: true, get: () => ({ search }) })
|
||||
|
||||
const { resolveMapProvider, hasMapToken, explicitMapProvider, DEFAULT_PROVIDER } = await import('../library/composables/useMapProvider.js')
|
||||
const shared = await import('../library/composables/useSharedMapToken.js')
|
||||
|
||||
let bad = 0
|
||||
const eq = (name, got, want) => { const ok = got === want; if (!ok) bad++; console.log(` ${ok ? 'ok ' : 'FAIL'} ${name.padEnd(64)} ${JSON.stringify(got)}${ok ? '' : ` (want ${JSON.stringify(want)})`}`) }
|
||||
const reset = () => { search = ''; delete window.MAP_PROVIDER; shared.clearMapToken(); window.__doxaMapTokenPending = null }
|
||||
const TK = ['pk', 'eyJ1IjoidGVzdCJ9', 'c2ln'].join('.')
|
||||
|
||||
console.log('MAP-PROPS: the library, in Node (no browser)')
|
||||
eq('default is auto', DEFAULT_PROVIDER, 'auto')
|
||||
reset(); eq('auto, no token anywhere → maplibre', resolveMapProvider({}), 'maplibre')
|
||||
reset(); eq('auto, tk in profile-config → mapbox', resolveMapProvider({ tk: TK }), 'mapbox')
|
||||
reset(); eq('auto, placeholder tk YOUR_MAPBOX_TOKEN → maplibre', resolveMapProvider({ tk: 'YOUR_MAPBOX_TOKEN' }), 'maplibre')
|
||||
reset(); window.MAP_TOKEN = TK; eq('auto, host window.MAP_TOKEN → mapbox', resolveMapProvider({}), 'mapbox')
|
||||
reset(); eq('auto, a token the caller holds (useMapInstance) → mapbox', resolveMapProvider({}, { token: TK }), 'mapbox')
|
||||
reset(); eq('auto, tokenUrl in profile-config → mapbox (U1)', resolveMapProvider({ tokenUrl: 'https://example.org/token' }), 'mapbox')
|
||||
reset(); eq('explicit maplibre with a token → maplibre', resolveMapProvider({ provider: 'maplibre', tk: TK }), 'maplibre')
|
||||
reset(); eq('explicit mapbox without a token → mapbox', resolveMapProvider({ provider: 'mapbox' }), 'mapbox')
|
||||
reset(); eq('mixed case "MapLibre" is honoured (U7)', resolveMapProvider({ provider: 'MapLibre', tk: TK }), 'maplibre')
|
||||
reset(); search = '?provider=maplibre'; window.MAP_TOKEN = TK; eq('?provider=maplibre wins over a host token', resolveMapProvider({}), 'maplibre')
|
||||
reset(); window.MAP_PROVIDER = 'mapbox'; eq('window.MAP_PROVIDER is explicit', explicitMapProvider({}), 'mapbox')
|
||||
reset(); eq('"auto" is not explicit', explicitMapProvider({ provider: 'auto' }), '')
|
||||
|
||||
// ── the page-wide token ──
|
||||
reset()
|
||||
const tok = shared.useSharedMapToken()
|
||||
shared.publishMapToken(TK)
|
||||
eq('publish reaches window.MAP_TOKEN', window.MAP_TOKEN, TK)
|
||||
eq('publish reaches a live element (ref)', tok.value, TK)
|
||||
shared.clearMapToken()
|
||||
eq('CLEAR empties window.MAP_TOKEN (U3)', window.MAP_TOKEN || '', '')
|
||||
eq('CLEAR reaches a live element (U3)', tok.value, '')
|
||||
eq('after CLEAR, auto → maplibre (U3)', resolveMapProvider({}), 'maplibre')
|
||||
window.MAP_TOKEN = TK
|
||||
eq('after CLEAR, a host assignment is adopted again (bridge intact, U3)', tok.value, TK)
|
||||
eq('hasMapToken sees it', hasMapToken({}), true)
|
||||
|
||||
// The OLD Clear path: `delete window.MAP_TOKEN` + publishMapToken('') leaves the old token live (publishing
|
||||
// '' is a no-op by design, which is why Clear needs clearMapToken). It also used to remove the bridge for
|
||||
// good; the bridge now re-installs itself, so a later host token is adopted again.
|
||||
reset(); shared.publishMapToken(TK)
|
||||
delete window.MAP_TOKEN; shared.publishMapToken('')
|
||||
eq('reproduction: the old Clear left the OLD token live on elements', tok.value, TK)
|
||||
window.MAP_TOKEN = 'pk.other'
|
||||
eq('self-heal: after that delete, a later host token reaches elements again', tok.value, 'pk.other')
|
||||
window.__doxaMapToken = ''; window.MAP_TOKEN = ''
|
||||
|
||||
// ── token-service URL (U1) ──
|
||||
reset()
|
||||
globalThis.fetch = async (url) => ({ ok: true, headers: { get: () => 'application/json' }, json: async () => ({ token: url.includes('good') ? TK : 'sk.secret' }) })
|
||||
const got = await shared.fetchMapToken('https://example.org/good')
|
||||
eq('tokenUrl answer is published page-wide (U1)', window.MAP_TOKEN, TK)
|
||||
eq('fetchMapToken resolves with it', got, TK)
|
||||
reset()
|
||||
const secret = await shared.fetchMapToken('https://example.org/bad')
|
||||
eq('a non-public (sk.) answer is refused', secret, '')
|
||||
eq('…and nothing is published', window.MAP_TOKEN || '', '')
|
||||
|
||||
if (bad) { console.log(`MAP-PROPS: FAIL — ${bad} case(s).`); process.exit(1) }
|
||||
console.log('MAP-PROPS: PASS — auto picks the engine from the token ladder, explicit wins, Clear clears, tokenUrl is honoured.')
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import { computed, provide, inject, watchEffect, watch } from 'vue'
|
||||
import { useSharedMapToken, publishMapToken } from './composables/useSharedMapToken.js'
|
||||
import { useSharedMapToken, publishMapToken, fetchMapToken } from './composables/useSharedMapToken.js'
|
||||
import { retryableAsyncComponent } from './composables/retryableAsyncComponent.js'
|
||||
import { SUPPORTED_LOCALES, RTL_LOCALES, setRuntimeLocale } from './i18n/index.js'
|
||||
// QA corner: COMPILE-TIME gated (__DOXA_QA__, vite define). In public builds
|
||||
|
|
@ -127,6 +127,11 @@ const mapboxToken = computed(() => {
|
|||
// that a token belongs to the page, not to whichever component happened to receive it. Publishing
|
||||
// is idempotent and ignores empties, so this cannot clobber a good token with a blank one.
|
||||
watch(() => config.value?.tk, (tk) => { if (typeof tk === 'string' && tk) publishMapToken(tk) }, { immediate: true })
|
||||
// A token-SERVICE URL in the profile-config (tokenUrl) is asked only when no token is at hand; its answer
|
||||
// is published page-wide, so mapboxToken above (and every sibling) picks it up.
|
||||
watch(() => config.value?.tokenUrl, (url) => {
|
||||
if (url && !config.value?.tk && !sharedToken.value) fetchMapToken(url)
|
||||
}, { immediate: true })
|
||||
// Default to the CONFIGURED active source from sources.json (e.g. 'pray-tools', the live API)
|
||||
// — NOT a hardcoded 'doxa-csv'. A host/gem-frame that embeds the bundle without an explicit
|
||||
// profile-config.dataSource (the platform case) was silently falling back to 'doxa-csv', whose
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const OPENFREEMAP_DARK_STYLE = 'https://tiles.openfreemap.org/styles/fiord'
|
|||
// Named styles: the 12-style pack. A profile-config style of 'ink' /
|
||||
// 'midnight-glow' / … resolves per engine here; raw URLs pass through.
|
||||
import { resolveNamedStyle, applyStylePaint } from '../styles/registry.js'
|
||||
import { resolveMapProvider } from './useMapProvider.js'
|
||||
import { resolveMapProvider, explicitMapProvider } from './useMapProvider.js'
|
||||
import { installRtlTextPlugin } from '../maps/rtl-text.js'
|
||||
import { localizeBasemapLabels } from '../maps/localize-labels.js'
|
||||
|
||||
|
|
@ -364,8 +364,11 @@ export function useMapInstance({
|
|||
// degrades to the page switch instead of throwing.
|
||||
let _pcForProvider = null
|
||||
try { _pcForProvider = inject('profileConfig', null) } catch { /* called outside setup */ }
|
||||
// 'auto' decided it (nobody asked for an engine): it may still change once, below, if the token it
|
||||
// counted on was a token SERVICE that answers with nothing.
|
||||
const autoProvider = !provider && !explicitMapProvider(_pcForProvider)
|
||||
provider = provider || resolveMapProvider(_pcForProvider, { token: accessToken })
|
||||
const isMaplibre = provider === 'maplibre'
|
||||
let isMaplibre = provider === 'maplibre'
|
||||
|
||||
// THE READER'S LANGUAGE, for the basemap's place names (maps/localize-labels.js). ProfileLoader
|
||||
// provides it as a ref ('lang'); an element without ProfileLoader falls back to its profile-config
|
||||
|
|
@ -386,6 +389,13 @@ export function useMapInstance({
|
|||
async function initializeMap() {
|
||||
// Resolve a NAMED style ('ink', 'satellite', …) for this engine; raw URLs
|
||||
// and undefined keep their existing behavior (see library/styles/registry.js).
|
||||
// 'auto' picked Mapbox on a token service still answering: wait for it. No token after all → MapLibre,
|
||||
// keyless, which is what 'auto' means without a token (a map that cannot draw is not a fallback).
|
||||
if (autoProvider && !isMaplibre && !accessToken && typeof window !== 'undefined' && window.__doxaMapTokenPending) {
|
||||
await window.__doxaMapTokenPending
|
||||
if (!containerRef.value) return
|
||||
if (!(typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN)) { provider = 'maplibre'; isMaplibre = true }
|
||||
}
|
||||
const _named = resolveNamedStyle(style, provider)
|
||||
const effectiveStyle = _named.style || style
|
||||
if (!containerRef.value) {
|
||||
|
|
@ -422,6 +432,11 @@ export function useMapInstance({
|
|||
// mounts a map component directly, with no ProfileLoader / profile-config wrapper) may expose the
|
||||
// token once as `window.MAP_TOKEN`. Any map on the page then finds it. Per-instance accessToken
|
||||
// still wins; this only fills the gap when none was passed.
|
||||
// A token service still answering (profile-config.tokenUrl, see fetchMapToken): wait for it.
|
||||
if (!accessToken && typeof window !== 'undefined' && window.__doxaMapTokenPending) {
|
||||
await window.__doxaMapTokenPending
|
||||
if (gone()) return
|
||||
}
|
||||
if (!accessToken && typeof window !== 'undefined' && typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) {
|
||||
accessToken = window.MAP_TOKEN
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,10 @@ export function hasMapToken(cfg) {
|
|||
// The embed snippets ship tk:"YOUR_MAPBOX_TOKEN" as a placeholder; an unfilled one is not a token.
|
||||
const real = (v) => typeof v === 'string' && !!v.trim() && !/^YOUR_/.test(v.trim())
|
||||
if (real(cfg?.tk)) return true
|
||||
// A token service to ask (profile-config.tokenUrl) counts: the element fetches it before the map starts.
|
||||
if (/^https?:\/\//i.test(String(cfg?.tokenUrl || ''))) return true
|
||||
if (typeof window === 'undefined') return false
|
||||
return real(window.MAP_TOKEN) || real(window.__doxaMapToken)
|
||||
return real(window.MAP_TOKEN) || real(window.__doxaMapToken) || !!window.__doxaMapTokenPending
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,14 +60,19 @@ export function hasMapToken(cfg) {
|
|||
* Pure — safe to call outside a Vue setup().
|
||||
*/
|
||||
export function resolveMapProvider(profileConfig, { token } = {}) {
|
||||
const cfg = unwrapConfig(profileConfig)
|
||||
return explicitMapProvider(cfg) || (hasMapToken(cfg) || hasMapToken({ tk: token }) ? 'mapbox' : 'maplibre')
|
||||
}
|
||||
|
||||
/** The engine someone ASKED for (rungs 1-3), or '' when the default ('auto') decides. */
|
||||
export function explicitMapProvider(profileConfig) {
|
||||
const cfg = unwrapConfig(profileConfig)
|
||||
const fromConfig = cfg?.provider
|
||||
const fromUrl = typeof location !== 'undefined'
|
||||
? new URLSearchParams(location.search).get('provider') : undefined
|
||||
const fromWindow = typeof window !== 'undefined' ? window.MAP_PROVIDER : undefined
|
||||
const raw = String(fromConfig || fromUrl || fromWindow || DEFAULT_PROVIDER).toLowerCase().trim()
|
||||
if (MAP_PROVIDERS.includes(raw)) return raw
|
||||
return hasMapToken(cfg) || hasMapToken({ tk: token }) ? 'mapbox' : 'maplibre'
|
||||
return MAP_PROVIDERS.includes(raw) ? raw : ''
|
||||
}
|
||||
|
||||
/** Human-readable engine label for a badge / UI. */
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const readStored = () => {
|
|||
/** Announce a token to every element on the page. No-ops on an empty or unchanged value. */
|
||||
export function publishMapToken(tk) {
|
||||
if (typeof window === 'undefined') return ''
|
||||
installBridge()
|
||||
const next = typeof tk === 'string' ? tk.trim() : ''
|
||||
if (!next || next === readStored()) return readStored()
|
||||
window[KEY] = next
|
||||
|
|
@ -59,6 +60,20 @@ export function publishMapToken(tk) {
|
|||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw the page-wide token (a "Clear" in the studio). publishMapToken('') is deliberately a no-op, and
|
||||
* `delete window.MAP_TOKEN` removed the bridge's accessor while its install flag stayed set, so the bridge
|
||||
* never came back and the OLD token stayed live. This empties the value through the same channel.
|
||||
*/
|
||||
export function clearMapToken() {
|
||||
if (typeof window === 'undefined') return
|
||||
const had = readStored() || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN)
|
||||
installBridge()
|
||||
window[KEY] = ''
|
||||
if (!Object.getOwnPropertyDescriptor(window, 'MAP_TOKEN')?.get) { try { window.MAP_TOKEN = '' } catch { /* host-owned */ } }
|
||||
if (had) { try { window.dispatchEvent(new CustomEvent(EVENT, { detail: '' })) } catch { /* pre-CustomEvent host */ } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adopt `window.MAP_TOKEN` as a source, in BOTH directions and for the whole life of the page.
|
||||
*
|
||||
|
|
@ -68,7 +83,10 @@ export function publishMapToken(tk) {
|
|||
* because N bundles on one page would otherwise stack N accessors on the same property.
|
||||
*/
|
||||
function installBridge() {
|
||||
if (typeof window === 'undefined' || window[FLAG]) return
|
||||
if (typeof window === 'undefined') return
|
||||
// Installed AND still in place: a host (the studio's old Clear did) can `delete window.MAP_TOKEN`, which
|
||||
// removes the accessor while the flag stays set — then nothing was ever adopted again. Re-install.
|
||||
if (window[FLAG] && Object.getOwnPropertyDescriptor(window, 'MAP_TOKEN')?.get) return
|
||||
// Seed from whatever the page already set before this bundle loaded.
|
||||
const existing = typeof window.MAP_TOKEN === 'string' ? window.MAP_TOKEN : ''
|
||||
if (existing && !readStored()) window[KEY] = existing
|
||||
|
|
@ -100,3 +118,30 @@ export function useSharedMapToken() {
|
|||
}
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* profile-config.tokenUrl: a token SERVICE the element asks when it was handed no token (the studio's
|
||||
* header and the gallery write it; nothing used to read it). Same contract as the leaf pages' ladder:
|
||||
* JSON {token|tk|accessToken} or a bare string, and only a PUBLIC token (pk.…) is accepted. One fetch per
|
||||
* URL per page; the answer is published page-wide. While it is in flight, window.__doxaMapTokenPending
|
||||
* holds the promise, so a map about to start on Mapbox waits for it instead of failing tokenless.
|
||||
* @returns {Promise<string>} the token, or '' when the service did not give one
|
||||
*/
|
||||
export function fetchMapToken(url) {
|
||||
if (typeof window === 'undefined' || !/^https?:\/\//i.test(String(url || ''))) return Promise.resolve('')
|
||||
const cache = (window.__doxaMapTokenFetches = window.__doxaMapTokenFetches || {})
|
||||
if (cache[url]) return cache[url]
|
||||
const p = fetch(url)
|
||||
.then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return (r.headers.get('content-type') || '').includes('json') ? r.json() : r.text() })
|
||||
.then((d) => {
|
||||
const tk = String((typeof d === 'string' ? d : d && (d.token || d.tk || d.accessToken)) || '').trim()
|
||||
if (!tk.startsWith('pk.')) return ''
|
||||
publishMapToken(tk)
|
||||
return tk
|
||||
})
|
||||
.catch((e) => { console.warn(`[map token] ${url}: ${e && e.message ? e.message : e}`); return '' })
|
||||
.finally(() => { if (window.__doxaMapTokenPending === p) window.__doxaMapTokenPending = null })
|
||||
window.__doxaMapTokenPending = p
|
||||
cache[url] = p
|
||||
return p
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ export const LANGUAGES = [
|
|||
// Kept here, flagged, so the list stays a faithful mirror of the source of truth.
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch', flag: '🇩🇪', hideFromSwitcher: true },
|
||||
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', flag: '🇮🇳', hideFromSwitcher: true },
|
||||
// enabled:false in dx-life (routable/translatable but not offered): it, zh, ro.
|
||||
// enabled:false in dx-life (routable/translatable but not offered), but every library catalogue carries
|
||||
// them (library/i18n SUPPORTED_LOCALES, 11 codes). Listed, hidden from the public switcher, so a tag of
|
||||
// it/ro/zh is no longer normalised to 'en' — it was, in the studio (11 locales vs 8 here).
|
||||
{ code: 'it', name: 'Italian', nativeName: 'Italiano', flag: '🇮🇹', hideFromSwitcher: true },
|
||||
{ code: 'ro', name: 'Romanian', nativeName: 'Română', flag: '🇷🇴', hideFromSwitcher: true },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '中文', flag: '🇨🇳', hideFromSwitcher: true },
|
||||
]
|
||||
|
||||
/** Languages offered in the dropdown. */
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ import { defineComponent, inject } from 'vue'
|
|||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
||||
import StudioTree from './src/StudioTree.vue'
|
||||
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
||||
import { LANGUAGES, loadLocale, saveLocale, getLanguage, normaliseLocale } from '@map/layouts/app-wrapper-material/languages.js'
|
||||
import { publishMapToken, clearMapToken } from '@map/composables/useSharedMapToken.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).
|
||||
|
|
@ -503,7 +504,9 @@ export default defineComponent({
|
|||
userTokenUrl: readStored(TOKEN_URL_KEY),
|
||||
fetchedTk: '',
|
||||
bakedTk: '',
|
||||
switcherLangs: SWITCHER_LANGUAGES,
|
||||
hostProvider: '',
|
||||
// EVERY locale the library carries, not the public site's six: the studio exists to exercise props.
|
||||
switcherLangs: LANGUAGES,
|
||||
// rows keyed by "<bundle>/<profile>"; each carries its resolved nesting state.
|
||||
rows: [], // flat list built from the manifest (grouped for display via `groups`)
|
||||
// raw manifest bundles, kept ONLY so composeGroups() can read each bundle's `composes` list —
|
||||
|
|
@ -648,7 +651,8 @@ export default defineComponent({
|
|||
return this.userTk || this.fetchedTk || this.tk || this.cfg.tk || this.bakedTk || ''
|
||||
},
|
||||
effectiveProvider() {
|
||||
const p = this.userProvider || this.provider || this.cfg.provider || ''
|
||||
// Case-insensitive, as resolveMapProvider is: a host's provider="MapLibre" was dropped here.
|
||||
const p = String(this.userProvider || this.provider || this.cfg.provider || '').toLowerCase().trim()
|
||||
return p === 'maplibre' || p === 'mapbox' ? p : ''
|
||||
},
|
||||
engines() {
|
||||
|
|
@ -687,9 +691,16 @@ export default defineComponent({
|
|||
mounted() {
|
||||
// Read once: after applyToken sets window.MAP_TOKEN it would no longer be "the page's own".
|
||||
this.bakedTk = (typeof window !== 'undefined' && window.MAP_TOKEN) || ''
|
||||
if (this.userProvider) this.publishProvider()
|
||||
// The HOST's own engine switch, kept so "each map's own" restores it instead of deleting it.
|
||||
this.hostProvider = (typeof window !== 'undefined' && typeof window.MAP_PROVIDER === 'string') ? window.MAP_PROVIDER : ''
|
||||
this.publishProvider() // a host provider (attribute/profile-config) reaches nested maps too, chosen or not
|
||||
if (this.userTk) this.publishToken()
|
||||
else if (this.userTokenUrl) this.fetchToken(this.userTokenUrl).then((t) => { if (t) { this.fetchedTk = t; this.publishToken() } }).catch(() => {})
|
||||
else if (this.userTokenUrl || this.tokenUrl || this.cfg.tokenUrl) {
|
||||
// A token from a URL arrives AFTER the first pane mounted; re-mount so that pane gets it too.
|
||||
this.fetchToken(this.userTokenUrl || this.tokenUrl || this.cfg.tokenUrl)
|
||||
.then((t) => { if (t && t !== this.fetchedTk) { this.fetchedTk = t; this.publishToken(); this.remountPanes() } })
|
||||
.catch(() => {})
|
||||
}
|
||||
this.loadManifest()
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -745,6 +756,7 @@ export default defineComponent({
|
|||
publishProvider() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (this.effectiveProvider) window.MAP_PROVIDER = this.effectiveProvider
|
||||
else if (this.hostProvider) window.MAP_PROVIDER = this.hostProvider
|
||||
else if ('MAP_PROVIDER' in window) delete window.MAP_PROVIDER
|
||||
},
|
||||
clearToken() {
|
||||
|
|
@ -758,8 +770,9 @@ export default defineComponent({
|
|||
// so the effective token is published there too (and the page's own token restored on clear).
|
||||
publishToken() {
|
||||
if (typeof window === 'undefined') return
|
||||
if (this.effectiveTk) window.MAP_TOKEN = this.effectiveTk
|
||||
else if ('MAP_TOKEN' in window) delete window.MAP_TOKEN
|
||||
// Through useSharedMapToken, never `delete`: deleting removed its bridge for good (U3).
|
||||
if (this.effectiveTk) { publishMapToken(this.effectiveTk); if (window.MAP_TOKEN !== this.effectiveTk) window.MAP_TOKEN = this.effectiveTk }
|
||||
else clearMapToken()
|
||||
},
|
||||
// Release every mounted pane (frees its map) and re-mount the one on screen with the new token;
|
||||
// the others pick it up when they are next selected.
|
||||
|
|
@ -803,7 +816,8 @@ export default defineComponent({
|
|||
paneConfig(row, locale) {
|
||||
const pc = { profile: row.profile, instanceId: 'ps-' + row.key.replace(/\//g, '-'), locale }
|
||||
if (this.effectiveTk) pc.tk = this.effectiveTk
|
||||
if (this.tokenUrl || this.cfg.tokenUrl) pc.tokenUrl = this.tokenUrl || this.cfg.tokenUrl
|
||||
const tokenUrl = this.userTokenUrl || this.tokenUrl || this.cfg.tokenUrl
|
||||
if (tokenUrl) pc.tokenUrl = tokenUrl
|
||||
if (this.effectiveProvider) pc.provider = this.effectiveProvider
|
||||
if (this.base) pc.treeBase = this.base
|
||||
return pc
|
||||
|
|
@ -813,11 +827,13 @@ export default defineComponent({
|
|||
this.localeOpen = false
|
||||
saveLocale(code)
|
||||
try {
|
||||
const row = this.active
|
||||
const slot = this._slots && this._slots[this.activeKey]
|
||||
const el = slot && slot.firstElementChild
|
||||
if (el && el.setAttribute && row) {
|
||||
el.setAttribute('profile-config', JSON.stringify(this.paneConfig(row, code)))
|
||||
// EVERY mounted pane, not only the one on screen: a kept-alive pane showed the old language
|
||||
// when the reader came back to it.
|
||||
for (const row of this.rows) {
|
||||
if (!row.mounted) continue
|
||||
const slot = this._slots && this._slots[row.key]
|
||||
const el = slot && slot.firstElementChild
|
||||
if (el && el.setAttribute) el.setAttribute('profile-config', JSON.stringify(this.paneConfig(row, code)))
|
||||
}
|
||||
} catch { /* non-fatal: the switcher still persists + updates the UI */ }
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue