diff --git a/app.js b/app.js index 46b962a..5eb8d3c 100644 --- a/app.js +++ b/app.js @@ -164,6 +164,7 @@ const map = new ol.Map({ createVisibleLayersExtentControl(), createAreaZoomControl(), ...(shouldShowDeviceCompassControl() ? [createDeviceCompassControl()] : []), + createShareControl(), createNorthPointerControl(), ]), layers: [ @@ -296,6 +297,45 @@ function createVisibleLayersExtentControl() { return new ol.control.Control({ element }); } +function createShareControl() { + const element = document.createElement("div"); + element.className = "ol-share ol-unselectable ol-control"; + + const button = document.createElement("button"); + button.type = "button"; + button.title = "Copy link to this map view"; + button.setAttribute("aria-label", "Copy link to this map view"); + const icon = document.createElement("span"); + icon.className = "material-symbols-outlined"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "share"; + button.append(icon); + button.addEventListener("click", copyCurrentViewUrl); + + element.append(button); + return new ol.control.Control({ element }); +} + +async function copyCurrentViewUrl() { + // Ensure the copied address reflects state changes that may not yet have + // reached the next map movement event. + syncSharedMapStateToUrl(); + + try { + if (!navigator.clipboard?.writeText) { + throw new Error("Clipboard access is unavailable."); + } + + await navigator.clipboard.writeText(window.location.href); + showMessage("Link to this map view copied to your clipboard."); + } catch (error) { + console.error(error); + showMessage("Unable to copy the map link. Please copy the address from your browser.", { + type: "error", + }); + } +} + function createAreaZoomControl() { const element = document.createElement("div"); element.className = "ol-area-zoom ol-unselectable ol-control"; @@ -993,12 +1033,35 @@ async function renderFeatureModal(feature, isParcel, requestId) { content.append(section); } + content.append(createFeatureModalShareButton()); + // A linked-feature request may finish after a newer selection was made. if (!els.featureModal.hidden && requestId === featureModalRequestId) { els.featureModalContent.replaceChildren(content); } } +function createFeatureModalShareButton() { + const actions = document.createElement("div"); + actions.className = "feature-modal-actions"; + + const button = document.createElement("button"); + button.type = "button"; + button.className = "feature-modal-share"; + button.title = "Copy link to this map view"; + button.setAttribute("aria-label", "Copy link to this map view"); + + const icon = document.createElement("span"); + icon.className = "material-symbols-outlined"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "share"; + button.append(icon); + button.addEventListener("click", copyCurrentViewUrl); + + actions.append(button); + return actions; +} + function createFeatureHeading(kind, id) { const heading = document.createElement("h2"); heading.id = "feature-modal-title"; diff --git a/index.html b/index.html index b718d3d..00e10a6 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ XPro Map Visualizer + diff --git a/styles.css b/styles.css index d4bc84e..3480b87 100644 --- a/styles.css +++ b/styles.css @@ -8,6 +8,7 @@ --accent: #1f7a62; --accent-strong: #125441; --shadow: 0 24px 60px rgba(52, 43, 28, 0.12); + --map-tool-size: 42px; } * { @@ -579,6 +580,26 @@ button:disabled { font-weight: 700; } +.feature-modal-actions { + display: flex; + justify-content: flex-end; + margin-top: 16px; +} + +.feature-modal-share { + display: grid; + width: 42px; + height: 42px; + padding: 0; + place-items: center; + border-radius: 50%; +} + +.feature-modal-share .material-symbols-outlined { + font-size: 1.25rem; + line-height: 1; +} + .map-wrap .ol-zoom { top: 16px; right: 16px; @@ -592,9 +613,10 @@ button:disabled { } .map-wrap .ol-zoom button, -.map-wrap .ol-visible-layers-extent button { - width: 2.0625em; - height: 2.0625em; +.map-wrap .ol-visible-layers-extent button, +.map-wrap .ol-share button { + width: var(--map-tool-size); + height: var(--map-tool-size); margin: 0; background: linear-gradient(135deg, var(--accent) 0%, #2b9a79 100%); color: #fffefb; @@ -610,7 +632,9 @@ button:disabled { .map-wrap .ol-zoom button:hover, .map-wrap .ol-zoom button:focus, .map-wrap .ol-visible-layers-extent button:hover, -.map-wrap .ol-visible-layers-extent button:focus { +.map-wrap .ol-visible-layers-extent button:focus, +.map-wrap .ol-share button:hover, +.map-wrap .ol-share button:focus { background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%); color: #fff; outline: 2px solid rgba(31, 122, 98, 0.35); @@ -618,7 +642,7 @@ button:disabled { } .map-wrap .ol-visible-layers-extent { - top: 96px; + top: 108px; right: 16px; left: auto; overflow: hidden; @@ -634,7 +658,7 @@ button:disabled { } .map-wrap .ol-area-zoom { - top: 140px; + top: 158px; right: 16px; left: auto; overflow: hidden; @@ -647,8 +671,8 @@ button:disabled { .map-wrap .ol-area-zoom button { display: grid; - width: 2.0625em; - height: 2.0625em; + width: var(--map-tool-size); + height: var(--map-tool-size); margin: 0; padding: 0; place-items: center; @@ -667,6 +691,33 @@ button:disabled { outline-offset: -2px; } +.map-wrap .ol-share { + top: 208px; + right: 16px; + left: auto; + overflow: hidden; + background: var(--panel); + border: 1px solid var(--panel-border); + border-radius: 14px; + backdrop-filter: blur(16px); + box-shadow: var(--shadow); +} + +/* When available, keep Share after the device compass in the tool stack. */ +.map-wrap .ol-device-compass ~ .ol-share { + top: 258px; +} + +.map-wrap .ol-share button { + display: grid; + place-items: center; +} + +.map-wrap .ol-share .material-symbols-outlined { + font-size: 1.25rem; + line-height: 1; +} + .area-zoom-icon { display: block; width: 1.2em; @@ -684,7 +735,7 @@ button:disabled { } .map-wrap .ol-device-compass { - top: 184px; + top: 208px; right: 16px; left: auto; overflow: hidden; @@ -697,8 +748,8 @@ button:disabled { .map-wrap .ol-device-compass button { display: grid; - width: 2.0625em; - height: 2.0625em; + width: var(--map-tool-size); + height: var(--map-tool-size); margin: 0; padding: 0; place-items: center; @@ -800,17 +851,26 @@ button:disabled { } .map-wrap .ol-visible-layers-extent { - top: 92px; + top: 104px; right: 12px; } .map-wrap .ol-area-zoom { - top: 136px; + top: 154px; right: 12px; } + .map-wrap .ol-share { + top: 204px; + right: 12px; + } + + .map-wrap .ol-device-compass ~ .ol-share { + top: 254px; + } + .map-wrap .ol-device-compass { - top: 180px; + top: 204px; right: 12px; }