Skip to content

Commit 2e42004

Browse files
jeremymanningclaude
andcommitted
Make minimap container draggable with event isolation
Add a drag bar at the top of the minimap for repositioning the container anywhere on screen. Uses pointer capture for smooth dragging and stopPropagation on all container events to prevent interactions from leaking to the map canvas below. Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
1 parent d9a0dba commit 2e42004

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

src/viz/minimap.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export class Minimap {
3434
this._handleMouseMove = this._onMouseMove.bind(this);
3535
this._handleMouseUp = this._onMouseUp.bind(this);
3636
this._handleClick = this._onClick.bind(this);
37+
38+
// Container drag state
39+
this._isContainerDragging = false;
40+
this._containerDragOffset = null;
41+
this._dragBar = null;
42+
this._handleDragBarDown = null;
43+
this._cbContainerMove = null;
44+
this._cbContainerUp = null;
3745
}
3846

3947
init(container, domains) {
@@ -66,6 +74,28 @@ export class Minimap {
6674
window.addEventListener('mousemove', this._handleMouseMove);
6775
window.addEventListener('mouseup', this._handleMouseUp);
6876

77+
// Prevent ALL mouse events on the minimap from propagating to the map canvas below
78+
container.addEventListener('mousedown', (e) => e.stopPropagation());
79+
container.addEventListener('mousemove', (e) => e.stopPropagation());
80+
container.addEventListener('mouseup', (e) => e.stopPropagation());
81+
container.addEventListener('click', (e) => e.stopPropagation());
82+
container.addEventListener('wheel', (e) => e.stopPropagation());
83+
container.addEventListener('pointerdown', (e) => e.stopPropagation());
84+
85+
// Drag bar for repositioning the minimap container
86+
this._dragBar = document.createElement('div');
87+
this._dragBar.style.cssText =
88+
'position:absolute;top:0;left:0;right:0;height:16px;cursor:grab;z-index:3;' +
89+
'background:linear-gradient(to bottom, rgba(0,105,62,0.12), transparent);';
90+
// Grip pill visual indicator
91+
const grip = document.createElement('div');
92+
grip.style.cssText =
93+
'width:32px;height:4px;margin:6px auto 0;border-radius:2px;' +
94+
'background:rgba(0,105,62,0.35);pointer-events:none;';
95+
this._dragBar.appendChild(grip);
96+
container.appendChild(this._dragBar);
97+
this._initContainerDrag();
98+
6999
this.render();
70100
}
71101

@@ -348,6 +378,62 @@ export class Minimap {
348378
ctx.strokeRect(x, y, vw, vh);
349379
}
350380

381+
_initContainerDrag() {
382+
this._handleDragBarDown = (e) => {
383+
e.preventDefault();
384+
e.stopPropagation();
385+
this._isContainerDragging = true;
386+
const rect = this.container.getBoundingClientRect();
387+
this._containerDragOffset = {
388+
x: e.clientX - rect.left,
389+
y: e.clientY - rect.top,
390+
};
391+
this._dragBar.style.cursor = 'grabbing';
392+
// Capture pointer so we receive events even outside the element
393+
if (e.pointerId !== undefined) {
394+
this._dragBar.setPointerCapture(e.pointerId);
395+
}
396+
};
397+
398+
this._cbContainerMove = (e) => {
399+
if (!this._isContainerDragging) return;
400+
const parent = this.container.parentElement;
401+
if (!parent) return;
402+
const parentRect = parent.getBoundingClientRect();
403+
const contW = this.container.offsetWidth;
404+
const contH = this.container.offsetHeight;
405+
406+
let newLeft = e.clientX - parentRect.left - this._containerDragOffset.x;
407+
let newTop = e.clientY - parentRect.top - this._containerDragOffset.y;
408+
409+
// Clamp within parent bounds
410+
newLeft = Math.max(0, Math.min(parentRect.width - contW, newLeft));
411+
newTop = Math.max(0, Math.min(parentRect.height - contH, newTop));
412+
413+
this.container.style.left = newLeft + 'px';
414+
this.container.style.top = newTop + 'px';
415+
// Clear bottom/right anchoring so left/top take effect
416+
this.container.style.right = 'auto';
417+
this.container.style.bottom = 'auto';
418+
};
419+
420+
this._cbContainerUp = (e) => {
421+
if (this._isContainerDragging) {
422+
this._isContainerDragging = false;
423+
this._containerDragOffset = null;
424+
this._dragBar.style.cursor = 'grab';
425+
if (e.pointerId !== undefined) {
426+
try { this._dragBar.releasePointerCapture(e.pointerId); } catch (_) { /* already released */ }
427+
}
428+
}
429+
};
430+
431+
// Use pointer events for capture support, with mouse fallback
432+
this._dragBar.addEventListener('pointerdown', this._handleDragBarDown);
433+
window.addEventListener('pointermove', this._cbContainerMove);
434+
window.addEventListener('pointerup', this._cbContainerUp);
435+
}
436+
351437
_onResizeStart(e) {
352438
e.preventDefault();
353439
e.stopPropagation();
@@ -394,6 +480,23 @@ export class Minimap {
394480
}
395481

396482
destroy() {
483+
// Clean up drag bar
484+
if (this._dragBar) {
485+
if (this._handleDragBarDown) {
486+
this._dragBar.removeEventListener('pointerdown', this._handleDragBarDown);
487+
}
488+
this._dragBar.remove();
489+
this._dragBar = null;
490+
}
491+
if (this._cbContainerMove) {
492+
window.removeEventListener('pointermove', this._cbContainerMove);
493+
this._cbContainerMove = null;
494+
}
495+
if (this._cbContainerUp) {
496+
window.removeEventListener('pointerup', this._cbContainerUp);
497+
this._cbContainerUp = null;
498+
}
499+
397500
if (this._resizeHandle) {
398501
if (this._handleResizeStart) {
399502
this._resizeHandle.removeEventListener('mousedown', this._handleResizeStart);

0 commit comments

Comments
 (0)