Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion src/essence/Tools/MapControl/MMGISMapControlAdapter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { MapControlBar } from './lib'
import type { BasemapStyle } from './lib'
// ShareExport's portable share control — reused verbatim so the button looks
// and behaves identically wherever it's hosted. Importing the lib barrel also
// loads its (host-class-scoped) styles.
import { ShareMenu } from '../ShareExport/lib'
import { useMMGISToolVars } from '../_shared/adapters/useMMGISToolVars'
import { useMMGISHandlerReady } from '../_shared/adapters/useMMGISHandlerReady'
import {
copyShareLink,
downloadSharePng,
downloadSharePdf,
} from '../_shared/adapters/shareActions'
import { getBasemaps } from './adapters/getBasemaps'
import {
selectBasemap,
Expand All @@ -22,21 +31,70 @@ type ToolVars = {
showSearch?: unknown
showMeasure?: unknown
showZoom?: unknown
showShare?: unknown
}

const COPIED_RESET_MS = 1800

const isFalsy = (v: unknown) =>
v === false || v === 'false' || v === 0 || v === '0'

export function MMGISMapControlAdapter() {
const [basemapStyles, setBasemapStyles] = useState<BasemapStyle[]>([])
const [activeBasemap, setActiveBasemap] = useState<BasemapStyle | null>(null)
const [shareBusy, setShareBusy] = useState(false)
const [shareCopied, setShareCopied] = useState(false)
const copiedTimer = useRef<number | null>(null)
const vars = useMMGISToolVars<ToolVars>('mapcontrol')

// Same handler pattern as MMGISShareExportAdapter, wired to the shared
// share actions.
useEffect(
() => () => {
if (copiedTimer.current) window.clearTimeout(copiedTimer.current)
},
[]
)
const handleCopyLink = useCallback(async () => {
try {
await copyShareLink()
setShareCopied(true)
if (copiedTimer.current) window.clearTimeout(copiedTimer.current)
copiedTimer.current = window.setTimeout(
() => setShareCopied(false),
COPIED_RESET_MS
)
} catch (err) {
console.error('MapControl: copy link failed', err)
}
}, [])
const handleDownloadPng = useCallback(async () => {
setShareBusy(true)
try {
await downloadSharePng()
} catch (err) {
console.error('MapControl: PNG download failed', err)
} finally {
setShareBusy(false)
}
}, [])
const handleDownloadPdf = useCallback(async () => {
setShareBusy(true)
try {
await downloadSharePdf()
} catch (err) {
console.error('MapControl: PDF download failed', err)
} finally {
setShareBusy(false)
}
}, [])

// Default ON; a saved false/0 disables the feature.
const showBasemapSwitcher = !isFalsy(vars.showBasemapSwitcher)
const showSearch = !isFalsy(vars.showSearch)
const showMeasure = !isFalsy(vars.showMeasure)
const showZoom = !isFalsy(vars.showZoom)
const showShare = !isFalsy(vars.showShare)

// Fetch once the map registers its basemap handlers. An empty style list
// then means the mission genuinely has no basemap — not "not ready yet".
Expand Down Expand Up @@ -67,6 +125,23 @@ export function MMGISMapControlAdapter() {
onRemoveMeasureLabel={showMeasure ? removeMeasureLabel : undefined}
onSetCursor={showMeasure ? setCursor : undefined}
onSearchSelect={showSearch ? flyToResult : undefined}
endSlot={
showShare ? (
// shareExport-tool-host scopes the component's tokens;
// blocks-map-control__share restyles the trigger to the
// bar's dark button design.
<div className="shareExport-tool-host blocks-map-control__share">
<ShareMenu
formats={{ png: true, pdf: true }}
busy={shareBusy}
copied={shareCopied}
onCopyLink={handleCopyLink}
onDownloadPng={handleDownloadPng}
onDownloadPdf={handleDownloadPdf}
/>
</div>
) : undefined
}
/>
)
}
8 changes: 8 additions & 0 deletions src/essence/Tools/MapControl/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
"type": "checkbox",
"width": 3,
"defaultChecked": true
},
{
"field": "variables.showShare",
"name": "Share",
"description": "Show the share button (copy link, export PNG/PDF).",
"type": "checkbox",
"width": 3,
"defaultChecked": true
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export type MapControlBarProps = {

// Geocode search
onSearchSelect?: (result: GeocodeResult) => void

/**
* Optional element rendered at the right end of the bar (e.g. a share
* control). The bar only places it; look and behavior belong to the host.
*/
endSlot?: React.ReactNode
}

export function MapControlBar({
Expand All @@ -49,6 +55,7 @@ export function MapControlBar({
onRemoveMeasureLabel,
onSetCursor,
onSearchSelect,
endSlot,
}: MapControlBarProps) {
const rootRef = useRef<HTMLDivElement>(null)
const [basemapOpen, setBasemapOpen] = useState(false)
Expand Down Expand Up @@ -162,6 +169,7 @@ export function MapControlBar({
</button>
</div>
)}
{endSlot}
</div>

{basemapOpen && hasStyles && (
Expand All @@ -185,6 +193,7 @@ export function MapControlBar({
/>
)}


{measure.awaitingFirst && (
<div className="blocks-map-control__hint">Click two points on the map</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,27 @@
background: var(--theme-color-base-lighter, #dfe1e2);
}

// The hosted share control ("Share map") in the bar's end slot — the
// dropdown stays the component's own; only the trigger is restyled to the
// bar's dark filled-button design.
&__share {
display: flex;
align-items: center;

&.shareExport-tool-host .share-menu__trigger {
background: var(--theme-color-primary-darker, #162e51);
color: var(--theme-color-white, #ffffff);
font-family: var(--theme-font-ui);
font-size: var(--theme-font-size-2xs, 0.88rem);
border-radius: var(--theme-radius-md, 0.25rem);
box-shadow: 0 1px 3px var(--theme-color-shadow, rgba(0, 0, 0, 0.15));

&:hover {
background: var(--theme-color-primary-dark, #1a4480);
}
}
}

&__hint {
margin-top: var(--theme-spacing-05, 0.25rem);
align-self: flex-end;
Expand Down
2 changes: 1 addition & 1 deletion src/essence/Tools/ShareExport/MMGISShareExportAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
copyShareLink,
downloadSharePng,
downloadSharePdf,
} from './adapters/shareActions'
} from '../_shared/adapters/shareActions'

const PLUGIN_ID = 'shareexport'
const DEFAULT_FORMATS: ShareFormats = { png: true, pdf: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
mmgisCopyText,
type MapScreenshotResult,
type ViewState,
} from '../../_shared/adapters/mmgisAPI'
} from './mmgisAPI'
import { buildSharePdf, type JsPdfLike } from './sharePdf'
import { blobToDataUrl, downloadBlob } from './download'

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/ShareExport/shareActions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
PNG_FILENAME,
PDF_FILENAME,
buildExportFilename,
} from '../../../src/essence/Tools/ShareExport/adapters/shareActions.ts'
} from '../../../src/essence/Tools/_shared/adapters/shareActions.ts'
import {
mmgisCopyText,
mmgisWriteCoordinateURL,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/ShareExport/sharePdf.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from 'vitest'
import {
computeCenteredPlacement,
buildSharePdf,
} from '../../../src/essence/Tools/ShareExport/adapters/sharePdf.ts'
} from '../../../src/essence/Tools/_shared/adapters/sharePdf.ts'

// Issue #144 - the PDF embeds the PNG snapshot centered on a portrait page,
// scaled to fit within margins while preserving aspect ratio.
Expand Down