diff --git a/app.js b/app.js index ba8fbf6..46b962a 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,11 @@ const DEVICE_ORIENTATION_EVENTS = ["deviceorientationabsolute", "deviceorientati const COMPASS_HEADING_DEADBAND = 2; const SMALL_SCREEN_MEDIA_QUERY = "(max-width: 920px)"; const MODAL_VIEWPORT_PADDING = 24; +const MESSAGE_DURATIONS = { + info: 5000, + warning: 6500, + error: 9000, +}; const MAP_URL_PARAMS = { latitude: "map-lat", longitude: "map-lon", @@ -25,6 +30,7 @@ const BACKGROUND_VALUES = new Set(["soft", "openstreetmap", "satellite"]); const els = { appShell: document.querySelector(".app-shell"), + messageRegion: document.getElementById("message-region"), sidebar: document.getElementById("sidebar"), sidebarOpen: document.getElementById("sidebar-open"), sidebarClose: document.getElementById("sidebar-close"), @@ -491,6 +497,56 @@ function updateDeviceCompassButton(message) { } } +/** + * Display a short, non-blocking application message. Callers can override + * `duration` in milliseconds; errors have the longest default reading time. + */ +function showMessage(message, { type = "info", duration = MESSAGE_DURATIONS[type] } = {}) { + if (!els.messageRegion || !message) { + return; + } + + const supportedTypes = new Set(Object.keys(MESSAGE_DURATIONS)); + const messageType = supportedTypes.has(type) ? type : "info"; + const displayDuration = Number.isFinite(duration) && duration > 0 + ? duration + : MESSAGE_DURATIONS[messageType]; + const toast = document.createElement("div"); + toast.className = `message message--${messageType}`; + toast.setAttribute("role", messageType === "error" ? "alert" : "status"); + toast.style.setProperty("--message-duration", `${displayDuration}ms`); + + const text = document.createElement("p"); + text.className = "message-text"; + text.textContent = message; + + const dismiss = document.createElement("button"); + dismiss.className = "message-dismiss"; + dismiss.type = "button"; + dismiss.setAttribute("aria-label", "Dismiss message"); + dismiss.textContent = "×"; + + const progress = document.createElement("span"); + progress.className = "message-progress"; + progress.setAttribute("aria-hidden", "true"); + + let timer = window.setTimeout(removeMessage, displayDuration); + function removeMessage() { + if (timer === undefined) { + return; + } + + window.clearTimeout(timer); + timer = undefined; + toast.classList.add("is-leaving"); + window.setTimeout(() => toast.remove(), 180); + } + + dismiss.addEventListener("click", removeMessage, { once: true }); + toast.append(text, dismiss, progress); + els.messageRegion.append(toast); +} + function setAreaZoomMode(active) { areaZoomInteraction.setActive(active); map.getViewport().classList.toggle("area-zoom-is-active", active); @@ -526,6 +582,7 @@ async function searchParcela(event) { const parcelNumber = els.parcelSearchInput.value.trim(); if (!parcelNumber) { updateParcelSearchStatus("Enter a parcel number to search.", "error"); + showMessage("Enter a parcel number to search.", { type: "warning" }); return; } @@ -555,6 +612,7 @@ async function searchParcela(event) { if (!features.length) { updateParcelSearchStatus(`No parcel found with number “${parcelNumber}”.`, "error"); + showMessage(`No parcel found with number “${parcelNumber}”.`, { type: "warning" }); return; } @@ -565,15 +623,16 @@ async function searchParcela(event) { maxZoom: features.length === 1 ? 19 : 16, }); - updateParcelSearchStatus( - features.length === 1 - ? `Found parcel ${parcelNumber}.` - : `Found ${features.length} parcels matching ${parcelNumber}.`, - "success", - ); + const searchMessage = features.length === 1 + ? `Found parcel ${parcelNumber}.` + : `Found ${features.length} parcels matching ${parcelNumber}.`; + updateParcelSearchStatus(searchMessage, "success"); + showMessage(searchMessage); } catch (error) { console.error(error); - updateParcelSearchStatus(error.message || "Unable to search for that parcel.", "error"); + const searchMessage = error.message || "Unable to search for that parcel."; + updateParcelSearchStatus(searchMessage, "error"); + showMessage(searchMessage, { type: "error" }); } } @@ -610,7 +669,9 @@ async function loadLayers() { updateStatus(`Showing ${availableLayers.length} layer(s) from GeoServer.`, "success"); } catch (error) { console.error(error); - updateStatus(error.message || "Unable to load the WMS service.", "error"); + const loadMessage = error.message || "Unable to load the WMS service."; + updateStatus(loadMessage, "error"); + showMessage(loadMessage, { type: "error" }); availableLayers = []; selectedLayers = []; dwellingParcelSource.clear(); @@ -1141,6 +1202,7 @@ function fitToVisibleLayers() { if (!projectedExtents.length) { updateStatus("Select a layer with a published extent to zoom to it.", "error"); + showMessage("Select a layer with a published extent to zoom to it.", { type: "warning" }); return; } diff --git a/index.html b/index.html index 6bba7de..b718d3d 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,14 @@
+
+