Merge origin/development into exp-shepherd/pinhint-s3
# Conflicts: # public/dx-maps/cities-internet/connectivity-list/index.html # public/dx-maps/cities-internet/connectivity-map/index.html # public/dx-maps/cities-internet/data-tables/index.html # public/dx-maps/doxa-maps-spa/manifest.json # public/dx-maps/gospel-resources/charts/index.html # public/dx-maps/gospel-resources/data-tables/index.html # public/dx-maps/gospel-resources/gospel-dashboard/index.html # public/dx-maps/gospel-resources/resources-list/index.html # public/dx-maps/gospel-resources/resources-map/index.html # public/dx-maps/manifest.json
This commit is contained in:
commit
e96ae2c133
8 changed files with 160 additions and 60 deletions
|
|
@ -39,7 +39,7 @@ function setGlobal(name, value) {
|
|||
* and mobile carriers but ALSO banks, universities and hosting resellers; the
|
||||
* carrier flag is a NAME-PATTERN HEURISTIC, not a verified classification.
|
||||
*/
|
||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useOptionalT, fillParams } from '@map/composables/useOptionalT.js'
|
||||
import { tStatic, formatList } from '@map/i18n/index.js'
|
||||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||||
|
|
@ -49,6 +49,7 @@ import { addMapAttribution } from '@map/composables/useMapAttribution'
|
|||
// children). ThemeToggleButton is omitted: this profile's isDark is a read-only
|
||||
// computed off profile-config with no theme-toggle handler to wire it to.
|
||||
import MapToolbar from '@map/components/map-controls/MapToolbar.vue'
|
||||
import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
|
||||
import ZoomInButton from '@map/components/map-controls/ZoomInButton.vue'
|
||||
import ZoomOutButton from '@map/components/map-controls/ZoomOutButton.vue'
|
||||
import LocationButton from '@map/components/map-controls/LocationButton.vue'
|
||||
|
|
@ -90,6 +91,42 @@ const err = ref(null)
|
|||
// measured 7-30s to the first data paint, and for that whole window the map was an empty basemap with
|
||||
// nothing to say it was still working — indistinguishable from a broken embed.
|
||||
const loading = ref(true)
|
||||
|
||||
// ─── "What can this map do?" is on its way (Prayer Progress's announcement) ────────────────────
|
||||
// The control column, with its ⓘ on top, renders only once the map exists, after the data, which can
|
||||
// take tens of seconds on a slow phone. So at mount this map tells the gallery's MapInfo a control is
|
||||
// COMING, and MapInfo waits for the map instead of a clock rather than flashing its floating ⓘ first.
|
||||
// A failure (err) or an unmount before the map exists withdraws it, so the floating ⓘ shows at once.
|
||||
// A standalone page has no MapInfo: nothing answers and nothing changes.
|
||||
function announceInfoPending(pending) {
|
||||
const el = mapEl.value
|
||||
if (!el) return
|
||||
try { el.dispatchEvent(new CustomEvent('dxmaps:mapinfo-pending', { bubbles: true, composed: true, detail: { pending } })) } catch (_) {}
|
||||
}
|
||||
// A MapInfo that mounts after this map asked into the void announces itself; answer it again.
|
||||
const reannounceInfoPending = () => { if (!mapRef.value && !err.value) announceInfoPending(true) }
|
||||
watch(err, (e) => { if (e) announceInfoPending(false) })
|
||||
|
||||
// ─── On a phone the control column starts BELOW the folded legend ─────────────────────────────────
|
||||
// The ⓘ is the column's top control, and its one-time pointer runs sideways from it across the map's
|
||||
// top band, which on a phone is this legend's. MapToolbar's phone rule puts the column at top:72px,
|
||||
// inside that band, so the pointer lay over the legend's title and buttons. The column now starts 8px
|
||||
// below the FOLDED legend's measured bottom, whatever its height in each language. Measured only while
|
||||
// folded, and before the toolbar exists, so opening the legend never moves the controls (MapInfo places
|
||||
// its pointer when the control appears and would not follow a later move). Desktop is unchanged.
|
||||
const legendEl = ref(null)
|
||||
const toolbarTop = ref(0)
|
||||
let legendRO = null
|
||||
function measureLegend() {
|
||||
const el = legendEl.value
|
||||
if (!el || !narrow.value || legendOpen.value) return
|
||||
const b = el.offsetTop + el.offsetHeight
|
||||
if (b > 0) toolbarTop.value = b + 8
|
||||
}
|
||||
function watchLegend() {
|
||||
try { legendRO = new ResizeObserver(measureLegend); if (legendEl.value) legendRO.observe(legendEl.value) } catch (_) { legendRO = null }
|
||||
measureLegend()
|
||||
}
|
||||
// India by state (data/tables/internet-by-adm1.json, NFHS-5): the one country with sub-national data.
|
||||
const adm1 = shallowRef(null) // { meta, byArea: { [adm1]: { name, women: {total,urban,rural}, men: {…} } } }
|
||||
const adm1Sex = ref('women') // which half of the survey the fill shows; both are in the sheet
|
||||
|
|
@ -409,6 +446,9 @@ const ciProvider = resolveMapProvider(inject('profileConfig', null))
|
|||
let unmounted = false // set on unmount; onMounted's awaits check it before building the map
|
||||
|
||||
onMounted(async () => {
|
||||
announceInfoPending(true)
|
||||
window.addEventListener('dxmaps:mapinfo-host', reannounceInfoPending)
|
||||
watchLegend()
|
||||
try { await ensureEngineScript(ciProvider) } catch (_) { /* fall through to the notice below */ }
|
||||
// THE ENGINE THE PAGE ASKED FOR, OR NONE. This read `engineGlobal(ciProvider) || window.mapboxgl`:
|
||||
// engineGlobal already answers for Mapbox, so the fallback only fired when MapLibre was asked for and
|
||||
|
|
@ -485,6 +525,9 @@ onMounted(async () => {
|
|||
} catch (e) { err.value = String(e.message || e); loading.value = false }
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') window.removeEventListener('dxmaps:mapinfo-host', reannounceInfoPending)
|
||||
try { legendRO?.disconnect() } catch (_) {}
|
||||
if (!mapRef.value) announceInfoPending(false)
|
||||
if (typeof window !== 'undefined') window.removeEventListener('resize', syncNarrow)
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
|
|
@ -538,6 +581,7 @@ useShadowStyles(
|
|||
// again — a geometric guarantee, not a tuned z-index.
|
||||
// LOGICAL, not physical: the control rail sits at inset-inline-end, so in Arabic it moves to the
|
||||
// LEFT; a legend pinned left:8px then sat under the zoom buttons (measured in the ar gallery tab).
|
||||
'@media(max-width:699px){.gm .map-toolbar.map-toolbar{top:var(--gm-toolbar-top,72px)}}' +
|
||||
'.gm-legend{position:absolute;top:8px;inset-inline-start:8px;width:var(--map-legend-width,380px);' +
|
||||
'max-width:calc(100% - 16px);box-sizing:border-box;z-index:4;padding:8px 10px;' +
|
||||
'border-radius:10px;background:rgba(255,255,255,.94);color:#0f1419;' +
|
||||
|
|
@ -600,13 +644,17 @@ useShadowStyles(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gm" :class="{ 'is-dark': isDark }">
|
||||
<div class="gm" :class="{ 'is-dark': isDark }" :style="narrow && toolbarTop ? { '--gm-toolbar-top': toolbarTop + 'px' } : null">
|
||||
<div ref="mapEl" class="gm-map"></div>
|
||||
|
||||
<!-- Shared control rail — MapToolbar self-positions top-right. Zoom/Location/
|
||||
Fullscreen gate on the live map instance; ShareButton needs neither and
|
||||
always renders so the map is embeddable. -->
|
||||
<!-- The TOP control is "What can this map do?": it opens the gallery's MapInfo for this tab, which then
|
||||
hides its own floating button. It renders only where a MapInfo answers it, so a standalone page
|
||||
has no dead button. Round and spaced, the library default. -->
|
||||
<MapToolbar v-if="mapRef">
|
||||
<MapInfoButton :is-dark="isDark" />
|
||||
<ZoomInButton :map="mapRef" :is-dark="isDark" />
|
||||
<ZoomOutButton :map="mapRef" :is-dark="isDark" />
|
||||
<LocationButton :map="mapRef" :is-dark="isDark" />
|
||||
|
|
@ -614,7 +662,7 @@ useShadowStyles(
|
|||
<ShareButton :is-dark="isDark" :embed-path="'cities-internet/connectivity-map/index.html'" />
|
||||
</MapToolbar>
|
||||
|
||||
<div v-show="!groupOpen" class="gm-legend">
|
||||
<div v-show="!groupOpen" ref="legendEl" class="gm-legend">
|
||||
<div class="gm-title">{{ t('connectivity.title') }}</div>
|
||||
<button v-if="narrow" type="button" class="gm-fold" @click="toggleLegend"
|
||||
:aria-expanded="legendOpen ? 'true' : 'false'">
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export const TAB_INFO = {
|
|||
},
|
||||
'cities-internet': {
|
||||
storageKey: 'cities-internet',
|
||||
trigger: 'control',
|
||||
pos: { side: '56px', narrow: { side: '4px' } },
|
||||
leadKey: 'mapInfo.tab.cities-internet.lead',
|
||||
takeawayKey: 'mapInfo.tab.cities-internet.takeaway',
|
||||
|
|
@ -131,6 +132,7 @@ export const TAB_INFO = {
|
|||
},
|
||||
'gospel-resources': {
|
||||
storageKey: 'gospel-resources',
|
||||
trigger: 'control',
|
||||
pos: { side: '56px', narrow: { side: '4px' } },
|
||||
leadKey: 'mapInfo.tab.gospel-resources.lead',
|
||||
takeawayKey: 'mapInfo.tab.gospel-resources.takeaway',
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ function setGlobal(name, value) {
|
|||
* boundaries. Verified against the corpus's own `country_count`: 0 of 2,106 rows
|
||||
* disagree, and the 5-country string above parses to exactly its 5 real names.
|
||||
*/
|
||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { computed, inject, ref, shallowRef, markRaw, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useOptionalT } from '@map/composables/useOptionalT.js' // vue-i18n stays: two guarded LOCALE reads below
|
||||
import { formatList } from '@map/i18n/index.js'
|
||||
|
|
@ -90,6 +90,7 @@ import { engineUiLocale } from '@map/composables/useMapInstance.js'
|
|||
// children). ThemeToggleButton is omitted: this profile's isDark is a read-only
|
||||
// computed off profile-config with no theme-toggle handler to wire it to.
|
||||
import MapToolbar from '@map/components/map-controls/MapToolbar.vue'
|
||||
import MapInfoButton from '@map/components/map-controls/MapInfoButton.vue'
|
||||
import ZoomInButton from '@map/components/map-controls/ZoomInButton.vue'
|
||||
import ZoomOutButton from '@map/components/map-controls/ZoomOutButton.vue'
|
||||
import LocationButton from '@map/components/map-controls/LocationButton.vue'
|
||||
|
|
@ -129,6 +130,42 @@ const rows = ref([])
|
|||
const selected = ref(null)
|
||||
const err = ref(null)
|
||||
|
||||
// ─── "What can this map do?" is on its way (Prayer Progress's announcement) ────────────────────
|
||||
// The control column, with its ⓘ on top, renders only once the map exists, after the data, which can
|
||||
// take tens of seconds on a slow phone. So at mount this map tells the gallery's MapInfo a control is
|
||||
// COMING, and MapInfo waits for the map instead of a clock rather than flashing its floating ⓘ first.
|
||||
// A failure (err) or an unmount before the map exists withdraws it, so the floating ⓘ shows at once.
|
||||
// A standalone page has no MapInfo: nothing answers and nothing changes.
|
||||
function announceInfoPending(pending) {
|
||||
const el = mapEl.value
|
||||
if (!el) return
|
||||
try { el.dispatchEvent(new CustomEvent('dxmaps:mapinfo-pending', { bubbles: true, composed: true, detail: { pending } })) } catch (_) {}
|
||||
}
|
||||
// A MapInfo that mounts after this map asked into the void announces itself; answer it again.
|
||||
const reannounceInfoPending = () => { if (!cardMode.value && !mapRef.value && !err.value) announceInfoPending(true) }
|
||||
watch(err, (e) => { if (e) announceInfoPending(false) })
|
||||
|
||||
// ─── On a phone the control column starts BELOW the folded legend ─────────────────────────────────
|
||||
// The ⓘ is the column's top control, and its one-time pointer runs sideways from it across the map's
|
||||
// top band, which on a phone is this legend's. MapToolbar's phone rule puts the column at top:72px,
|
||||
// inside that band, so the pointer lay over the legend's title and buttons. The column now starts 8px
|
||||
// below the FOLDED legend's measured bottom, whatever its height in each language. Measured only while
|
||||
// folded, and before the toolbar exists, so opening the legend never moves the controls (MapInfo places
|
||||
// its pointer when the control appears and would not follow a later move). Desktop is unchanged.
|
||||
const legendEl = ref(null)
|
||||
const toolbarTop = ref(0)
|
||||
let legendRO = null
|
||||
function measureLegend() {
|
||||
const el = legendEl.value
|
||||
if (!el || !narrow.value || legendOpen.value) return
|
||||
const b = el.offsetTop + el.offsetHeight
|
||||
if (b > 0) toolbarTop.value = b + 8
|
||||
}
|
||||
function watchLegend() {
|
||||
try { legendRO = new ResizeObserver(measureLegend); if (legendEl.value) legendRO.observe(legendEl.value) } catch (_) { legendRO = null }
|
||||
measureLegend()
|
||||
}
|
||||
|
||||
// GAP = what is missing (default). COVERAGE = what was found. See the header note.
|
||||
const mode = ref('gap')
|
||||
|
||||
|
|
@ -1638,6 +1675,11 @@ function openPin(i) {
|
|||
* NOT YET LOADED, a case the local copy would have skipped straight past.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
if (!cardMode.value) {
|
||||
announceInfoPending(true)
|
||||
window.addEventListener('dxmaps:mapinfo-host', reannounceInfoPending)
|
||||
watchLegend()
|
||||
}
|
||||
/* ⚠ CARD MODE RETURNS BEFORE THE ENGINE. Everything below this block — the engine script, its
|
||||
stylesheet, the boundaries, the pin index — is what makes the card expensive, so none of it
|
||||
runs. One fetch, then render. */
|
||||
|
|
@ -1801,6 +1843,9 @@ onMounted(async () => {
|
|||
} catch (e) { err.value = String(e.message || e) }
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') window.removeEventListener('dxmaps:mapinfo-host', reannounceInfoPending)
|
||||
try { legendRO?.disconnect() } catch (_) {}
|
||||
if (!mapRef.value) announceInfoPending(false)
|
||||
// ★ RELEASE THE TEST HOOKS. Each pins whatever it closes over — a map instance pins its
|
||||
// canvas, its sources, and through them the whole app — so every mount that leaves one behind
|
||||
// leaks another copy, measured at roughly 10 MB each.
|
||||
|
|
@ -1987,6 +2032,7 @@ useShadowStyles(
|
|||
'.gm-sheet-full{top:0;max-height:100%;border-radius:0}' +
|
||||
'@media(min-width:700px){.gm-sheet-full{inset:0;width:auto;border-radius:0}}' +
|
||||
'.gm-card-wait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:15px;color:#6d766e;padding:24px;text-align:center}' +
|
||||
'@media(max-width:699px){.gm .map-toolbar.map-toolbar{top:var(--gm-toolbar-top,72px)}}' +
|
||||
'.gm-legend{position:absolute;top:8px;inset-inline-start:8px;width:var(--map-legend-width,380px);' +
|
||||
'max-height:calc(100% - 16px);overflow-y:auto;overscroll-behavior:contain;' +
|
||||
'max-width:calc(100% - 16px);box-sizing:border-box;z-index:4;padding:8px 10px;' +
|
||||
|
|
@ -2214,7 +2260,7 @@ useShadowStyles(
|
|||
override is gone. What stays LTR is each block of English EXPLANATORY PROSE, marked
|
||||
where it sits: those are still English by decision, and English inside an RTL box is
|
||||
the defect this replaced. -->
|
||||
<div class="gm" :class="{ 'is-dark': isDark, 'has-sheet': !!(selected || showGapList) }">
|
||||
<div class="gm" :class="{ 'is-dark': isDark, 'has-sheet': !!(selected || showGapList) }" :style="narrow && toolbarTop ? { '--gm-toolbar-top': toolbarTop + 'px' } : null">
|
||||
<div v-show="!cardMode" ref="mapEl" class="gm-map"></div>
|
||||
<!-- ⚠ CARD MODE RENDERED BLANK TWICE. First because the sheet read selected.country when card
|
||||
mode had no country; then because the sheet mounted on cardMode ALONE, before the shard
|
||||
|
|
@ -2228,7 +2274,11 @@ useShadowStyles(
|
|||
<!-- Shared control rail — MapToolbar self-positions top-right. Zoom/Location/
|
||||
Fullscreen gate on the live map instance; ShareButton needs neither and
|
||||
always renders so the map is embeddable. -->
|
||||
<!-- The TOP control is "What can this map do?": it opens the gallery's MapInfo for this tab, which then
|
||||
hides its own floating button. It renders only where a MapInfo answers it, so a standalone page
|
||||
has no dead button. Round and spaced, the library default. -->
|
||||
<MapToolbar v-if="mapRef">
|
||||
<MapInfoButton :is-dark="isDark" />
|
||||
<ZoomInButton :map="mapRef" :is-dark="isDark" />
|
||||
<ZoomOutButton :map="mapRef" :is-dark="isDark" />
|
||||
<LocationButton :map="mapRef" :is-dark="isDark" />
|
||||
|
|
@ -2236,7 +2286,7 @@ useShadowStyles(
|
|||
<ShareButton :is-dark="isDark" :embed-path="shareEmbedPath" />
|
||||
</MapToolbar>
|
||||
|
||||
<div v-if="!cardMode" class="gm-legend" :class="{ closed: !legendOpen && !narrow }">
|
||||
<div v-if="!cardMode" ref="legendEl" class="gm-legend" :class="{ closed: !legendOpen && !narrow }">
|
||||
<!-- ═══ SOMEBODY ELSE'S LIST, SAID PLAINLY ════════════════════════════════════════
|
||||
First thing in the legend, because it is the first thing that is different about
|
||||
this map: the reader is looking at a subset somebody chose. Two numbers and one
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-GroDOm6hlQtsW1ATYE7OtUoJoT1kv8bw1UNS00Z2tuLlj0OSA1dIUvmk2Z+XPOPZ" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-1u+pox1NZIUdGV3Zm085Le7CJvxDl2xSPx4mdD4GbQExILhQvBmD5+27t3yV8kM7" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-GroDOm6hlQtsW1ATYE7OtUoJoT1kv8bw1UNS00Z2tuLlj0OSA1dIUvmk2Z+XPOPZ" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-1u+pox1NZIUdGV3Zm085Le7CJvxDl2xSPx4mdD4GbQExILhQvBmD5+27t3yV8kM7" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue