Tell if shift is being held in onClick in React - javascript

I need to fire an onClick only if meta(mac) / ctrl(win) is being held when it's clicked.
Here's what I tried:
const [shiftOn, setShiftOn] = useState(false)
useEffect(() => {
document.addEventListener('keydown', (e) => {
e.preventDefault()
if ((e.metaKey || e.ctrlKey) && e.code === 'KeyC') {
setShiftOn(true)
}
})
})
useEffect(() => {
document.addEventListener('keyup', (e) => {
e.preventDefault()
if ((e.metaKey || e.ctrlKey) && e.code === 'KeyC') {
setShiftOn(false)
}
})
})
const highlightCol = () => {
console.log(shiftOn) // always false
if (shiftOn) ... do something
}
const col = (
<td onClick={highlightCol} {...tdProps}>
{colName}
</td>

You're almost there -- metaKey, shiftKey, etc are what you're looking for -- but you're looking for them in the wrong place: those will simply be boolean properties on the click event itself. You do not need to check for separate keydown or keyup events on the shift or cmd keys, so can delete everything you have in a useEffect.
All you need is the single click handler:
const highlightCol = e => {
if (e.shiftKey) {
// shift key was down during the click
}
if (e.ctrlKey) {
// ctrl key was down during the click
}
}
The onClick will always be fired; just check its event's shiftKey (or metaKey or altKey or ctrlKey) properties to decide whether to do anything in it.

Different browser can have different implementations of detecting pressed keys.
I think the problem here is that you detect ctrl/meta key incorrectly.
Try to look into this service to find out what ways you have to detect particular key.
Below is what it shows for me when I press ctrl key on win.
It is better to use several methods of detecting keys to cover all browsers and platforms.

You may wrong in the way detect control key. By the way, just register event listener 1 time.
const Component =(props)=>{
const [valueFromChild, setValueFromChild] = useState('');
const [shiftOn, setShiftOn] = useState(false)
useEffect(() => {
document.addEventListener('keydown', (e) => {
console.log('dow', e);
e.preventDefault()
if (e.key === 'Control') {
setShiftOn(true)
}
})
}, [])
useEffect(() => {
document.addEventListener('keyup', (e) => {
console.log('up', e);
e.preventDefault()
if (e.key === 'Control') {
setShiftOn(false)
}
})
}, [])
const highlightCol = () => {
console.log(shiftOn) // always false
if (shiftOn) {
}
}
return <>
{shiftOn ? 'on' : 'off'}
<button onClick={highlightCol} > Click </button>
</>
}

Related

useCallback returns n+1 times where n is the number of state variables

I'm trying to set up a custom context menu, however whenever the user right clicks the context menu function returns 6 separate times, the 5th being what I need and the 6th being the default state values. However if the user double right-clicks in the same spot it returns 5 times, with the 5th return being the desired values and the menu opens. Is there a way to check before the return if all the states are changed and only return from the callback if all the needed information is present?
const ContextMenu = outerRef => {
const [xPos, setXPos] = useState("0px");
const [yPos, setYPos] = useState("0px");
const [menu, showMenu] = useState(false);
const [menuTarget, setMenuTarget] = useState('');
const [menuTargetId, setMenuTargetId] = useState('');
const handleContextMenu = useCallback(
event => {
if(event.target.className && (event.target.className.includes('bar') ||event.target.className == 'timeline' || event.target.className == 'draggablediv' || event.target.className == 'editableDiv')){
event.preventDefault();
if (outerRef && outerRef.current.contains(event.target)) {
setXPos(`${event.pageX}px`);
setYPos(`${event.pageY}px`);
setMenuTarget(event.target.className)
setMenuTargetId(event.target.id)
showMenu(true);
} else {
showMenu(false);
}
}
},[showMenu, outerRef, setXPos, setYPos]);
const handleClick = useCallback(() => {
showMenu(false);
}, [showMenu]);
useEffect(() => {
document.addEventListener("click", handleClick);
document.addEventListener("contextmenu", handleContextMenu);
return () => {
document.removeEventListener("click", handleClick);
document.removeEventListener("contextmenu", handleContextMenu);
};
}, []);
return {xPos, yPos, menu, menuTarget, menuTargetId};
};
useCallback accepts a function that returns the memoized value. Like useCallback(() => 5, []), or in your case useCallback(() => event => {...}, []).
This is because React has to call the function to get the value to memoize, so your setters are being called on every render. Which is what's causing all the weirdness.
However, even with that fix I don't think it's correct to use a function that changes references in addEventListener. You will never have up-to-date values in your contextmenu handler because it will refer to an old version of handleContextMenu. You will probably have to use a more idiomatic way of attaching a function to a UI event than the global document api.

Use Javascript onclick or keydown for event

I'm using JavaScript and CSS to create keys that flips and play a sound. On the front side there is an image then when the key is pressed, it plays a sound and flips to revels the back which displays a different img and flips back over after the key is released. The code works for that purpose, but I want the same functionality for when someone also clicks the key. Can I have both?
function removeTransition(e) {
if (e.propertyName !== 'transform') return;
e.target.classList.remove('playing');
}
function playSound(e) {
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
const key = document.querySelector(`div[data-key="${e.keyCode}"]`);
if (!audio) return;
key.classList.add('playing');
audio.currentTime = 0;
audio.play();
}
const keys = Array.from(document.querySelectorAll('.key'));
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
window.addEventListener('keydown', playSound);
Yes, you can have both, you can do something like this:
function removeTransition(e) {
if (e.propertyName !== 'transform') return;
e.target.classList.remove('playing');
}
function playSound(e) {
let keyCode;
if (e.type === 'click') {
keyCode = e.currentTarget.dataset.key;
} else {
keyCode = e.keyCode;
}
const audio = document.querySelector(`audio[data-key="${keyCode}"]`);
const key = document.querySelector(`div[data-key="${keyCode}"]`);
if (!audio) return;
key.classList.add('playing');
audio.currentTime = 0;
audio.play();
}
const keys = Array.from(document.querySelectorAll('.key'));
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
window.addEventListener('keydown', playSound);
keys.forEach(key => key.addEventListener('click', playSound))
I've used event.type to determine if the event is a click or a keydown and used element.dataset to retrieve the appropriate key in case of click events (these events don't have the keyCode property).
Also, in case of click events, event.target is actually the clicked key, you can use that instead of looking for the key in the DOM (the querySelector(`div[data-key="${keyCode}"]`) call).
Yes, as far as I am concerned, you can have as many event listeners per object as you want. Just use the relevant event listeners and attach them to the relevant HTML objects. Remember, in Javascript you need an event listener for each event.
For example: (from https://gomakethings.com/listening-to-multiple-events-in-vanilla-js/)
This doesn't work:
document.addEventListener('click mouseover', function (event) {
// this doesn't work
}, false);
Instead, you'll have to do something like this:
var someFunction = function (event) {
// Do something...
};
// Add our event listeners
window.addEventListener('click', someFunction, false);
window.addEventListener('mouseover', someFunction, false);

How do i set a predefined key before local-storage is set?

I have a hotkey which clicks an element on the page and uses the default key 'r'. I also have an input which allows the hotkey to be changed when a key is inputted. When a value is inputted it is cached to local-storage. The only issue is the default key doesn't work with the event, only when the input is set to a value.
let IsInputing = true
let key = 82
setTimeout(() => {
key = localStorage.getItem('savedKey');
input.onchange = function(){
localStorage.setItem('savedKey', key)
}
window.addEventListener("keydown", activate, false);
function activate(key) {
if (IsInputing == false) {
if (key.keyCode == key) {
console.log('key pressed')
element.click();
}
}
}
input.onkeydown = function (key) {
IsInputing = true
key = key.keyCode;
console.log('key changed')
setTimeout(changeKey, 1000)
}
function changeKey() {
IsInputing = false;
}
}, 500);
Your problem is that, the first time the code runs, you set key to 82, and then you set it to whatever localStorage.getItem returns, which will return null if nothing is cached (first time).
let key = 82;
setTimeout(() => {
key = localStorage.getItem('savedKey');
});
This means that your code essentially does:
key = 82;
setTimeout(() => {
key = null; // <-- you overwrite the key with null on the first run
});
Try setting the default value for key only if localStorage doesn't have a previously cached value:
let DEFAULT_KEY = 82;
setTimeout(() => {
key = localStorage.getItem('savedKey') || DEFAULT_KEY;
});
Or more tersely:
setTimeout(() => {
key = localStorage.getItem('savedKey') || 82;
});
Note: to avoid some potential future bugs, you might want to convert your cached value to a number when you return it (localStorage only saves strings).
key = Number(localStorage.getItem('savedKey'));
Or use a string as your default value.
let DEFAULT_KEY = '82';
setTimeout(() => {
key = localStorage.getItem('savedKey') || DEFAULT_KEY;
});
Having consistent types would avoid errors such as unexpected comparisons:
'82' == 82 // true
'82' === 82 // false
As mentioned in the comments to my answer, you have another bug in the activate function.
You are naming the parameter of activate as key, which will shadow the global key variable when you do the key.keyCode == key comparison.
function activate(key) {
// ^^^ you are using the same name as the global key
if (IsInputing == false) {
if (key.keyCode == key) {
console.log('key pressed')
element.click();
}
}
}
If key within activate was for example 'abcd', you code will do 'abcd'.keyCode == 'abcd'.
One way to solve it is to rename the activate's parameter:
function activate(activationKey) {
if (IsInputing == false) {
if (activationKey.keyCode == key) {
console.log('key pressed')
element.click();
}
}
}

Is there there an initial keypress event in JavaScript? [duplicate]

I want to have a onkeydown event fire a function only once. for that function to fire again, the user has to release the key and press/hold again.
I know its fairly simple but I'm new at JS. Also I prefer to avoid using jQuery or other libs.
One more thing, this should work for both ie and firefox.
I'm surprised it's not mentioned, there's also event.repeat:
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
console.log(e.key);
});
This will only fire once per each keypress, since event.repeat turns true after holding the key down.
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key#keyboardevent_sequence
You could set a flag:
var fired = false;
element.onkeydown = function() {
if(!fired) {
fired = true;
// do something
}
};
element.onkeyup = function() {
fired = false;
};
Or unbind and rebind the event handler (might be better):
function keyHandler() {
this.onkeydown = null;
// do something
}
element.onkeydown = keyHandler;
element.onkeyup = function() {
this.onkeydown = keyHandler;
};
More information about "traditional" event handling.
You might also want to use addEventListener and attachEvent to bind the event handlers. For more information about that, have a look at quirksmode.org - Advanced event registration models.
There's a "once" parameter you can use
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
Eg:
element.addEventListener('keydown', function(event) {
doSomething()
}, {once: true});
It'll remove it as soon as it's been called.
Alternatively you can use removeEventListener if it's a named function
Here is a method that uses addEventListener and removeEventListener
var textBox = document.getElementById("textBox");
function oneKeyDown(){
$("body").append("<h1>KeyDown<h1>"); //just to show the keypress
textBox.removeEventListener('keydown', oneKeyDown, false);
}
function bindKeyDown(){
textBox.addEventListener('keydown', oneKeyDown, false);
}
textBox.addEventListener('keyup', bindKeyDown, false)
bindKeyDown();
Code example on jsfiddle.
One note, for IE you will need to use attachEvent, detachEvent.
Here you go:
test.onkeydown = function() {
if ( this.className === 'hold' ) { return false; }
this.className = 'hold';
// call your function here
};
test.onkeyup = function() {
this.className = '';
};
Live demo: http://jsfiddle.net/simevidas/xAReL/2/
JQuery's one will help you.
What it does is, bind the eventHandler to event, and when event occurs, it runs the eventHandler and unbinds it, so that its not fired at next event.
as stated in the other answers, there is no 'onkeyfirstdown' or similar event to listen for.
the best solution is to keep track of which keys are already down in a js-object:
var keysdown = {};
element.addEventListener('keydown', function(evt) {
if(!(evt.key in keysdown)) {
keysdown[evt.key] = true;
// key first pressed
}
});
element.addEventListener('keyup', function(evt) {
delete keysdown[evt.key];
});
this way, you will not be skipping 'keyfirstpressed' events if more than one key is held down.
(many of the other solutions posted here will only fire when no other keys are down).
Here is my solution that will only run the function you pass it when a key is FIRST pressed on the target (eg window or some input field). If the user wants to trigger a key again, they'll have to release it and press it again.
Vanilla JS
const onKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
let pressed = {};
// whenever a keydown event is fired ontarget element
const onKeyDown = (event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
pressed = { ...pressed, [event.which]: true };
};
// whenever a keyup event is fired on the window element
const onKeyUp = (event) => {
const { [event.which]: id, ...rest } = pressed;
// remove key from store
pressed = rest;
};
// add listeners
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// return a function that can be called to remove listeners
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
};
And then to use it:
const removeListener = onKeyPress((event) => console.log(event.which + ' key pressed'))
removeListener(); // when you want to remove listeners later
React and React Hooks
import { useState } from 'react';
import { useEffect } from 'react';
import { useCallback } from 'react';
export const useKeyPress = (func, target = window) => {
// persistent "store" to track what keys are being pressed
const [pressed, setPressed] = useState({});
// whenever a keydown event is fired ontarget element
const onKeyDown = useCallback(
(event) => {
// if key isn't already pressed, run func
if (!pressed[event.which])
func(event);
// add key to store
setPressed({ ...pressed, [event.which]: true });
},
[func, pressed]
);
// whenever a keyup event is fired on the window element
const onKeyUp = useCallback((event) => {
// remove key from store
const { [event.which]: id, ...rest } = pressed;
setPressed(rest);
}, [pressed]);
useEffect(() => {
// add listeners when component mounts/changes
target.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// cleanup/remove listeners when component unmounts/changes
return () => {
target.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
}, [target, onKeyDown, onKeyUp]);
};
And then to use it:
import { useKeyPress } from 'wherever';
useKeyPress((event) => console.log(event.which + ' key pressed'))

Can jQuery .keypress() detect more than one key at the same time?

Is there a way for jQuery to detect that more than one key was pressed at the same time?
Is there any alternative that allows for pressing two keys at the same time to be detected?
In order to detect multiple keys being held down, use the keydown and keyup events.
var keys = {};
$(document).keydown(function (e) {
keys[e.which] = true;
});
$(document).keyup(function (e) {
delete keys[e.which];
});
I've put together a demo here: http://jsfiddle.net/gFcuU/. It's kind of fun, though I noticed my keyboard is only able to detect at most 6 keys.
It depends. For "normal" keys, that means Non- Shift, Ctrl, ALT, (CMD), the answer is no, the event handler will catch/fire in a queue, one after another.
For the modifier keys I mentioned above, there is a property on the event object.
Example:
$(document).bind('keypress', function(event) {
if( event.which === 65 && event.shiftKey ) {
alert('you pressed SHIFT+A');
}
});
Jsfiddle demo.
Other propertys are:
event.ctrlKey
event.altKey
event.metaKey
If you just want to fire a handler when several keys are pressed in series, try something like:
jQuery.multipress = function (keys, handler) {
'use strict';
if (keys.length === 0) {
return;
}
var down = {};
jQuery(document).keydown(function (event) {
down[event.keyCode] = true;
}).keyup(function (event) {
// Copy keys array, build array of pressed keys
var remaining = keys.slice(0),
pressed = Object.keys(down).map(function (num) { return parseInt(num, 10); }),
indexOfKey;
// Remove pressedKeys from remainingKeys
jQuery.each(pressed, function (i, key) {
if (down[key] === true) {
down[key] = false;
indexOfKey = remaining.indexOf(key);
if (indexOfKey > -1) {
remaining.splice(indexOfKey, 1);
}
}
});
// If we hit all the keys, fire off handler
if (remaining.length === 0) {
handler(event);
}
});
};
For instance, to fire on s-t,
jQuery.multipress([83, 84], function () { alert('You pressed s-t'); })
Nope. keypress will fire for every individual key that is pressed - except for modifier keys such as CTRL, ALT and SHIFT, you can combine them with other keys, so long as it is only one other key.
Here's a jQuery solution based on
Maciej's answer https://stackoverflow.com/a/21522329/
// the array to add pressed keys to
var keys = [];
// listen for which key is pressed
document.addEventListener('keydown', (event) => {
if ($.inArray(event.keyCode, keys) == -1) {
keys.push(event.keyCode);
}
console.log('keys array after pressed = ' + keys);
});
// listen for which key is unpressed
document.addEventListener('keyup', (event) => {
// the key to remove
var removeKey = event.keyCode;
// remove it
keys = $.grep(keys, function(value) {
return value != removeKey;
});
console.log('keys array after unpress = ' + keys);
});
// assign key number to a recognizable value name
var w = 87;
var d = 68;
var s = 83;
var a = 65;
// determine which keys are pressed
document.addEventListener('keydown', (event) => {
if ($.inArray(w, keys) != -1 && $.inArray(d, keys) != -1) { // w + d
console.log('function for w + d combo');
} else if ($.inArray(s, keys) != -1 && $.inArray(a, keys) != -1) { // s + a
console.log('function for s + a combo');
}
})
fiddle demo
https://jsfiddle.net/Hastig/us00zdo6/
If you're using esma6, you could do the following using sets.
const KEYS = new Set();
$(document).keydown(function (e) {
KEYS.add(e.which);
if(KEYS.has(12) && KEYS.has(31)){
//do something
}
});
$(document).keyup(function (e) {
KEYS.delete(e.which);
});
And if you want the user to press them together, you can do:
const KEYS = new Set(); // for other purposes
const RECENT_KEYS = new Set(); // the recently pressed keys
const KEY_TIMELAPSE = 100 // the miliseconds of difference between keys
$(document).keydown(function (e) {
KEYS.add(e.which);
RECENT_KEYS.add(e.which);
setTimeout(()=>{
RECENT_KEYS.delete(e.which);
}, KEY_TIMELAPSE);
if(RECENT_KEYS.has(37) && RECENT_KEYS.has(38)){
// Do something
}
});
$(document).keyup(function (e) {
KEYS.delete(e.which);
RECENT_KEYS.delete(e.which);
});
Here is a codepen https://codepen.io/Programador-Anonimo/pen/NoEeKM?editors=0010
As my gist expired ( no one was using it :( ) I decided to update the answer with more 2017 solution. Check below.
You can use my plugin for jquery to detect shortcuts.
It basically cache's events and get what keys are pressed at the moment. If all the keys are pressed it fires function.
https://github.com/maciekpaprocki/bindShortcut (expired!)
You have small explanation how to use it in readme file. Hope this helps. Feedback more than appreciated.
Edit 2017:
It's 2017 and we don't need jQuery plugins to solve stuff like that. In short you will need something like this:
let pressed = {};
document.addEventListener('keydown', (event) => {
pressed[event.key] = true;
});
document.addEventListener('keyup', (event) => {
delete pressed[event.key];
});
//and now write your code
document.addEventListener('keydown', (event) => {
if(pressed[firstKey]&&pressed[secondKey]){
//dosomething
}
});
Older browsers might have some quirks, however from IE9 everything should work fine except of marginal amounts of OSs that don't support right event delegation (super old ubuntu etc.). There's no way to fix that in them as that's not the browser issue.
There are some quirks in new macs connected to boolean keys like for example caps lock.
Read more: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names_and_Char_values
According to #David Tang's solution, here is a quick and dirty customization for capturing Shift+Ctrl+A combination:
var pressedKeys = {};
function checkPressedKeys() {
var shiftPressed=false, ctrlPressed=false, aPressed=false;
for (var i in pressedKeys) {
if (!pressedKeys.hasOwnProperty(i)) continue;
if(i==16){
shiftPressed=true;
}
else if(i==17){
ctrlPressed=true;
}
else if(i==65){
aPressed=true;
}
}
if(shiftPressed && ctrlPressed && aPressed){
//do whatever you want here.
}
}
$(document).ready(function(){
$(document).keydown(function (e) {
pressedKeys[e.which] = true;
checkPressedKeys();
});
$(document).keyup(function (e) {
delete pressedKeys[e.which];
});
});

Categories