Search code examples
javascriptscrolldom-eventskeyboard-eventssmooth-scrolling

Having Trouble Implementing Vim-like Hotkeys


I'm trying to implement Vim-like hotkeys for my project; first and foremost, I've decided to realise j and k bindings, so I've wrote this KeyboardEvent handler:

window.onkeydown = function( event ) {
    if (event.code === 'KeyJ')
        window.scrollBy({ top: 128, behavior: 'smooth' });

    else if (event.code === 'KeyK')
        window.scrollBy({ top: -128, behavior: 'smooth' });
};

Seems legit, right? Actually — no: when I holding j or k the scrolling process becomes disjointed and teared (also it's seems like the scroll speed is cutted in a half, like if 64 would be added to scrollTop instead of 128).

Can I somehow directly map k to and j to using plain JavaScript?


I've tried this solution:

const ArrowUp = new KeyboardEvent( 'keydown', { code: 'ArrowUp' } );
const ArrowDown = new KeyboardEvent( 'keydown', { code: 'ArrowDown' } );

window.onkeydown = function( event ) {
    if (event.code === 'KeyJ')
        window.dispatchEvent( ArrowDown );

    else if (event.code === 'KeyK')
        window.dispatchEvent( ArrowUp );
};

But it doesn't work at all, I do not even get any errors .


Solution

  • I have two solutions. This first solution is a bit... let's just say it's not great. You can force it to only listen to the events every so often to prevent the glitchy-ness.

    var lastScrollTime = Date.now();
    window.onkeydown = function( event ) {
        var d = Date.now();
        if(d - lastScrollTime < 64){
            return;
        }
        lastScrollTime = d;
        if (event.code === 'KeyJ')
            window.scrollBy({ top: 128, behavior: 'smooth' });
    
        else if (event.code === 'KeyK')
            window.scrollBy({ top: -128, behavior: 'smooth' });
    };
    

    My second solution is a bit better by still by no means great. You could implement your own smooth scrolling system.

    var scrollTo = 0;
    function lerpTo(){
        var newPt = lerp(scrollTo, window.scrollY, 1.1);
        console.log(newPt);
        window.scrollBy(0, window.scrollY - newPt);
    }
    function lerp(v0, v1, t) {
        return v0*(1-t)+v1*t
    }
    setInterval(lerpTo, 20);
    

    Then with that, all you have to do is change 'scrollTo' whenever you press j or k. Now that code used a lerp function. If you wanted more consistent motion you could change it out for a fixed amount. The big downside to that one is you can no longer scroll normally. To re-add normal scrolling you would have to listen to that event and set scroll to again, however I think that is a bad solution. Alternatively, to add normal scrolling again you could disable the lerp code whenever you scroll manually, then re-enable it whenever someone presses j or k.