I'd like to detect a click inside or outside a div area. The tricky part is that the div will contain other elements and if one of the elements inside the div is clicked, it should be considered a click inside, the same way if an element from outside the div is clicked, it should be considered an outside click.
I've been researching a lot but all I could find were examples in jquery and I need pure javascript.
Any suggestion will be appreciated.
It depends on the individual use case but it sounds like in this example there are likely to be other nested elements inside the main div e.g. more divs, lists etc. Using Node.contains would be a useful way to check whether the target element is within the div that is being checked.
window.addEventListener('click', function(e){
if (document.getElementById('clickbox').contains(e.target)){
// Clicked in box
} else{
// Clicked outside the box
}
});
An example that has a nested list inside is here.
You can check if the clicked Element is the div you want to check or not:
document.getElementById('outer-container').onclick = function(e) {
if(e.target != document.getElementById('content-area')) {
console.log('You clicked outside');
} else {
console.log('You clicked inside');
}
}
Referring to Here.
you can apply if check for that inside your click event
if(event.target.parentElement.id == 'yourID')
In Angular 6 and IONIC 3, I do same as here:
import {Component} from 'angular/core';
#Component({
selector: 'my-app',
template: `
<ion-content padding (click)="onClick($event)">
<div id="warning-container">
</div>
</ion-content>
`
})
export class AppComponent {
onClick(event) {
var target = event.target || event.srcElement || event.currentTarget;
if (document.getElementById('warning-container').contains(target)){
// Clicked in box
} else{
// Clicked outside the box
}
}
}
This working fine on web/android/ios.
It might be helpful for someone, Thanks.
Try this solution it uses pure javascript and it solves your problem. I added css just for better overview... but it is not needed.
document.getElementById('outer-div').addEventListener('click', function(){
alert('clicked outer div...');
});
document.getElementById('inner-div').addEventListener('click', function(e){
e.stopPropagation()
alert('clicked inner div...');
});
#outer-div{
width: 200px;
height: 200px;
position: relative;
background: black;
}
#inner-div{
top: 50px;
left: 50px;
width: 100px;
height: 100px;
position: absolute;
background: red;
}
<div id="outer-div">
<div id="inner-div">
</div>
</div>
I came up with a hack for this that's working well for me and that might help others.
When I pop up my dialog DIV, I simultaneously display another transparent DIV just behind it, covering the whole screen.
This invisible background DIV closes the dialog DIV onClick.
This is pretty straightforward, so I'm not going to bother with the code here. LMK in the comments if you want to see it and I'll add it in.
HTH!
closePopover () {
var windowBody = window
var popover = document.getElementById('popover-wrapper') as HTMLDivElement;
windowBody?.addEventListener('click', function(event){
if(popover === event.target) {
console.log("clicked on the div")
}
if(popover !== event.target) {
console.log("clicked outside the div")
}
})
}
}
I recently needed a simple vanilla JS solution which solves for:
Ignoring specific selectors including whether a parent contains one of these selectors
Ignoring specific DOM nodes
This solution has worked quite well in my app.
const isClickedOutsideElement = ({ clickEvent, elToCheckOutside, ignoreElems = [], ignoreSelectors = [] }) => {
const clickedEl = clickEvent.srcElement;
const didClickOnIgnoredEl = ignoreElems.filter(el => el).some(element => element.contains(clickedEl) || element.isEqualNode(clickedEl));
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map(selector => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false;
if (
isDOMElement(elToCheckOutside) &&
!elToCheckOutside.contains(clickedEl) &&
!didClickOnIgnoredEl &&
!didClickOnIgnoredSelector
){
return true;
}
return false;
}
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument;
}
In React you can use useClickOutside hook from react-cool-onclickoutside.
Demo from Github:
import { useClickOutside } from 'use-events';
const Example = () => {
const ref1 = React.useRef(null);
const ref2 = React.useRef(null);
const [isActive] = useClickOutside([ref1, ref2], event => console.log(event));
return (
<div>
<div ref={ref1} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
<br />
<div ref={ref2} style={{ border: '1px dotted black' }}>
You are {isActive ? 'clicking' : 'not clicking'} outside of this div
</div>
</div>
);
};
Live demo
Related
I have a bunch of
<div class="location-box" data-location-id="123">
<img src="img_url" />
</div>
Loaded into my .locations div.
I want that whenever you click on a .location-box that the clicked div gets a highlighted class on it. And the attribute value gets added to a hidden input. When you click on another one, the class from the previous one gets removed. And so on and so on.
I've tried it before when those divs where static, and it worked fine. But now I'm appending these divs out of pure Javascript from an api call.
I also know that not yet generated DOM can't be manupilated by event listeners etc.
I've looked into mutation observers, and tried some simple stuff from the docs. But I could make this code work with it
let locations = document.querySelectorAll(".location-box");
locations.forEach( el =>
el.addEventListener('click', function() {
locations.forEach( els => els.classList.remove('active-location'));
document.getElementById('location_id').value = this.getAttribute('data-location-id');
this.classList.add("active-location");
})
);
Does anyone know how to make this work? Maybe not only this time, but in multiple cases. Cause in the near future I'd probably have more not yet generated DOM.
From my above comment ...
"#Coolguy31 ... A MutationObserver based approach most likely is overkill. Event Delegation might be the technique of choice. But in order to implement it somehow correctly it was nice to know whether all the later rendered stuff is always inserted/appended below a common and also known root node, cause document.body as event listening root is not the most elegant/performant choice either."
function uuid(a) {
// [https://gist.github.com/jed/982883] - Jed Schmidt
return a
? (a^Math.random()*16>>a/4).toString(16)
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,uuid);
}
function addLocations(evt) {
evt.preventDefault();
const allLocationsRoot =
document.querySelector('.locations');
allLocationsRoot.innerHTML = `
<div class="location-box">
<img src="https://picsum.photos/133/100?grayscale" />
</div>
<div class="location-box">
<img src="https://picsum.photos/100/75?grayscale" />
</div>
<div class="location-box">
<img src="https://picsum.photos/120/90?grayscale" />
</div>
`;
allLocationsRoot
.querySelectorAll('.location-box')
.forEach(locationNode => locationNode.dataset.locationId = uuid());
allLocationsRoot
.closest('form[name="location-data"]')
.elements['location']
.value = '';
}
function initializeAddLocations() {
document
.querySelector('button')
.addEventListener('click', addLocations);
}
function handleLocationSelect(evt) {
const locationItemRoot = evt
.target
.closest('.location-box');
if (locationItemRoot) {
const allLocationsRoot = locationItemRoot
.closest('.locations');
const locationControl = allLocationsRoot
.closest('form[name="location-data"]')
.elements['location'];
// console.log({
// target: evt.target,
// locationItemRoot,
// allLocationsRoot,
// locationControl,
// });
allLocationsRoot
.querySelectorAll('.location-box')
.forEach(locationNode => locationNode.classList.remove('selected'));
locationItemRoot.classList.add('selected');
locationControl.value = locationItemRoot.dataset.locationId;
}
}
function initializeLocationHandling() {
document
.querySelector('.locations')
.addEventListener('click', handleLocationSelect)
}
function main() {
initializeLocationHandling();
initializeAddLocations();
}
main();
body { margin: 0; }
[type="text"][name="location"] { width: 23em; }
.locations:after { display: block; content: ''; clear: both; }
.location-box { float: left; margin: 4px; padding: 10px; min-height: 104px; background-color: #eee; }
.location-box.selected { outline: 2px solid #fc0; }
<form name="location-data">
<div class="locations">
</div>
<button>update locations</button>
<!--
<input type="hidden" name="location" />
//-->
<input type="text" name="location" disabled />
</form>
You can do that with MutationObserver, the code it's something like below, it doesn't have the piece to get the attribute, but you can add that, another way of doing it would be like #scara9 is saying, on the code you use to render each .location-box you can assign the click handler.
in the code below i used jquery to "add" new location-box, you don't need jquery for this
// select the parent node: .locations, i switched to ID for test
var target = document.getElementById("locations");
// create an observer instance
var observer = new MutationObserver(function (mutations) {
//loop through the detected mutations(added controls)
mutations.forEach(function (mutation) {
//addedNodes contains all detected new controls
if (mutation && mutation.addedNodes) {
mutation.addedNodes.forEach(function (elm) {
if (elm && elm.className=="location-box") {
elm.addEventListener("click", function () {
elm.classList.add("active-location");
var locations = document.querySelectorAll(".location-box");
locations.forEach((e) => {
if (e != elm) {
//ignore clicked element
e.classList.remove("active-location");
}
});
});
}
});
}
});
});
// pass in the target node, as well as the observer options
observer.observe(target, {
childList: true
});
//you don't need this, it's only to simulate dynamic location-box
$(function () {$("button").on("click", function () {var count = $(".location-box").length + 1;$(".locations").append($("<div class='location-box' data-attribute-id='" +count +"'>location box:" +count +"</div>"));});});
.active-location{
background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="locations" id="locations">
</div>
<!-- This button it's only to add the controls, not needed in your case-->
<button type="button" id="add">
Add
</button>
I would like to use a code when I press the space bar a shape appears and it disappears when I press it again. I'm trying to get the addEventListener to work with a sample:
hello = document.querySelector('#Player');
with player being the id of the shape that I want to control. I declared hello above and initialized it in setup (I am using JavaScript), the Player id has also been initialized in HTML and given a shape in CSS. When I use
hello.addEventListener('keypress', (event) => {
console.log(event.key)
})
nothing happens, but when I use
window.addEventListener('keypress', (event) => {
console.log(event.key)
})
it works. Is there anything that I am doing wrong?
It is because by default div is not selectable. In order to make it selectable you need to use tabindex attribute on your div. It will make your div selectable.
const hello = document.querySelector('#player');
hello.addEventListener("keypress", evt => {
console.log(evt.key)
})
<div id="player" tabindex="0">
Player Shape
</div>
It will show a boundary around your div which can be remove by using css -
outline: none;
That's what should happen.
You can add the keypress event to the window or document.
And, if you add it to both, the window wins over for some reason – someone else might clarify this to both of us.
const el = document.getElementById("el");
el.addEventListener("keypress", event => keyPressed(event, "blue"));
document.addEventListener("keypress", event => keyPressed(event, "green"));
window.addEventListener("keypress", event => keyPressed(event, "purple"));
function keyPressed(event, color) {
if (event.key = " ")
el.style.backgroundColor = color;
}
#el {
width: 100px;
height: 100px;
background: red;
}
<div id="el"></div>
Hello using a table kind of like this
https://jsfiddle.net/vw19pbfo/24/
how could i make a trigger onClick that removes borders on first click and on second click add them back but that should only happen on the row that is being clicked on and not affect the other. I have tried to have a conditional css on the first and last <td> but that affected every border but i only want to affect the clicked one
function removeBorders(e){
var target = e.target || e.srcElement;
target.parentElement.classList.toggle('without-border');
};
Working JSFiddle: https://jsfiddle.net/andrewincontact/su86fhxo/9/
Changes:
1) to css:
.my-table-row.without-border td {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
2) to html:
onclick=removeBorders(event) instead onClick=this.removeBorders()
One way would be to check the <td> element's parent and add/remove a custom class, like so:
function removeBorders(e) {
var row = e.parentElement;
if (row.className.indexOf("has-borders") === -1) {
row.classList.add("has-borders");
} else {
row.classList.remove("has-borders");
}
};
Working JSFiddle: https://jsfiddle.net/d380sjrh/
I also changed onClick=this.removeBorders() to onclick="removeBorders(this);".
JSFiddle: https://jsfiddle.net/4qdstec7/
Use React's state to set and unset the selected class.
const Row = ({ children }) => {
const [selected, setSelected] = useState(false);
const onClick = e => setSelected(!selected);
return (
<tr
className={selected && 'selected'}
onClick={onClick}
>
{children}
</tr>
)
}
Developing in React requires a shift in thinking from traditional web development. Please take some time to read this excellent post from the React team.
I'm trying to remove all jQuery from my code. Until now I used
if ($(selector).find(':focus').length === 0) {
// focus is outside of my element
} else {
// focus is inside my element
}
to distinguish wether the focus is inside of one of my elements. Can you show me a jQuery-free way of doing it?
You can use Node.contains native DOM method for this.
el.contains(document.activeElement);
will check if activeElement is a descendant of el.
If you have multiple elements to check, you can use a some function to iterate.
It is possible with Element's matches() method and with a simple selector string as follows:
let hasFocused = elem.matches(':focus-within:not(:focus)');
let focusedOrHasFocused = elem.matches(':focus-within');
Use CSS :focus pseudo-class in querySelectorAll()
setTimeout(function(){
if (document.querySelectorAll("div :focus").length === 0)
console.log("not focused");
else
console.log("focused")
}, 2000);
<div>
<input type="text">
</div>
Depending on your situation, using events might be more performant.
You can use the focusin and focusout events in that case.
const el = document.getElemen
el.addEventListener("focusin", () => console.log("focus!"));
el.addEventListener("focusout", () => console.log("blur!"));
Note that during focusout events the document.activeElement will be the document body. To work around this issue, you can make use of FocusEvent.relatedTarget.
If you have issue where document.activeElement is returning <body> element after blur event, you just need to wrap it with setTimeout() and it will return correct element.
handleBlur() {
setTimeout(() => {
console.log(document.activeElement); // this actually return active/focused element
});
}
if you are using it standalone without timeout
handleBlur() {
console.log(document.activeElement); // this is returning <body> element
}
Combined some of answers posted here. Using a combination of focusin, focusout, contains and relatedTarget, you should be able to know when focus is on the children of a particular element.
const elm = document.getElementById('check-focus-here')
elm.addEventListener('focusin', (event) => {
console.log(event.target, event.relatedTarget)
// console.log(elm.contains(event.relatedTarget))
})
elm.addEventListener('focusout', (event) => {
console.log(event.target, event.relatedTarget)
console.log(elm.contains(event.relatedTarget))
})
#check-focus-here {
border: 2px solid;
padding: 8px;
margin: 8px;
}
<div id="check-focus-here">
<input id="first-name" type="text" />
<input id="middle-name" type="text" />
<input id="last-name" type="text" />
<button type="button">Save Name</button>
</div>
<button type="button">Tab to this for outside focus</button>
Here's a working example following #Northern and #Adam Šipický answers...
const tr = document.querySelector("table tbody tr");
tr.addEventListener('blur', () => {
setTimeout(() => {
if (!tr.contains(document.activeElement)) {
// Execute your condition code here...
}
}, 200);
}, true);
In 2021 you can probably avoid javascript altogether to check if an element or any of the element's child nodes have focus – unless you are manipulating DOM elements outside of a parent element.
For example:
<div class="parent">
<button>foo</button>
<button>food</button>
<button>foosh</button>
</div>
.parent { background: white }
.parent:focus-within { background: red }
.parent:focus-within button:not(:focus) { opacity: .5 }
None of these existing non CSS based solutions account for the situation where the JavaScript context does not match the frame the node was rendered in. To account for this you would want to do something like the following:
el.contains(el.ownerDocument.activeElement)
To retrieve the selected element you can use:
let activeElement = document.activeElement
To check a specific element:
let elem = document.getElementById('someId');
let isFocused = (document.activeElement === elem);
I use react and react-modal to create an overlay over a website. This overlay contains various elements and also a form (overview below). I want to be able to guide the user through the form using TAB keys. I assigned tabindex=0 to the required elements to be tabbable in order of appearance.
My problem is: It does not work in Chrome (Version 61.0.3163.100) while it does work in Firefox. I read that this happens if any element up the DOM-tree is invisible or has height/width of 0. I made some styling changes to fix that but with no effect.
<div class="ReactModalPortal">
<div data-reactroot="" class="ReactModal__Overlay" style="position: fixed; top: 0px; left: 0px; right: 0px; bottom: 0px;">
<div class="ReactModal__Content" tabindex="-1" aria-label="Questionnaire" style="position: absolute; top: 0px; left: 0px; right: 0px; height: 100%; background: transparent none repeat scroll 0% 0%; overflow: auto;">
<!-- Some other stuff and nested elements -->
<div id="...">
<form>
<input tabindex="0">
<button tabindex="0">
</form>
</div>
</div>
</div>
As you can see one of the parent elements has tabindex="-1". When changing it through the inspect function in Chrome or programmatically with JS the problem still persists (or is it a difference if the element was rendered with this index initially?).
Update
I realized that something else was causing the issues. I was using the CSS attribute initial: all on the root node of my modal to fence my inner CSS from everything outside. For some reason this was preventing the tabindex from working. If you can help me understanding I will reward this wis the bounty. My workaround is just not using all: initial (it is not IE-compatible anyways but also there is no real good alternative I am aware of).
all: initial resets all CSS properties of the node with initial properties.
For display property, the initial value would be inline.
So, setting all: initial to the root div would set the display property to inline. An inline element does not have height or width, so these are 0x0.
This is also because the div contains only fixed, absolutely positioned elements.
React Modal checks if elements are focusable by running a loop through all the elements inside the modal. However, for an element to be focusable, it has to visible. For each element, we have to iterate till the body element to ensure it's visibility.
Here is the function that checks whether the element is visible.
function hidden(el) {
return (
(el.offsetWidth <= 0 && el.offsetHeight <= 0) || el.style.display === "none"
);
}
As you can see, our div would have no offsetHeight or offsetWidth and would be deemed as hidden. Therefore, the modal cannot not be focused.
I had the same issue and was not able to get other solutions working quicky, so I came up with brute force approach. Make a ref to the container element that holds the focusable elements that you wish to make tabbable.
const formRef = useRef();
<ReactModalTabbing containerRef={formRef}>
<form ref={formRef} onSubmit={handleSubmit} >
<input type="text" />
<input type="text" />
<input type="text" />
<input type="text" />
</form>
</ReactModalTabbing>
And this is the component
import React, { useState, useEffect } from 'react';
const ReactModalTabbing = ({ containerRef, children }) => {
const [configuredTabIndexes, setConfiguredTabIndexes] = useState(false);
const focusableElements = () => {
// found this method body here.
//https://zellwk.com/blog/keyboard-focusable-elements/
return [...containerRef?.current?.querySelectorAll(
'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]):not([type="hidden"]):not([disabled])'
)];
}
const isTabbable = (element) =>{
if(element.getAttribute('tabindex')){
return true;
}
return false;
}
const findElementByTabIndex = (tabIndex) => {
return containerRef?.current?.querySelector(`[tabindex="${tabIndex}"]`);
}
const moveFocusToTabIndex = (tabIndex) => {
findElementByTabIndex(tabIndex)?.focus();
}
const handleKeyDownEvent = (event) => {
if(!isTabbable(event.target)){
return;
}
const tabIndex = parseInt(event.target.getAttribute('tabindex'));
if(event.shiftKey && event.key === 'Tab'){
moveFocusToTabIndex(tabIndex - 1);
}else if(event.key === 'Tab'){ //should probably make sure there is no other modifier key pressed.
moveFocusToTabIndex(tabIndex + 1);
}
}
useEffect(() => {
if(!configuredTabIndexes && containerRef.current){
setConfiguredTabIndexes(true);
focusableElements().forEach((el, index) => el.setAttribute('tabindex', index + 1));
containerRef?.current?.addEventListener('keydown', handleKeyDownEvent);
}
});
return children;
}
export default ReactModalTabbing;