Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to override the keydown repeat delay, in JavaScript?

The objective is manually set a held key's "repeat rate".

For example, when in a text box and pressing and holding the X key, I understand that there is browser-specific ways of repeating the pressed character. In some, it pauses, then continuously triggers the pressed key. In others, it doesn't repeat at all. I want to mitigate this by forcing the pressed key to repeat at a specific interval, regardless of browser.

Through research, I've come up with a timer-based attempt, but in Safari, it does not repeat the character. I've got a menu system where holding down arrow scrolls through the list, but the translation animation and repeat rate don't like each other.

var repeating = false;
var repeatRateTimer = null;

$( document ).bind( 'keyup', function( input ) {
    if( repeatRateTimer != null )
    {
        clearTimeout( repeatRateTimer );
        repeatRateTimer = null;
    }

    repeating = false;
} );

$( document ).bind( 'keydown', function( input ) {
    input.preventDefault( );

    if( repeating == true )
    {
        if( repeatRateTimer != null )
        {
            clearTimeout( repeatRateTimer );
            repeatRateTimer = null;
        }
        else
        {
            repeatRateTimer = setTimeout( function( ){ repeating = false; }, 1000 );
        }

        return;
    }
    repeating = true;

    // ...keyboard logic
} );

I may have botched this whole thing up...I tried to recreate a simplified version of this SO post. However, I feel there has to be a better way of doing this. Any thoughts?

Update:

We can assume that the end-user hasn't set their OS keyboard repeat rate greater than the rate I want to use (1000ms). If it is, then it should fall back to their repeat rate, since it won't keep triggering the key press event. If it isn't (more likely since most people don't modify that), then we would be overriding that behavior to make it delay our specified period.

like image 929
Jeff Jenkins Avatar asked Jul 06 '12 03:07

Jeff Jenkins


2 Answers

Look at the following JavaScript file. If you scroll down to line 530 you will find the following class:

var Keyboard = new Class(function (constructor) {
    var key = {};

    var eventListener = {
        keyup: {},
        keydown: {},
        keypress: {}
    };

    constructor.overload(["Number"], function (interval) {
        setInterval(keypress, interval);
    });

    window.addEventListener("keyup", keyup, false);
    window.addEventListener("keydown", keydown, false);

    function keyup(event) {
        var keyCode = event.keyCode;
        var listener = eventListener.keyup[keyCode];
        key[keyCode] = false;
        if (listener)
        listener();
    }

    function keydown(event) {
        var keyCode = event.keyCode;
        var listener = eventListener.keydown[keyCode];
        key[keyCode] = true;
        if (listener)
        listener();
    }

    function keypress() {
        for (var code in key) {
            var listener = eventListener.keypress[code];
            if (key[code] && listener) listener();
        }
    }

    this.addEventListener = new Dispatcher(["String", "Number", "Function"], function (type, keyCode, listener) {
        type = eventListener[type];
        if (type) type[keyCode] = listener;
        else throw new Error("Unexpected value for type.");
    });
});

What the author has done is that he has created a special Keyboard class for delegating the key events: keyup, keydown and keypress. The class has only one constructor which accepts a single argument - the interval of the keypress event (which is what you want). You can add event listeners using the addEventListener method of the instance of the Keyboard class:

var keyboard = new Keyboard(125); // fire key press 8 times a second.

keypress.addEventListener("keypress", 65, function () {
    // do something every time A is pressed
});

Note that the above class depends on the following framework: Lambda JS. You can see a working demo of the above script here. Hope this helps.

Update 1:

Your code does not work in Opera. In addition the second event fires after a extra 500 ms delay in Firefox and consecutive events do not maintain the same interval. Plus it can't handle multiple key events at the same time. Let's rectify this problem:

First we need to create a simple script for Delta Timing so that the key events fire after constant interval. We use the following snippet for creating a DeltaTimer:

function DeltaTimer(render, interval) {
    var timeout;
    var lastTime;

    this.start = start;
    this.stop = stop;

    function start() {
        timeout = setTimeout(loop, 0);
        lastTime = Date.now();
        return lastTime;
    }

    function stop() {
        clearTimeout(timeout);
        return lastTime;
    }

    function loop() {
        var thisTime = Date.now();
        var deltaTime = thisTime - lastTime;
        var delay = Math.max(interval - deltaTime, 0);
        timeout = setTimeout(loop, delay);
        lastTime = thisTime + delay;
        render(thisTime);
    }
}

Next we write the logic to fire custom keypressed events. We need custom events since we must be able to handle multiple keys at the same time:

(function (interval) {
    var keyboard = {};

    window.addEventListener("keyup", keyup, false);
    window.addEventListener("keydown", keydown, false);

    function keyup(event) {
        keyboard[event.keyCode].pressed = false;
    }

    function keydown(event) {
        var keyCode = event.keyCode;
        var key = keyboard[keyCode];

        if (key) {
            if (!key.start)
                key.start = key.timer.start();
            key.pressed = true;
        } else {
            var timer = new DeltaTimer(function (time) {
                if (key.pressed) {
                    var event = document.createEvent("Event");
                    event.initEvent("keypressed", true, true);
                    event.time = time - key.start;
                    event.keyCode = keyCode;
                    window.dispatchEvent(event);
                } else {
                    key.start = 0;
                    timer.stop();
                }
            }, interval);

            key = keyboard[keyCode] = {
                pressed: true,
                timer: timer
            };

            key.start = timer.start();
        }
    }
})(1000);

The interval is set at 1000 ms but you may change that. Finally to register an event we do:

window.addEventListener("keypressed", function (event) {
    document.body.innerHTML += event.keyCode + " (" + event.time + " ms)<br/>";
}, false);

This is simple and efficient JavaScript. No jQuery required. You can see the live demo here, and see the difference between your script and mine. Cheers.

Update 2:

Looking at the other question on StackOverflow, this is how you would implement it using the above pattern:

window.addEventListener("keypressed", function (event) {
    switch (event.keyCode) {
    case 37:
        Move(-1, 0);
        break;
    case 38:
        Move(0, -1);
        break;
    case 39:
        Move(1, 0);
        break;
    case 40:
        Move(0, 1);
        break;
    }
}, false);

Using the above code will remove the short delay you're experiencing and also allow multiple events to be fired for different keys at the same time.

like image 124
Aadit M Shah Avatar answered Oct 11 '22 13:10

Aadit M Shah


Well, I figured out why my example wasn't looping. In the keydown loop, it was clearing the timeout before it expired:

if( repeatRateTimer != null )
{
    clearTimeout( repeatRateTimer );
    repeatRateTimer = null;
}
else
{
    repeatRateTimer = setTimeout( function( ){ repeating = false; }, 1000 );
}

The timeout should be cleared only after it expires, so the if condition needed to be moved into the timeout function:

if( repeatRateTimer == null )
{
    repeatRateTimer = setTimeout( function( ) {
        repeating = false;
        clearTimeout( repeatRateTimer );
        repeatRateTimer = null;
    }, 1000 );
}

I'll leave this bounty open in case someone can improve upon this, or provide a better alternative.

like image 34
Jeff Jenkins Avatar answered Oct 11 '22 12:10

Jeff Jenkins