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.
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.
Share Improve this question edited May 23, 2017 at 11:47 CommunityBot 11 silver badge asked Jul 6, 2012 at 3:55 jeffjenxjeffjenx 17.5k6 gold badges62 silver badges103 bronze badges 10 | Show 5 more comments5 Answers
Reset to default 5 +50Look 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.
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.
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');
});
});
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.
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);
}
};
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. – Aadit M Shah Commented Jul 11, 2012 at 9:31