diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue index 75d080f30..f4e8c1fb1 100644 --- a/library/components/info/MapInfo.vue +++ b/library/components/info/MapInfo.vue @@ -81,6 +81,13 @@ const props = defineProps({ * actually on screen (see "control mode" below). */ trigger: { type: String, default: 'floating', validator: (v) => ['floating', 'control'].includes(v) }, + /** + * With trigger 'control': the popup a MAP mounts for its own control when no host answers it (an + * embed, a standalone page). It never draws its own button, since the map's control is the only + * button; it answers that control from inside the map, points at it once per page load, and opens + * the same popup. It announces itself as local, so the control can tell it from a real host. + */ + popupOnly: { type: Boolean, default: false }, }) // ★ THE PAGE-LOAD POINTER (control mode). The pointer beside the map's own ⓘ shows ONCE PER PAGE LOAD: // the first time that control is on screen in the load. It goes for the rest of the load once the reader @@ -292,7 +299,7 @@ onMounted(() => { timer = window.setInterval(measureControl, 1500) window.addEventListener('resize', onResize) // A control that mounted before we did asked into the void; tell it to ask again. - try { window.dispatchEvent(new CustomEvent('dxmaps:mapinfo-host')) } catch { /* no CustomEvent */ } + try { window.dispatchEvent(new CustomEvent('dxmaps:mapinfo-host', { detail: { local: props.popupOnly } })) } catch { /* no CustomEvent */ } }) onBeforeUnmount(() => { if (pane) { @@ -305,7 +312,7 @@ onBeforeUnmount(() => { window.removeEventListener('resize', onResize) }) watch(open, (v) => { control.value?.setAttribute('aria-expanded', String(v)) }) -const floatingShown = () => props.trigger !== 'control' || (!awaitingControl.value && !controlShown.value) +const floatingShown = () => !props.popupOnly && (props.trigger !== 'control' || (!awaitingControl.value && !controlShown.value)) // The pointer beside the control has been on screen: this page load has pointed once, so a remount (the // gallery rebuilds a heavy tab on every switch back) does not point again. Marked on SCREEN, not on mount: // a tab's MapInfo can mount inside a hidden pane. diff --git a/library/components/map-controls/MapInfoButton.vue b/library/components/map-controls/MapInfoButton.vue index 8f27e81d3..7fcf9d2e5 100644 --- a/library/components/map-controls/MapInfoButton.vue +++ b/library/components/map-controls/MapInfoButton.vue @@ -27,8 +27,13 @@ import MapControlButton from './MapControlButton.vue' const { t } = useI18n() -defineProps({ +const props = defineProps({ isDark: { type: Boolean, default: false }, + // The map carries its own info, so the control may stand ALONE: when no host answers (an embed on a + // partner page, a standalone preset page), it asks its map to mount a popup of its own (the + // 'standalone' event) instead of rendering nothing. Never inside the gallery: there the gallery alone + // decides, its own info:false included. Off by default, so every other consumer is unchanged. + standalone: { type: Boolean, default: false }, // 'round' (default) is the map-control form. 'rounded-square' is the dashboard HEADER form (the ⓘ // sits in a header bar beside Share, not in a map's control column) — forwarded to MapControlButton. shape: { type: String, default: 'round' }, @@ -40,6 +45,7 @@ defineProps({ // QUESTION is asked from the toolbar's own element (provided by MapToolbar as mapToolbar.el), or from this // component's placeholder's parent, and once answered the rendered button announces ITSELF, so the host // measures and focuses the real element. +const emit = defineEmits(['standalone']) const toolbar = inject('mapToolbar', null) const self = getCurrentInstance() const hosted = ref(false) @@ -51,12 +57,30 @@ function announce(from, el) { try { from.dispatchEvent(new CustomEvent('dxmaps:mapinfo-control', { bubbles: true, composed: true, detail })) } catch { /* no CustomEvent */ } return detail.ack === true } +// Inside the gallery (its panes carry data-doxa-gallery), across shadow roots. +function inGallery(el) { + for (let n = el; n; n = n.parentNode || n.host) if (n.nodeType === 1 && n.hasAttribute('data-doxa-gallery')) return true + return false +} +// The map's own popup is up: it answers from the map, so this control is hosted by it. +let local = false function ask() { const from = origin() if (!from) return const was = hosted.value hosted.value = announce(from, node()) if (hosted.value && !was) nextTick(() => { const el = node(); if (el) announce(el, el) }) + // Nobody answered and the map carries its info: ask the map for its own popup. When it mounts, it + // announces itself ('dxmaps:mapinfo-host', detail.local), and the question above is asked again. + if (!hosted.value && props.standalone && !local && !inGallery(from)) { local = true; emit('standalone', true) } +} +// A host appeared. The map's own popup asks us again; any OTHER host (the gallery's) takes over, so the +// map's popup is dropped first: it sits inside the map and would answer before the gallery could. +function onHost(e) { + if (local && !(e && e.detail && e.detail.local)) { + local = false; hosted.value = false; emit('standalone', false); nextTick(ask); return + } + ask() } function openInfo() { const el = node() @@ -65,9 +89,12 @@ function openInfo() { } onMounted(() => { nextTick(ask) - window.addEventListener('dxmaps:mapinfo-host', ask) + window.addEventListener('dxmaps:mapinfo-host', onHost) +}) +onBeforeUnmount(() => { + window.removeEventListener('dxmaps:mapinfo-host', onHost) + if (local) emit('standalone', false) }) -onBeforeUnmount(() => { window.removeEventListener('dxmaps:mapinfo-host', ask) })