mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(canvas): keep card drags alive on logos and pan with the middle mouse button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e1637cbe02
commit
f751365863
@@ -6156,6 +6156,12 @@ html.dark .canvas-zoom-max {
|
||||
background-image: var(--canvas-dots-image);
|
||||
}
|
||||
|
||||
/* A pan (canvas-core panZoom), also a middle-button pan that started on a card. */
|
||||
:is([data-ec-viewport], .cv-viewport).panning,
|
||||
:is([data-ec-viewport], .cv-viewport).panning * {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
/* "Floating" navigation (Appearance → Navigation style): the top bar, sidebar,
|
||||
tab strip, and settings rail float as framed panels over a dotted backdrop,
|
||||
like the canvas islands. Set on <html> by applyStoredTheme() from
|
||||
|
||||
@@ -108,9 +108,9 @@ export function zoomShortcut(e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wheel zoom (ctrl/cmd) and pan, and drag-to-pan on the viewport background.
|
||||
* Wheel zoom (ctrl/cmd) and pan, drag-to-pan on the viewport background, and middle-button drag-to-pan anywhere.
|
||||
* getView() returns the current { x, y, z }, which is changed in place.
|
||||
* canPan(event) decides which presses pan; onPanEnd(moved) runs on release.
|
||||
* canPan(event) decides which left-button presses pan; onPanEnd(moved) runs on their release.
|
||||
*/
|
||||
export function panZoom({ root, viewport, world, zoomLabel, getView, onChange, canPan, onPanStart, onPanEnd }) {
|
||||
function apply() {
|
||||
@@ -196,8 +196,18 @@ export function panZoom({ root, viewport, world, zoomLabel, getView, onChange, c
|
||||
viewport.addEventListener('pointerup', liftFinger, true);
|
||||
viewport.addEventListener('pointercancel', liftFinger, true);
|
||||
|
||||
viewport.addEventListener('pointerdown', e => {
|
||||
if (e.button !== 0 || pinch || !canPan(e)) return;
|
||||
// A native drag (card logos, links) cancels the pointer, so the card or pan drag stops after a few pixels.
|
||||
viewport.addEventListener('dragstart', e => e.preventDefault());
|
||||
// A middle-button pan that moved does not also open the link under the pointer in a new tab.
|
||||
let panned = false;
|
||||
viewport.addEventListener('auxclick', e => {
|
||||
if (!panned) return;
|
||||
panned = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
|
||||
function startPan(e, middle) {
|
||||
onPanStart?.();
|
||||
const view = getView();
|
||||
const start = { cx: e.clientX, cy: e.clientY, x: view.x, y: view.y };
|
||||
@@ -220,11 +230,25 @@ export function panZoom({ root, viewport, world, zoomLabel, getView, onChange, c
|
||||
viewport.removeEventListener('pointermove', move);
|
||||
viewport.removeEventListener('pointerup', up);
|
||||
viewport.removeEventListener('pointercancel', up);
|
||||
onPanEnd?.(moved);
|
||||
if (middle) panned = moved;
|
||||
else onPanEnd?.(moved);
|
||||
};
|
||||
viewport.addEventListener('pointermove', move);
|
||||
viewport.addEventListener('pointerup', up);
|
||||
viewport.addEventListener('pointercancel', up);
|
||||
}
|
||||
// The middle mouse button pans from anywhere (also from cards and frames).
|
||||
// Capture phase: the press does not reach the card, zone or link drags.
|
||||
viewport.addEventListener('pointerdown', e => {
|
||||
panned = false; // the auxclick of the last pan did not come (released elsewhere)
|
||||
if (pinch || e.pointerType !== 'mouse' || e.button !== 1) return;
|
||||
e.preventDefault(); // no autoscroll on middle press
|
||||
e.stopPropagation();
|
||||
startPan(e, true);
|
||||
}, true);
|
||||
viewport.addEventListener('pointerdown', e => {
|
||||
if (e.button !== 0 || pinch || !canPan(e)) return;
|
||||
startPan(e, false);
|
||||
});
|
||||
|
||||
return { apply, toWorld, zoomTo };
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { panZoom } from './canvas-core.js';
|
||||
|
||||
const rect = () => ({ left: 0, top: 0, width: 800, height: 600 });
|
||||
const fakeElement = () => Object.assign(new EventTarget(), {
|
||||
style: { setProperty() {} },
|
||||
classList: { add() {}, remove() {}, toggle() {} },
|
||||
getBoundingClientRect: rect,
|
||||
setPointerCapture() {},
|
||||
});
|
||||
|
||||
/** A viewport whose presses never start a left-button pan (they land on cards). */
|
||||
function canvas() {
|
||||
const viewport = fakeElement();
|
||||
const view = { x: 0, y: 0, z: 1 };
|
||||
const ends = [];
|
||||
panZoom({ root: fakeElement(), viewport, world: { style: {} }, zoomLabel: {}, getView: () => view, canPan: () => false, onPanEnd: moved => ends.push(moved) });
|
||||
const fire = (type, props = {}) => {
|
||||
const event = Object.assign(new Event(type, { cancelable: true }), { pointerId: 1, pointerType: 'mouse', button: 0, clientX: 0, clientY: 0 }, props);
|
||||
viewport.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
||||
return { view, ends, fire };
|
||||
}
|
||||
|
||||
test('the middle mouse button pans from a card, and the release does not open a link', () => {
|
||||
const { view, ends, fire } = canvas();
|
||||
assert.equal(fire('pointerdown', { button: 1, clientX: 10, clientY: 10 }).defaultPrevented, true, 'no autoscroll');
|
||||
fire('pointermove', { button: 1, clientX: 60, clientY: 40 });
|
||||
fire('pointerup', { button: 1, clientX: 60, clientY: 40 });
|
||||
|
||||
assert.deepEqual({ x: view.x, y: view.y }, { x: 50, y: 30 });
|
||||
assert.deepEqual(ends, [], 'a middle pan does not close the side panel');
|
||||
assert.equal(fire('auxclick', { button: 1 }).defaultPrevented, true);
|
||||
});
|
||||
|
||||
test('a middle click without a move still opens the link under the pointer', () => {
|
||||
const { fire } = canvas();
|
||||
fire('pointerdown', { button: 1, clientX: 10, clientY: 10 });
|
||||
fire('pointerup', { button: 1, clientX: 10, clientY: 10 });
|
||||
|
||||
assert.equal(fire('auxclick', { button: 1 }).defaultPrevented, false);
|
||||
});
|
||||
|
||||
test('a left-button press on a card does not pan', () => {
|
||||
const { view, fire } = canvas();
|
||||
fire('pointerdown', { clientX: 10, clientY: 10 });
|
||||
fire('pointermove', { clientX: 60, clientY: 40 });
|
||||
fire('pointerup', { clientX: 60, clientY: 40 });
|
||||
|
||||
assert.deepEqual({ x: view.x, y: view.y }, { x: 0, y: 0 });
|
||||
});
|
||||
|
||||
test('a native drag (card logo, link) cannot start and cancel the card drag', () => {
|
||||
const { fire } = canvas();
|
||||
|
||||
assert.equal(fire('dragstart').defaultPrevented, true);
|
||||
});
|
||||
Reference in New Issue
Block a user