diff --git a/src/essence/Tools/MapControl/MMGISMapControlAdapter.tsx b/src/essence/Tools/MapControl/MMGISMapControlAdapter.tsx index 03249dcbc..67e1fd8b4 100644 --- a/src/essence/Tools/MapControl/MMGISMapControlAdapter.tsx +++ b/src/essence/Tools/MapControl/MMGISMapControlAdapter.tsx @@ -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, @@ -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([]) const [activeBasemap, setActiveBasemap] = useState(null) + const [shareBusy, setShareBusy] = useState(false) + const [shareCopied, setShareCopied] = useState(false) + const copiedTimer = useRef(null) const vars = useMMGISToolVars('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". @@ -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. +
+ +
+ ) : undefined + } /> ) } diff --git a/src/essence/Tools/MapControl/config.json b/src/essence/Tools/MapControl/config.json index ac9d282ec..e0de47565 100644 --- a/src/essence/Tools/MapControl/config.json +++ b/src/essence/Tools/MapControl/config.json @@ -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 } ] } diff --git a/src/essence/Tools/MapControl/lib/geo/MapControlBar/MapControlBar.tsx b/src/essence/Tools/MapControl/lib/geo/MapControlBar/MapControlBar.tsx index d845a2aa4..1b3910af9 100644 --- a/src/essence/Tools/MapControl/lib/geo/MapControlBar/MapControlBar.tsx +++ b/src/essence/Tools/MapControl/lib/geo/MapControlBar/MapControlBar.tsx @@ -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({ @@ -49,6 +55,7 @@ export function MapControlBar({ onRemoveMeasureLabel, onSetCursor, onSearchSelect, + endSlot, }: MapControlBarProps) { const rootRef = useRef(null) const [basemapOpen, setBasemapOpen] = useState(false) @@ -162,6 +169,7 @@ export function MapControlBar({ )} + {endSlot} {basemapOpen && hasStyles && ( @@ -185,6 +193,7 @@ export function MapControlBar({ /> )} + {measure.awaitingFirst && (
Click two points on the map
)} diff --git a/src/essence/Tools/MapControl/lib/styles/components-geo/map-control-bar.scss b/src/essence/Tools/MapControl/lib/styles/components-geo/map-control-bar.scss index b8989de1f..2ac60f5cf 100644 --- a/src/essence/Tools/MapControl/lib/styles/components-geo/map-control-bar.scss +++ b/src/essence/Tools/MapControl/lib/styles/components-geo/map-control-bar.scss @@ -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; diff --git a/src/essence/Tools/ShareExport/MMGISShareExportAdapter.tsx b/src/essence/Tools/ShareExport/MMGISShareExportAdapter.tsx index e2faeac00..19fbb8a0b 100644 --- a/src/essence/Tools/ShareExport/MMGISShareExportAdapter.tsx +++ b/src/essence/Tools/ShareExport/MMGISShareExportAdapter.tsx @@ -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 } diff --git a/src/essence/Tools/ShareExport/adapters/download.ts b/src/essence/Tools/_shared/adapters/download.ts similarity index 100% rename from src/essence/Tools/ShareExport/adapters/download.ts rename to src/essence/Tools/_shared/adapters/download.ts diff --git a/src/essence/Tools/ShareExport/adapters/shareActions.ts b/src/essence/Tools/_shared/adapters/shareActions.ts similarity index 99% rename from src/essence/Tools/ShareExport/adapters/shareActions.ts rename to src/essence/Tools/_shared/adapters/shareActions.ts index 9113453f5..c8a8d075b 100644 --- a/src/essence/Tools/ShareExport/adapters/shareActions.ts +++ b/src/essence/Tools/_shared/adapters/shareActions.ts @@ -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' diff --git a/src/essence/Tools/ShareExport/adapters/sharePdf.ts b/src/essence/Tools/_shared/adapters/sharePdf.ts similarity index 100% rename from src/essence/Tools/ShareExport/adapters/sharePdf.ts rename to src/essence/Tools/_shared/adapters/sharePdf.ts diff --git a/tests/unit/ShareExport/shareActions.spec.js b/tests/unit/ShareExport/shareActions.spec.js index 5b34e7ab5..e80b81218 100644 --- a/tests/unit/ShareExport/shareActions.spec.js +++ b/tests/unit/ShareExport/shareActions.spec.js @@ -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, diff --git a/tests/unit/ShareExport/sharePdf.spec.js b/tests/unit/ShareExport/sharePdf.spec.js index 40409540e..4193578c8 100644 --- a/tests/unit/ShareExport/sharePdf.spec.js +++ b/tests/unit/ShareExport/sharePdf.spec.js @@ -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.