From 2510bc583d5a030bff7da34d894444c1a590d883 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 24 Jul 2026 03:46:44 +0100 Subject: [PATCH] feat: allow dragging feature dialog --- app.js | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 11 ++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index d969dba..21f4d38 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,7 @@ const FEATURE_INFO_LAYER_NAMES = new Set(["parcelas", "predios"]); const DEVICE_ORIENTATION_EVENTS = ["deviceorientationabsolute", "deviceorientation"]; const COMPASS_HEADING_DEADBAND = 2; const SMALL_SCREEN_MEDIA_QUERY = "(max-width: 920px)"; +const MODAL_VIEWPORT_PADDING = 24; const els = { appShell: document.querySelector(".app-shell"), @@ -21,6 +22,7 @@ const els = { parcelSearchStatus: document.getElementById("parcel-search-status"), backgroundSelect: document.getElementById("background-select"), featureModal: document.getElementById("feature-modal"), + featureModalDialog: document.querySelector(".feature-modal-dialog"), featureModalClose: document.getElementById("feature-modal-close"), featureModalKind: document.getElementById("feature-modal-kind"), featureArea: document.getElementById("feature-area"), @@ -108,6 +110,7 @@ let lastCompassHeading; let pendingCompassHeading; let compassAnimationFrame; let deviceCompassFallbackTimer; +let modalDrag; const map = new ol.Map({ target: "map", @@ -148,6 +151,10 @@ els.backgroundSelect.addEventListener("change", syncBackground); els.sidebarOpen.addEventListener("click", () => setSidebarCollapsed(false)); els.sidebarClose.addEventListener("click", () => setSidebarCollapsed(true)); els.featureModalClose.addEventListener("click", closeFeatureModal); +els.featureModalDialog.addEventListener("pointerdown", startFeatureModalDrag); +els.featureModalDialog.addEventListener("pointermove", dragFeatureModal); +els.featureModalDialog.addEventListener("pointerup", stopFeatureModalDrag); +els.featureModalDialog.addEventListener("pointercancel", stopFeatureModalDrag); els.featureModal.addEventListener("click", (event) => { if (event.target.matches("[data-feature-modal-close]")) { closeFeatureModal(); @@ -164,6 +171,7 @@ document.addEventListener("keydown", (event) => { } } }); +window.addEventListener("resize", keepFeatureModalInViewport); map.on("singleclick", showFeatureInformation); areaZoomInteraction.on("boxend", zoomToSelectedArea); areaZoomInteraction.on("boxcancel", () => { @@ -745,9 +753,75 @@ function unqualifiedLayerName(name) { } function closeFeatureModal() { + resetFeatureModalPosition(); els.featureModal.hidden = true; } +function startFeatureModalDrag(event) { + if (event.button !== 0 || event.target.closest("button, a, input, select, textarea, label")) { + return; + } + + const dialogRect = els.featureModalDialog.getBoundingClientRect(); + els.featureModalDialog.style.position = "fixed"; + els.featureModalDialog.style.left = `${dialogRect.left}px`; + els.featureModalDialog.style.top = `${dialogRect.top}px`; + els.featureModalDialog.style.margin = "0"; + els.featureModalDialog.classList.add("is-dragging"); + modalDrag = { + pointerId: event.pointerId, + offsetX: event.clientX - dialogRect.left, + offsetY: event.clientY - dialogRect.top, + }; + els.featureModalDialog.setPointerCapture(event.pointerId); +} + +function dragFeatureModal(event) { + if (!modalDrag || event.pointerId !== modalDrag.pointerId) { + return; + } + + const dialogRect = els.featureModalDialog.getBoundingClientRect(); + const maxLeft = Math.max(MODAL_VIEWPORT_PADDING, window.innerWidth - dialogRect.width - MODAL_VIEWPORT_PADDING); + const maxTop = Math.max(MODAL_VIEWPORT_PADDING, window.innerHeight - dialogRect.height - MODAL_VIEWPORT_PADDING); + const left = Math.min(maxLeft, Math.max(MODAL_VIEWPORT_PADDING, event.clientX - modalDrag.offsetX)); + const top = Math.min(maxTop, Math.max(MODAL_VIEWPORT_PADDING, event.clientY - modalDrag.offsetY)); + + els.featureModalDialog.style.left = `${left}px`; + els.featureModalDialog.style.top = `${top}px`; +} + +function stopFeatureModalDrag(event) { + if (!modalDrag || event.pointerId !== modalDrag.pointerId) { + return; + } + + els.featureModalDialog.releasePointerCapture(event.pointerId); + els.featureModalDialog.classList.remove("is-dragging"); + modalDrag = undefined; +} + +function keepFeatureModalInViewport() { + if (els.featureModal.hidden || !els.featureModalDialog.style.left) { + return; + } + + const dialogRect = els.featureModalDialog.getBoundingClientRect(); + const maxLeft = Math.max(MODAL_VIEWPORT_PADDING, window.innerWidth - dialogRect.width - MODAL_VIEWPORT_PADDING); + const maxTop = Math.max(MODAL_VIEWPORT_PADDING, window.innerHeight - dialogRect.height - MODAL_VIEWPORT_PADDING); + els.featureModalDialog.style.left = `${Math.min(maxLeft, Math.max(MODAL_VIEWPORT_PADDING, dialogRect.left))}px`; + els.featureModalDialog.style.top = `${Math.min(maxTop, Math.max(MODAL_VIEWPORT_PADDING, dialogRect.top))}px`; +} + +function resetFeatureModalPosition() { + modalDrag = undefined; + els.featureModalDialog.classList.remove("is-dragging"); + els.featureModalDialog.style.removeProperty("position"); + els.featureModalDialog.style.removeProperty("left"); + els.featureModalDialog.style.removeProperty("top"); + els.featureModalDialog.style.removeProperty("margin"); +} + function formatFeatureArea(feature) { const properties = feature.properties || {}; const areaEntry = Object.entries(properties).find(([name, value]) => diff --git a/styles.css b/styles.css index cbe17ea..81823af 100644 --- a/styles.css +++ b/styles.css @@ -335,7 +335,7 @@ button:disabled { position: absolute; inset: 0; background: rgba(31, 42, 46, 0.35); - backdrop-filter: blur(3px); + backdrop-filter: blur(1px); } .feature-modal-dialog { @@ -345,8 +345,15 @@ button:disabled { border: 1px solid var(--panel-border); border-radius: 24px; background: var(--panel); - backdrop-filter: blur(5px); + backdrop-filter: blur(3px); box-shadow: var(--shadow); + cursor: grab; + touch-action: none; +} + +.feature-modal-dialog.is-dragging { + cursor: grabbing; + user-select: none; } .feature-modal-close {