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;
}