I’m implementing a feature where pressing Alt enables scrolling 5x faster, similar to the behavior in VSCode. Below is the relevant code snippet (full code can be found at CodePen):
/**
* Scrolling speed multiplier when pressing `Alt`.
* @type {number}
*/
const fastScrollSensitivity = 5;
function findScrollableElement(e, vertical = true, plus = true) {
// -- Omitted --
}
/**
* Handle the mousewheel event.
* @param {WheelEvent} e The WheelEvent.
*/
function onWheel(e) {
if (!e.altKey || e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) return;
e.preventDefault();
e.stopImmediatePropagation();
const { deltaY } = e;
const amplified = deltaY * fastScrollSensitivity;
const [vertical, plus] = [!e.shiftKey, e.deltaY > 0];
const el = findScrollableElement(e, vertical, plus);
Object.assign(lastScroll, { time: e.timeStamp, el, vertical, plus });
el.scrollBy({
top: e.shiftKey ? 0 : amplified,
left: e.shiftKey ? amplified : 0,
behavior: "instant"
});
// TODO: Smooth scrolling
}
/**
* Enable or disable the fast scroll feature.
* @param {boolean} enabled Whether the fast scroll feature is enabled.
*/
function fastScroll(enabled) {
if (enabled) {
document.addEventListener("wheel", onWheel, { capture: false, passive: false });
} else {
document.removeEventListener("wheel", onWheel, { capture: false, passive: false });
}
}
The feature works as expected when using behavior: "instant"
in scrollBy
. However, switching to behavior: "smooth"
causes issues.
The issue arises when I use behavior: "smooth"
. If scrollBy
is called repeatedly before the previous animation ends, the subsequent call seems to interrupt the ongoing animation, leading to inconsistent behavior and slower scrolling speeds.
scrollBy
calls instead of cancelling previous ones?I prefer having the browser handle the smooth animations rather than manually calculating and animating via JavaScript.
Using scrollBy
with behavior: "smooth"
Accessing scrollTop
Smooth scrolling with requestAnimationFrame
(considered but not implemented)
Instead of using scrollBy
(which doesn't have as good support on old browsers as scrollTo
anyway), you can use scrollTo
and manually keep track of the target coordinates by adding each deltaY
. Like so (full edited example on CodePen)
const lastScroll = {
targetX: -1, // keep track of target coordinates
targetY: -1, // keep track of target coordinates
time: 0,
el: document.scrollingElement,
vertical: true,
plus: true,
};
// ...
function onWheel(e) {
if (!e.altKey || e.deltaMode !== WheelEvent.DOM_DELTA_PIXEL) {
reset();
return;
}
// ... as before
let targetX = lastScroll.targetX >= 0 ? lastScroll.targetX : el.scrollLeft;
targetX += e.shiftKey ? amplified : 0;
let targetY = lastScroll.targetY >= 0 ? lastScroll.targetY : el.scrollTop;
targetY += e.shiftKey ? 0 : amplified;
// set boundaries
targetX = Math.max(0, Math.min(targetX, el.scrollWidth - el.clientWidth));
targetY = Math.max(0, Math.min(targetY, el.scrollHeight - el.clientHeight));
Object.assign(lastScroll, {
targetX,
targetY,
time: e.timeStamp,
el,
vertical,
plus,
});
el.scrollTo({
top: targetY,
left: targetX,
behavior: "smooth",
});
}
function reset() {
Object.assign(lastScroll, {
targetX: -1,
targetY: -1,
time: 0,
el: document.scrollingElement,
vertical: true,
plus: true
})
}