From 0403f00789dfa4f8308fb8fa55125d26c5464ab5 Mon Sep 17 00:00:00 2001 From: silent-reader-cn Date: Sat, 11 Jul 2026 10:44:46 +0800 Subject: [PATCH] fix: address Greptile review and fix CI test mock for touch handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes in one commit: 1. **CI test mock** — Added addEventListener/removeEventListener to the Node.js makeElement mock so viewport.addEventListener() calls in the touch handler registration don't crash with 'not a function'. 2. **Two-finger pan during pinch** — Replaced _setScale() call with direct transform math. The new formula computes scale and position simultaneously: ratio = boundedScale / pinchStartScale x = currMidX - (startMidX - startPosX) * ratio This keeps the diagram under both fingers regardless of midpoint drift, enabling simultaneous zoom + pan. 3. **Post-pinch click suppression** — _onTouchEnd now sets dragged=true when exiting pinch mode so any synthesized click event is caught by the existing dragged guard in _openViewerOnClick instead of opening the lightbox. --- static/ui.js | 28 +++++++++++++++++++------ tests/test_issue4814_mermaid_toolbar.py | 9 ++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/static/ui.js b/static/ui.js index 7850aa0c..f4d66e18 100644 --- a/static/ui.js +++ b/static/ui.js @@ -2016,6 +2016,10 @@ function _mountMermaidViewer(svgEl, options = {}) { pinching: false, pinchStartDist: 0, pinchStartScale: 1, + pinchStartCX: 0, + pinchStartCY: 0, + pinchStartX: 0, + pinchStartY: 0, }; root._mermaidViewer = state; @@ -2219,6 +2223,11 @@ function _mountMermaidViewer(svgEl, options = {}) { state.pinching = true; state.pinchStartDist = _touchDist(e.touches); state.pinchStartScale = state.scale; + state.pinchStartX = state.x; + state.pinchStartY = state.y; + const rect = viewport.getBoundingClientRect(); + state.pinchStartCX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - (rect.left || 0); + state.pinchStartCY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - (rect.top || 0); _endPointerDrag(); if(e.preventDefault) e.preventDefault(); } @@ -2226,19 +2235,26 @@ function _mountMermaidViewer(svgEl, options = {}) { function _onTouchMove(e){ if(!state.pinching || e.touches.length < 2) return; + const rect = viewport.getBoundingClientRect(); + const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - (rect.left || 0); + const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2 - (rect.top || 0); const currDist = _touchDist(e.touches); - if(state.pinchStartDist > 0){ - const rect = viewport.getBoundingClientRect(); - const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - (rect.left || 0); - const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2 - (rect.top || 0); - _setScale(state.pinchStartScale * (currDist / state.pinchStartDist), cx, cy); + if(state.pinchStartDist > 0 && state.pinchStartScale > 0){ + const rawScale = state.pinchStartScale * (currDist / state.pinchStartDist); + const boundedScale = Math.max(_minScale(), Math.min(_MERMAID_VIEWER_MAX_SCALE, rawScale)); + const ratio = boundedScale / state.pinchStartScale; + state.scale = boundedScale; + state.x = cx - (state.pinchStartCX - state.pinchStartX) * ratio; + state.y = cy - (state.pinchStartCY - state.pinchStartY) * ratio; + _applyTransform(); } if(e.preventDefault) e.preventDefault(); } function _onTouchEnd(e){ - if(e.touches.length < 2){ + if(e.touches.length < 2 && state.pinching){ state.pinching = false; + state.dragged = true; } } diff --git a/tests/test_issue4814_mermaid_toolbar.py b/tests/test_issue4814_mermaid_toolbar.py index 896f9e4c..159fff9d 100644 --- a/tests/test_issue4814_mermaid_toolbar.py +++ b/tests/test_issue4814_mermaid_toolbar.py @@ -113,6 +113,15 @@ function makeElement(tagName) { this.releasedPointerId = pointerId; if (this.capturedPointerId === pointerId) this.capturedPointerId = null; }, + _listeners: {}, + addEventListener(type, handler, _options) { + (this._listeners[type] ||= []).push(handler); + }, + removeEventListener(type, handler) { + const list = this._listeners[type] || []; + const idx = list.indexOf(handler); + if (idx >= 0) list.splice(idx, 1); + }, cloneNode() { const copy = makeElement(this.tagName); copy.className = this.className;