How to check if element has focused child using javascript? - javascript

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);

Related

Adding an eventlistener on not yet generated DOM

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>

addEventListener only works when I use window infront of it

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>

Close modal in vanilla js [duplicate]

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

change this style attribute from class element on click

const expandIt = document.querySelectorAll('.k-grid')
.forEach(el => el.addEventListener('click', () => {
el.classList.add('pressed') // This wont work getting HTML obj etc in console
console.log('pressed' + el)
$(this).css({'white-space':'normal'}); // this works in console
}))
Above is what I have; I am simply trying to set white space to normal on current clicked element. I think using this would work - classList doesn't seem to be working I guess from how/when dynamic elems are injected. I am looking for plain JS, or ideally ES6.
Add event as a function parameter and set el = event.target
const expandIt = document.querySelectorAll('.k-grid')
.forEach(el => el.addEventListener('click', (event) => {
const el = event.target;
el.classList.add('pressed') // This will work getting HTML obj etc in console
console.log('pressed' + el);
}))
.k-grid {padding: 1em; float: left;}
.pressed {font-weight: bold;}
<div class="k-grid">Hello</div>
<div class="k-grid">Hello</div>
<div class="k-grid">Hello</div>
HTML DOM addEventListener() Method
function
Required. Specifies the function to run when the event occurs.
When the event occurs, an event object is passed to the function as
the first parameter. The type of the event object depends on the
specified event. For example, the "click" event belongs to the
MouseEvent object.
Bold emphasis mine

Event listener outside of Shadow DOM won't bind to elements inside of Shadow DOM

I have an Angular web component installed on my site. It uses Shadow DOM so it's super fast (which it has to be in my case).
On my site I also have a shortcut on h which opens up a popup that displays some helpful information. It's a must that this h keybinding stays as it is. Example code of how it was implemented can be seen here: https://jsfiddle.net/js1edv37/
It's a simple event listener that listens on document:
$(document).on("keyup", function(e) {
}
However, this also gets triggered when my web component has focused textarea or input elements. This happens because it uses Shadow DOM, which a script from the outside cannot access.
You can test it by pressing h on the keyboard inside and outside of the input and textarea elements.
Is there a way to let my script from outside of the Shadow DOM web component, still listen for the keyup event, but make it listen for all elements on the page? Even the ones inside the Shadow DOM.
In the Web Component, get the input element with a querySelector() call on the shadowRoot property:
let textareainshadow = div.shadowRoot.querySelector( 'textarea' )
Then listen to the keyup event and stop its propagation with the help of the stopImmediatePropagation() method.
textareainshadow.addEventListener( 'keyup' , ev => {
console.log( 'caught', ev.type )
ev.stopImmediatePropagation()
})
https://jsfiddle.net/7mkrxh25/1/
If you save the reference to the shadow root you can always access it's children as search on those
$(document).on("keyup", function(e) {
let focusedInputs = $("input:focus, textarea:focus").length + $(shadow).children("input:focus, textarea:focus").length;
if (focusedInputs > 0) {
return true;
}
if (e.keyCode === 72) {
trigger();
}
});
function trigger() {
alert("If this was triggered, everything is perfectly fine");
}
let div = document.querySelector("div");
let shadow = div.createShadowRoot();
shadow.innerHTML = "<textarea>This shouldn't fail</textarea>";
textarea {
width: 500px;
height: 100px;
}
input {
width: 250px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<textarea>Some stuff here</textarea>
<br />
<input type="text" value="Some more text here" />
<br />
<br />
<h1>Shadow DOM element WON'T fail now :)</h1>
<div></div>
Fiddle

Categories

Resources