Drag the analysis popup by its title bar

The popup covers the thing it is reporting on — a circle's own parcels sit
directly underneath it — so it has to be movable to be read against the map.
Pressing the title bar and dragging now moves it.

The drag changes the overlay's offset, not its position. The popup stays tied to
the shape it describes, so panning and zooming carry it along and it goes on
pointing at the right circle; only where it rests relative to that anchor
changes. Setting a new position instead would cut it loose and re-anchor it to
whatever ground it happened to be dropped on.

The resting offset is now a named constant shared with the overlay, so the two
cannot drift apart, and each showing starts from it — a popup opened for a new
shape appears where it belongs rather than where the last one was left.

A press on the close button is not a drag, and the overlay already has
stopEvent:true, so the map does not pan underneath.

This covers the Area Analysis popup too: both are built by the same
_buildAnalysisPopupHtml, so both get the handle.

Separate commit from the radius work so it can be cherry-picked onto main if the
radius entry does not survive the usability check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ekke 2026-09-08 17:26:30 +02:00
parent 631b316118
commit 90a5a01b05
7 changed files with 137 additions and 74 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/index-By1A2oQD.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/index.html vendored
View File

@ -1997,7 +1997,7 @@
}
</style>
<script type="module" crossorigin src="/assets/index-C-By5vWJ.js"></script>
<script type="module" crossorigin src="/assets/index-By1A2oQD.js"></script>
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
<link rel="modulepreload" crossorigin href="/assets/pako-Xa-UToif.js">
<link rel="modulepreload" crossorigin href="/assets/geotiff-B9aROX49.js">

View File

@ -105,6 +105,12 @@ import '../styles/layerswitcher.css';
* and every button is given an icon from here instead, which makes this the single
* place the toolbar's iconography is defined.
*/
/**
* Resting offset of the info / analysis popup above its anchor. Dragging the
* popup by its title bar moves it from here; reopening returns it here.
*/
const POPUP_BASE_OFFSET = [0, -10];
const TOOLBAR_ICONS = {
// ol-ext built-ins
'ol-selection': 'bi-cursor',
@ -1671,7 +1677,7 @@ export class MapView {
this.infoPopup = new Overlay({
element: this.infoPopupElement,
positioning: 'bottom-center',
offset: [0, -10],
offset: POPUP_BASE_OFFSET.slice(),
stopEvent: true,
autoPan: true,
autoPanAnimation: { duration: 250 },
@ -1822,6 +1828,61 @@ export class MapView {
* @param {Array<{label:string, value:string, color?:string, empty?:boolean}>} dataRows
* @returns {string} HTML
*/
/**
* Let the analysis popup be dragged by its title bar.
*
* The popup covers whatever it is reporting on — a circle's own parcels sit
* directly under it — so it has to be movable to be read against the map.
*
* The drag changes the overlay's OFFSET, not its position. The popup stays
* tied to the shape it describes, so panning or zooming carries it along and
* it keeps pointing at the right circle; only its resting place relative to
* that anchor changes. Setting a new position instead would cut it loose and
* re-anchor it to whatever ground it was dropped on.
*
* @private
*/
_makePopupDraggable() {
const header = this.infoPopupElement.querySelector('#info-popup-header');
if (!header) return;
// Each popup opens where it belongs; a drag applies to that showing only.
this.infoPopup.setOffset(POPUP_BASE_OFFSET.slice());
let startX = 0;
let startY = 0;
let baseOffset = POPUP_BASE_OFFSET.slice();
const onMove = (evt) => {
this.infoPopup.setOffset([
baseOffset[0] + (evt.clientX - startX),
baseOffset[1] + (evt.clientY - startY),
]);
};
const onUp = (evt) => {
header.releasePointerCapture?.(evt.pointerId);
header.removeEventListener('pointermove', onMove);
header.removeEventListener('pointerup', onUp);
header.removeEventListener('pointercancel', onUp);
header.style.cursor = 'grab';
};
header.addEventListener('pointerdown', (evt) => {
// The close button lives in the header; a press on it is not a drag.
if (evt.target.closest('#info-popup-close') || evt.button !== 0) return;
evt.preventDefault();
startX = evt.clientX;
startY = evt.clientY;
baseOffset = (this.infoPopup.getOffset() || POPUP_BASE_OFFSET).slice();
header.style.cursor = 'grabbing';
header.setPointerCapture?.(evt.pointerId);
header.addEventListener('pointermove', onMove);
header.addEventListener('pointerup', onUp);
header.addEventListener('pointercancel', onUp);
});
}
_buildAnalysisPopupHtml(emoji, title, dataRows) {
let tableRows = '';
for (const row of dataRows) {
@ -1842,7 +1903,7 @@ export class MapView {
}
return `
<div style="background:var(--brand-navy, #1e1a4b);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;">
<div id="info-popup-header" style="background:var(--brand-navy, #1e1a4b);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;">
<span>${emoji} ${title}</span>
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
@ -1885,6 +1946,8 @@ export class MapView {
this.hideInfoPopup();
});
this._makePopupDraggable();
// Summary rows, cleaned for the PDF report (HTML stripped, spacers removed).
// These travel with the export event so the export modal can offer PDF
// alongside the GIS formats — one "Export" entry point instead of two.