diff --git a/static/ui.js b/static/ui.js index b30e220f..3f500ac3 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1393,6 +1393,7 @@ function _mountMermaidViewer(svgEl, options = {}) { dragging: false, dragOriginX: 0, dragOriginY: 0, + dragPointerId: null, dragStartX: 0, dragStartY: 0, dragged: false, @@ -1477,7 +1478,9 @@ function _mountMermaidViewer(svgEl, options = {}) { const rect = viewport.getBoundingClientRect ? viewport.getBoundingClientRect() : {left: 0, top: 0}; const anchorX = Number.isFinite(e.clientX) ? e.clientX - rect.left : undefined; const anchorY = Number.isFinite(e.clientY) ? e.clientY - rect.top : undefined; - const factor = Math.exp((-(Number(e.deltaY) || 0)) * 0.0015); + const deltaMode = Number(e.deltaMode) || 0; + const lineScale = deltaMode === 1 ? 30 : deltaMode === 2 ? 600 : 1; + const factor = Math.exp((-(Number(e.deltaY) || 0)) * lineScale * 0.0015); _setScale(state.scale * factor, anchorX, anchorY); } @@ -1487,9 +1490,11 @@ function _mountMermaidViewer(svgEl, options = {}) { state.dragged = false; state.dragOriginX = Number(e.clientX) || 0; state.dragOriginY = Number(e.clientY) || 0; + state.dragPointerId = e.pointerId != null ? e.pointerId : null; state.dragStartX = state.x; state.dragStartY = state.y; viewport.classList.add('is-panning'); + if(state.dragPointerId != null && viewport.setPointerCapture) viewport.setPointerCapture(state.dragPointerId); if(e.preventDefault) e.preventDefault(); } @@ -1506,6 +1511,10 @@ function _mountMermaidViewer(svgEl, options = {}) { function _endPointerDrag(){ if(!state.dragging) return; state.dragging = false; + if(state.dragPointerId != null && viewport.releasePointerCapture){ + try{ viewport.releasePointerCapture(state.dragPointerId); }catch(_){} + } + state.dragPointerId = null; viewport.classList.remove('is-panning'); } diff --git a/tests/test_issue4814_mermaid_toolbar.py b/tests/test_issue4814_mermaid_toolbar.py index 4aaec7b8..c5b88c87 100644 --- a/tests/test_issue4814_mermaid_toolbar.py +++ b/tests/test_issue4814_mermaid_toolbar.py @@ -63,6 +63,8 @@ function makeElement(tagName) { onpointerleave: null, onwheel: null, classList: null, + capturedPointerId: null, + releasedPointerId: null, appendChild(child) { if (child.parentNode) child.parentNode.removeChild(child); this.children.push(child); @@ -98,6 +100,13 @@ function makeElement(tagName) { getBoundingClientRect() { return {left: 0, top: 0, width: this.clientWidth, height: this.clientHeight}; }, + setPointerCapture(pointerId) { + this.capturedPointerId = pointerId; + }, + releasePointerCapture(pointerId) { + this.releasedPointerId = pointerId; + if (this.capturedPointerId === pointerId) this.capturedPointerId = null; + }, cloneNode() { const copy = makeElement(this.tagName); copy.className = this.className; @@ -183,7 +192,6 @@ function runScenario(payload) { if (payload.scenario === 'toolbar') { result.hasInlineFullscreen = result.labels.includes('Fullscreen'); - result.hasLightboxFullscreen = result.labels.includes('Fullscreen'); return result; } @@ -202,7 +210,11 @@ function runScenario(payload) { const beforeWheel = {scale: state.scale, x: state.x, y: state.y}; state.viewport.onwheel({deltaY: -120, clientX: 240, clientY: 160, preventDefault() {}}); const afterWheel = {scale: state.scale, x: state.x, y: state.y}; - return {fitScale, zoomInScale, zoomOutScale, maxScale, reset, beforeWheel, afterWheel}; + state.fit(); + const beforeLineWheel = {scale: state.scale, x: state.x, y: state.y}; + state.viewport.onwheel({deltaY: -3, deltaMode: 1, clientX: 240, clientY: 160, preventDefault() {}}); + const afterLineWheel = {scale: state.scale, x: state.x, y: state.y}; + return {fitScale, zoomInScale, zoomOutScale, maxScale, reset, beforeWheel, afterWheel, beforeLineWheel, afterLineWheel}; } if (payload.scenario === 'drag') { @@ -217,11 +229,18 @@ function runScenario(payload) { interactiveState.viewport.getBoundingClientRect = () => ({left: 0, top: 0, width: interactiveState.viewport.clientWidth, height: interactiveState.viewport.clientHeight}); interactiveState.fit(); const start = {x: interactiveState.x, y: interactiveState.y, scale: interactiveState.scale}; - interactiveState.viewport.onpointerdown({button: 0, clientX: 100, clientY: 100, preventDefault() {}}); + interactiveState.viewport.onpointerdown({button: 0, clientX: 100, clientY: 100, pointerId: 7, preventDefault() {}}); interactiveState.viewport.onpointermove({clientX: 160, clientY: 150}); - interactiveState.viewport.onpointerup({preventDefault() {}}); + interactiveState.viewport.onpointerup({pointerId: 7, preventDefault() {}}); interactiveState.viewport.onclick({preventDefault() {}, stopPropagation() {}, target: interactiveState.viewport}); - const afterDrag = {x: interactiveState.x, y: interactiveState.y, dragged: interactiveState.dragged, opens: opens.length}; + const afterDrag = { + x: interactiveState.x, + y: interactiveState.y, + dragged: interactiveState.dragged, + opens: opens.length, + releasedPointerId: interactiveState.viewport.releasedPointerId, + capturedPointerId: interactiveState.viewport.capturedPointerId, + }; interactiveState.dragged = false; interactiveState.viewport.onclick({preventDefault() {}, stopPropagation() {}, target: interactiveState.viewport}); const afterClick = {opens: opens.length}; @@ -302,6 +321,7 @@ def test_zoom_fit_reset_and_wheel_update_state(_driver_path): assert result["reset"]["scale"] == 1 assert result["afterWheel"]["scale"] > result["beforeWheel"]["scale"] assert (result["afterWheel"]["x"], result["afterWheel"]["y"]) != (result["beforeWheel"]["x"], result["beforeWheel"]["y"]) + assert result["afterLineWheel"]["scale"] > result["beforeLineWheel"]["scale"] * 1.1 def test_drag_suppresses_accidental_lightbox_open(_driver_path): @@ -310,6 +330,8 @@ def test_drag_suppresses_accidental_lightbox_open(_driver_path): assert result["start"]["scale"] > 0 assert (result["afterDrag"]["x"], result["afterDrag"]["y"]) != (result["start"]["x"], result["start"]["y"]) assert result["afterDrag"]["opens"] == 0 + assert result["afterDrag"]["releasedPointerId"] == 7 + assert result["afterDrag"]["capturedPointerId"] is None assert result["afterClick"]["opens"] == 1