From 4b6c2592619b2d8890c4225603e402b2034bc7c4 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Mon, 19 Oct 2026 14:12:37 +0200 Subject: [PATCH 1/2] fix(editor): scroll and zoom the mind map with the wheel in Firefox The editor pans on the non-standard mousewheel event and zooms on it with Ctrl, but kity only listens for an event that elements have an on property for, and Firefox has neither onmousewheel nor fires mousewheel. So wheel scrolling and Ctrl+wheel zoom did nothing in Firefox. Where mousewheel is missing, give elements the property and re-dispatch each wheel event as mousewheel with the deltas Chrome reports. Fixes #93 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- src/viewer.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/viewer.js b/src/viewer.js index c3fea97..3e012d8 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -25,7 +25,36 @@ function redirectIfNotDisplayedInFrame() { window.location.href = '/' } -redirectIfNotDisplayedInFrame(); +redirectIfNotDisplayedInFrame() + +/** + * The editor pans and zooms on the non-standard mousewheel event, which Firefox + * does not fire. Where it is missing, re-dispatch each wheel event as + * mousewheel with the deltas Chrome reports: 120 per wheel notch of three lines. + */ +function polyfillMouseWheel() { + if ('onmousewheel' in window) { + return + } + // kity only listens for an event an element has an on property for + Object.defineProperty(Element.prototype, 'onmousewheel', { value: null, writable: true, configurable: true }) + window.addEventListener('wheel', (event) => { + // Read deltaMode first: reading a delta first makes Firefox report pixels + const pixelsPerUnit = [1.2, 40, 800][event.deltaMode] + const wheelDeltaX = -event.deltaX * pixelsPerUnit + const wheelDeltaY = -event.deltaY * pixelsPerUnit + const mouseWheel = new WheelEvent('mousewheel', event) + Object.defineProperties(mouseWheel, { + wheelDelta: { value: wheelDeltaY || wheelDeltaX }, + wheelDeltaX: { value: wheelDeltaX }, + wheelDeltaY: { value: wheelDeltaY }, + }) + if (!event.target.dispatchEvent(mouseWheel)) { + event.preventDefault() + } + }, { capture: true, passive: false }) +} +polyfillMouseWheel(); (function() { const t = function(msg) { From cb780ced25335524b2901c926059095e723d9a64 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Mon, 19 Oct 2026 15:08:17 +0200 Subject: [PATCH 2/2] feat(editor): scroll horizontally with Shift+wheel, zoom on small Ctrl steps The editor ignored the wheel while Shift was held, and zoomed with Ctrl or Cmd only once a single event reported more than 100, which touchpad pinches and the small pixel steps of macOS never do. Re-dispatch the wheel as mousewheel in Chrome too where needed: with Shift as horizontal scrolling, and with Ctrl or Cmd with at least 120. The editor still zooms once per gesture, as it waits for 100 ms of quiet. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- src/viewer.js | 75 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/viewer.js b/src/viewer.js index 3e012d8..f1f3345 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -28,33 +28,72 @@ function redirectIfNotDisplayedInFrame() { redirectIfNotDisplayedInFrame() /** - * The editor pans and zooms on the non-standard mousewheel event, which Firefox - * does not fire. Where it is missing, re-dispatch each wheel event as - * mousewheel with the deltas Chrome reports: 120 per wheel notch of three lines. + * The editor pans on the non-standard mousewheel event, ignoring it while Shift + * is held, and zooms on it with Ctrl or Cmd once it reports more than 100. + * Re-dispatch wheel events as mousewheel where needed, with the deltas Chrome + * reports for a mouse notch of three lines (120): + * - in Firefox, which has no mousewheel at all, + * - with Shift, so that it scrolls horizontally, + * - with Ctrl or Cmd, at least 120, so that touchpad pinches and small wheel + * steps zoom too; the editor zooms once per gesture, after 100 ms of quiet. */ -function polyfillMouseWheel() { - if ('onmousewheel' in window) { - return +function normalizeMouseWheel() { + const nativeMouseWheel = 'onmousewheel' in window + if (!nativeMouseWheel) { + // kity only listens for an event an element has an on property for + Object.defineProperty(Element.prototype, 'onmousewheel', { value: null, writable: true, configurable: true }) } - // kity only listens for an event an element has an on property for - Object.defineProperty(Element.prototype, 'onmousewheel', { value: null, writable: true, configurable: true }) - window.addEventListener('wheel', (event) => { - // Read deltaMode first: reading a delta first makes Firefox report pixels - const pixelsPerUnit = [1.2, 40, 800][event.deltaMode] - const wheelDeltaX = -event.deltaX * pixelsPerUnit - const wheelDeltaY = -event.deltaY * pixelsPerUnit - const mouseWheel = new WheelEvent('mousewheel', event) + const redispatched = new WeakSet() + window.addEventListener(nativeMouseWheel ? 'mousewheel' : 'wheel', (event) => { + if (redispatched.has(event)) { + return + } + const zoom = event.ctrlKey || event.metaKey + const horizontal = event.shiftKey && !zoom + let deltaX = event.wheelDeltaX + let deltaY = event.wheelDeltaY + if (!nativeMouseWheel) { + // Read deltaMode first: reading a delta first makes Firefox report pixels + const pixelsPerUnit = [1.2, 40, 800][event.deltaMode] + deltaX = -event.deltaX * pixelsPerUnit + deltaY = -event.deltaY * pixelsPerUnit + } else if (!horizontal && !(zoom && Math.abs(deltaY || deltaX) < 120)) { + return + } + if (horizontal) { + deltaX = deltaX || deltaY + deltaY = 0 + } + let delta = deltaY || deltaX + if (zoom) { + delta = Math.sign(delta) * Math.max(120, Math.abs(delta)) + } + const mouseWheel = new WheelEvent('mousewheel', { + bubbles: true, + cancelable: true, + view: window, + screenX: event.screenX, + screenY: event.screenY, + clientX: event.clientX, + clientY: event.clientY, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + shiftKey: event.shiftKey && !horizontal, + }) Object.defineProperties(mouseWheel, { - wheelDelta: { value: wheelDeltaY || wheelDeltaX }, - wheelDeltaX: { value: wheelDeltaX }, - wheelDeltaY: { value: wheelDeltaY }, + wheelDelta: { value: delta }, + wheelDeltaX: { value: deltaX }, + wheelDeltaY: { value: deltaY }, }) + redispatched.add(mouseWheel) + event.stopImmediatePropagation() if (!event.target.dispatchEvent(mouseWheel)) { event.preventDefault() } }, { capture: true, passive: false }) } -polyfillMouseWheel(); +normalizeMouseWheel(); (function() { const t = function(msg) {