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

5

12

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.

Spearman answered 6/7, 2012 at 3:55 Comment(10)
Isn't the repeat rate set by the operating system? (E.g., in Windows this can be set via Control Panel / Keyboard Properties.) And so wouldn't users expect the keys to repeat at that rate? (And is it just a typo in the question that you've got keyup twice rather than keyup and keydown?)Vaish
Correct, it is browser/OS-specific. I am trying to create consistency and, for my application specifically, I want to manually set the rate at which it repeats. Also, thanks for the heads up on the typo, @nnnnnn.Spearman
Is there a reason why you're not using the code from the SO post you referenced?Kaila
@JefferyTo the primary reason is because I want to write it myself so I can understand it better and be able to modify as my application grows, without relying on someone else's idea. It's a personal preference that stems from the need to know why and how things work. I also don't get that same sensation of "YES! I GOT IT!" when I just plop someone else's code in. I'll refer to others for ideas and direction, but I won't blatantly copy someone else's logic, especially when it wasn't designed specifically for my application.Spearman
@MrSlayer I'm not suggesting anyone should use anyone else's code without first understanding what it does. I asked because bobince's code is well written, and I was wondering if / why it wasn't working for you. (bobince explains what his code does in the second paragraph.)Kaila
@JeffreyTo It simply did more than I needed it to at this time. So I tried to simplify it and integrate it into what I had already written. The main problem I was facing was that for my application, keyboard functionality changes frequently. The ...keyboard logic part handles various application states, where key mappings change. In hindsight, I probably should have tried harder with bobince's, before asking this question. The code in the question was my attempt at trying, though, and I didn't want to give up on it.Spearman
On the bright side, it also appears that AaditMShah may have come up with a solution that doesn't require the use of timers. I don't know if there's any major performance benefit to that.Spearman
@MrSlayer - Actually I do use timers. However unlike the setInterval function my Delta Timer fires more accurately. Hence the 1000 ms interval you are expecting is always maintained. In addition my code fires multiple events for different keys pressed at the same time. All of them maintain a constant interval. The last time I checked it was able to handle four keys pressed at the same time. However that number is OS specific. I believe being able to handle 4 keys pressed at the same time is good enough for any application. You don't want users to spam keys.Rajiv
@MrSlayer - Seeing that your bounty is ending tomorrow would you mind selecting an answer please?Rajiv
@AaditMShah Thanks for the friendly reminder.Spearman
S
5

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.

Spearman answered 8/7, 2012 at 14:37 Comment(2)
This is what I would have suggested and is how many race conditions are handled - via a semaphore ( a spinning lock in this case where you are spinning the boolean implemented variable repeatRateTimer ).Gownsman
@TravisJ, thanks for the introduction to semaphore.Spearman
R
5

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.

Rajiv answered 6/7, 2012 at 4:54 Comment(3)
Cheers for the idea and info. I've never used Lambda JS before, and I'd rather not have to; but, it is something to fall back on or refer to for ideas on how to write my own.Spearman
@MrSlayer - I edited my answer after reading your answer, and I managed to improve upon your code. =)Rajiv
Very well, @AaditMShah. I will certainly be taking a deeper look at this over the next few days. I greatly appreciate your effort and continued interest.Spearman
S
5

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.

Spearman answered 8/7, 2012 at 14:37 Comment(2)
This is what I would have suggested and is how many race conditions are handled - via a semaphore ( a spinning lock in this case where you are spinning the boolean implemented variable repeatRateTimer ).Gownsman
@TravisJ, thanks for the introduction to semaphore.Spearman
T
2

How about you make custom key events. you can listen to the original ones (keyup/keydown) and if they pass the time condition, you trigger your custom event.
This way has the benefit that you do not rely on timers and it gives you more power, because you use custom events (btw, you can skip the cancel event part if you wish).
Here's a demo to see what I'm talking about : http://jsfiddle.net/gion_13/gxEMz/
And the basic code looks something like this :

