On a partner page or a standalone preset page the ⓘ control rendered nothing, because only the gallery answered it and the ⓘ's text lived in the gallery's settings. Now the map carries its info (plugin-designs/doxa-simple-map/src/mapInfo.js) and a host only adds placement: - MapInfoButton gains an opt-in `standalone` prop. When no host answers and the map carries info, it asks the map for a popup of its own (the 'standalone' event). If a real host appears later, the map's popup is dropped and the host takes over. Never inside the gallery (its panes carry data-doxa-gallery): there the gallery alone decides, its own info:false included. - MapInfo gains `popupOnly` (with trigger 'control'): no floating 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 (the bottom sheet on phones). It announces itself as local, so the control can tell it from a real host. - Prayer Progress mounts that popup beside its control column, only when asked. Per preset: the text describes the prayer map, so a map that shows the Prayer tab has info (the full profile and the prayer preset); the engagement-only and adoption-only presets have none and show no ⓘ, as before. A host's profile-config info:false removes the ⓘ. Every other MapInfoButton consumer is unchanged (standalone is off by default). doxa-simple-map grows by 20,590 bytes raw (5,699 gzipped), for compiling MapInfo. Rebuilt doxa-simple-map and doxa-maps-spa, then tree + SRI.
109 lines
5.8 KiB
Vue
109 lines
5.8 KiB
Vue
<!--
|
|
MapInfoButton.vue — the map's "what can this map do for you?" control, IN the map's control column.
|
|
|
|
It opens the host's MapInfo popup and owns no popup of its own. The host (the gallery) mounts one
|
|
MapInfo per tab over the tab's pane, with trigger="control"; this button reaches it with a bubbling,
|
|
COMPOSED event, so the plugin's shadow root is no wall. The popup, its examples and its focus handling
|
|
stay in one place (library/components/info/MapInfo.vue).
|
|
|
|
★ IT SHOWS ONLY WHEN A HOST ANSWERS. A control that opens nothing is worse than no control: a
|
|
standalone leaf or a partner page has no MapInfo. So on mount the control ASKS ('dxmaps:mapinfo-control',
|
|
detail.ack = false), and a MapInfo in control mode on the SAME pane sets ack = true while the event is
|
|
being dispatched (dispatch is synchronous, so the answer is there when dispatchEvent returns). No answer:
|
|
nothing renders (not even a hidden button), and it asks again when a host announces itself later
|
|
('dxmaps:mapinfo-host' on window, for a MapInfo that mounted after the map). Use it inside a MapToolbar.
|
|
|
|
★ HIDING IS NOT THE HOST'S SIGNAL. The host decides whether its floating ⓘ is needed by MEASURING this
|
|
button (connected, a non-zero on-screen rect, not inert), never by an unmount event: a detached element
|
|
cannot bubble anything to the pane. See MapInfo's control mode.
|
|
|
|
Props:
|
|
isDark — theme flag forwarded from parent
|
|
-->
|
|
<script setup>
|
|
import { ref, inject, nextTick, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import MapControlButton from './MapControlButton.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
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' },
|
|
})
|
|
|
|
// ★ AN UNANSWERED CONTROL ADDS NO CHILD TO THE COLUMN. The button renders only once a host has answered
|
|
// (v-if, not v-show): a display:none child is still a child, so it would take the square toolbar's
|
|
// :last-child corners (MapToolbar's controls="square") from the last button a reader can see. So the
|
|
// 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)
|
|
const btn = ref(null)
|
|
const node = () => (btn.value && btn.value.$el) || null
|
|
const origin = () => node() || (toolbar && toolbar.el && toolbar.el.value) || (self && self.proxy && self.proxy.$el && self.proxy.$el.parentNode) || null
|
|
function announce(from, el) {
|
|
const detail = { el, ack: false }
|
|
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()
|
|
if (!el) return
|
|
try { el.dispatchEvent(new CustomEvent('dxmaps:mapinfo-open', { bubbles: true, composed: true, detail: { el } })) } catch { /* no CustomEvent */ }
|
|
}
|
|
onMounted(() => {
|
|
nextTick(ask)
|
|
window.addEventListener('dxmaps:mapinfo-host', onHost)
|
|
})
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('dxmaps:mapinfo-host', onHost)
|
|
if (local) emit('standalone', false)
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<MapControlButton v-if="hosted" ref="btn" data-control="map-info" :is-dark="isDark" :shape="shape" :title="t('mapInfo.open')"
|
|
aria-haspopup="dialog" aria-expanded="false" @click="openInfo">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
|
|
<rect x="11" y="10" width="2" height="7" rx="1" fill="currentColor" />
|
|
<circle cx="12" cy="7" r="1.3" fill="currentColor" />
|
|
</svg>
|
|
</MapControlButton>
|
|
</template>
|