feat: add share controls

This commit is contained in:
2026-07-24 12:24:16 +01:00
parent a478f8972f
commit d2567c59ce
3 changed files with 138 additions and 14 deletions
+63
View File
@@ -164,6 +164,7 @@ const map = new ol.Map({
createVisibleLayersExtentControl(), createVisibleLayersExtentControl(),
createAreaZoomControl(), createAreaZoomControl(),
...(shouldShowDeviceCompassControl() ? [createDeviceCompassControl()] : []), ...(shouldShowDeviceCompassControl() ? [createDeviceCompassControl()] : []),
createShareControl(),
createNorthPointerControl(), createNorthPointerControl(),
]), ]),
layers: [ layers: [
@@ -296,6 +297,45 @@ function createVisibleLayersExtentControl() {
return new ol.control.Control({ element }); 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() { function createAreaZoomControl() {
const element = document.createElement("div"); const element = document.createElement("div");
element.className = "ol-area-zoom ol-unselectable ol-control"; element.className = "ol-area-zoom ol-unselectable ol-control";
@@ -993,12 +1033,35 @@ async function renderFeatureModal(feature, isParcel, requestId) {
content.append(section); content.append(section);
} }
content.append(createFeatureModalShareButton());
// A linked-feature request may finish after a newer selection was made. // A linked-feature request may finish after a newer selection was made.
if (!els.featureModal.hidden && requestId === featureModalRequestId) { if (!els.featureModal.hidden && requestId === featureModalRequestId) {
els.featureModalContent.replaceChildren(content); 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) { function createFeatureHeading(kind, id) {
const heading = document.createElement("h2"); const heading = document.createElement("h2");
heading.id = "feature-modal-title"; heading.id = "feature-modal-title";
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XPro Map Visualizer</title> <title>XPro Map Visualizer</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,400,0,0" />
<link rel="stylesheet" href="./vendor/ol.css" /> <link rel="stylesheet" href="./vendor/ol.css" />
<link rel="stylesheet" href="./styles.css" /> <link rel="stylesheet" href="./styles.css" />
</head> </head>
+74 -14
View File
@@ -8,6 +8,7 @@
--accent: #1f7a62; --accent: #1f7a62;
--accent-strong: #125441; --accent-strong: #125441;
--shadow: 0 24px 60px rgba(52, 43, 28, 0.12); --shadow: 0 24px 60px rgba(52, 43, 28, 0.12);
--map-tool-size: 42px;
} }
* { * {
@@ -579,6 +580,26 @@ button:disabled {
font-weight: 700; 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 { .map-wrap .ol-zoom {
top: 16px; top: 16px;
right: 16px; right: 16px;
@@ -592,9 +613,10 @@ button:disabled {
} }
.map-wrap .ol-zoom button, .map-wrap .ol-zoom button,
.map-wrap .ol-visible-layers-extent button { .map-wrap .ol-visible-layers-extent button,
width: 2.0625em; .map-wrap .ol-share button {
height: 2.0625em; width: var(--map-tool-size);
height: var(--map-tool-size);
margin: 0; margin: 0;
background: linear-gradient(135deg, var(--accent) 0%, #2b9a79 100%); background: linear-gradient(135deg, var(--accent) 0%, #2b9a79 100%);
color: #fffefb; color: #fffefb;
@@ -610,7 +632,9 @@ button:disabled {
.map-wrap .ol-zoom button:hover, .map-wrap .ol-zoom button:hover,
.map-wrap .ol-zoom button:focus, .map-wrap .ol-zoom button:focus,
.map-wrap .ol-visible-layers-extent button:hover, .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%); background: linear-gradient(135deg, var(--accent-strong) 0%, var(--accent) 100%);
color: #fff; color: #fff;
outline: 2px solid rgba(31, 122, 98, 0.35); outline: 2px solid rgba(31, 122, 98, 0.35);
@@ -618,7 +642,7 @@ button:disabled {
} }
.map-wrap .ol-visible-layers-extent { .map-wrap .ol-visible-layers-extent {
top: 96px; top: 108px;
right: 16px; right: 16px;
left: auto; left: auto;
overflow: hidden; overflow: hidden;
@@ -634,7 +658,7 @@ button:disabled {
} }
.map-wrap .ol-area-zoom { .map-wrap .ol-area-zoom {
top: 140px; top: 158px;
right: 16px; right: 16px;
left: auto; left: auto;
overflow: hidden; overflow: hidden;
@@ -647,8 +671,8 @@ button:disabled {
.map-wrap .ol-area-zoom button { .map-wrap .ol-area-zoom button {
display: grid; display: grid;
width: 2.0625em; width: var(--map-tool-size);
height: 2.0625em; height: var(--map-tool-size);
margin: 0; margin: 0;
padding: 0; padding: 0;
place-items: center; place-items: center;
@@ -667,6 +691,33 @@ button:disabled {
outline-offset: -2px; 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 { .area-zoom-icon {
display: block; display: block;
width: 1.2em; width: 1.2em;
@@ -684,7 +735,7 @@ button:disabled {
} }
.map-wrap .ol-device-compass { .map-wrap .ol-device-compass {
top: 184px; top: 208px;
right: 16px; right: 16px;
left: auto; left: auto;
overflow: hidden; overflow: hidden;
@@ -697,8 +748,8 @@ button:disabled {
.map-wrap .ol-device-compass button { .map-wrap .ol-device-compass button {
display: grid; display: grid;
width: 2.0625em; width: var(--map-tool-size);
height: 2.0625em; height: var(--map-tool-size);
margin: 0; margin: 0;
padding: 0; padding: 0;
place-items: center; place-items: center;
@@ -800,17 +851,26 @@ button:disabled {
} }
.map-wrap .ol-visible-layers-extent { .map-wrap .ol-visible-layers-extent {
top: 92px; top: 104px;
right: 12px; right: 12px;
} }
.map-wrap .ol-area-zoom { .map-wrap .ol-area-zoom {
top: 136px; top: 154px;
right: 12px; right: 12px;
} }
.map-wrap .ol-share {
top: 204px;
right: 12px;
}
.map-wrap .ol-device-compass ~ .ol-share {
top: 254px;
}
.map-wrap .ol-device-compass { .map-wrap .ol-device-compass {
top: 180px; top: 204px;
right: 12px; right: 12px;
} }