$(document).ready(function(){
    var dispatcher = $(window),
        keyRate = 1000, //ms
        lastKeyEvent = 0,
        cancelEvent = function(e){
            var evt = e ? e:window.event;
            if(evt.stopPropagation)    
                evt.stopPropagation();
            if(evt.cancelBubble!=null) 
                evt.cancelBubble = true;
            return false;
        };

    dispatcher
        .bind('keydown',function(e){
            var now = new Date().getTime();
            if(now - lastKeyEvent <= keyRate)
                // cancel the event
                return cancelEvent(e);
            var keyEventsTimeDiff = now - lastKeyEvent;
            lastKeyEvent = now;
            dispatcher.trigger('special-keydown',[e,keyEventsTimeDiff ]);
        })
        .bind('keyup',function(e){
            cancelEvent(e);
            dispatcher.trigger('special-keyup',[e]);
        })
        // binding the custom events
        .bind('special-keydown',function(e,keyEventsTimeDiff){
            console.log(e,'special keydown triggered again after ' + keyEventsTimeDiff +'ms');
        })
       .bind('special-keyup',function(e,keyEventsTimeDiff){
            console.log(e,'special keyup');
        });
});
Telegu answered 8/7, 2012 at 14:44 Comment(0)
J
0

It is an old post but I want to share my current answer with rxjs. I had to do a Tetris game in Javascript and I wanted to change the keydown repeat delay to have a better gameplay.

Indeed, we can't override the real keydown repeat delay property which is specific to the OS. However we can simulate it thanks to the keyup and keydown events.

Finally, I came up with this (typescript) :

function keyDown$(key: KeyboardKey): Observable<KeyboardEvent> {
  return fromEvent<KeyboardEvent>(document, 'keydown').pipe(filter(event => event.key === key));
}

function keyUp$(key: KeyboardKey): Observable<KeyboardEvent> {
  return fromEvent<KeyboardEvent>(document, 'keyup').pipe(filter(event => event.key === key));
}

export function keyPressed$(key: KeyboardKey): Observable<KeyboardEvent> {
  return merge(keyDown$(key), keyUp$(key)).pipe(
    filter(event => event.repeat === false),
    switchMap(event => (event.type === 'keydown' ? merge(of(event), timer(150, 50).pipe(mapTo(event))) : NEVER))
  );
}

...

keyPressed$('ArrowRight').subscribe({
  next: (event) => {
    ...
  }
});

JS version:

function keyDown$(key) {
  return fromEvent(document, 'keydown').pipe(filter(event => event.key === key));
}

function keyUp$(key) {
  return fromEvent(document, 'keyup').pipe(filter(event => event.key === key));
}

export function keyPressed$(key) {
  return merge(keyDown$(key), keyUp$(key)).pipe(
    filter(event => event.repeat === false),
    switchMap(event => (event.type === 'keydown' ? merge(of(event), timer(150, 50).pipe(mapTo(event))) : NEVER))
  );
}

...

keyPressed$('ArrowRight').subscribe({
  next: (event) => {
    ...
  }
});

The keyPressed$ function take a keyboard key as argument and return an observable. This observable emits immediately when the corresponding key is pressed then wait for 150ms and will emit each 50ms.

You can change these values for your case in the timer function.

Don't forget to unsubscribe from the observable.

Janijania answered 12/4, 2021 at 8:21 Comment(0)
I
0

I will add to this as well. I needed to delay the keydown repeat events so the UI could manage the updates before making a final AJAX request.

// Example of 3 delays
const delayedKeydownEvents = makeDelays(600, 100, 300);

// 3 callbacks to be triggered
document.body.addEventListener('keydown', () => {
    delayedKeydownEvents(
        () => {
            console.log('REQUIRED: Called after 600 ms from when the last event was triggered.');
        },
        () => {
            console.log('OPTIONAL: Called each 100 ms');
        },
        () => {
            console.log('...OPTIONAL: Called each 300 ms');
        }
    );
});


/**
 * Takes n-th amount of delays and expects equivalent number of callback functions to be declared.
 * The first callback (required) is executed when the timer expires after param1 ms delay.
 */
function makeDelays() {
    const [completeDelay, ...intermediateDelays] = arguments;
    let lastRuns = new Array(intermediateDelays.length).fill(0);
    let timeoutID;

    return function() {
        clearTimeout(timeoutID);
        const [completeCallback, ...intermediateCallbacks] = arguments;
        const now = Date.now();
        intermediateCallbacks.forEach((intermediateCallback, i) => {
            if (now - lastRuns[i] >= intermediateDelays[i]) {
                lastRuns[i] = now;
                intermediateCallback();
            }
        });

        timeoutID = setTimeout(() => {
            lastRuns = new Array(intermediateDelays.length).fill(0);
            completeCallback();
        }, completeDelay);
    }
};
Income answered 6/10, 2022 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.