Using vanilla JS, I'm trying to create an off click - as in, if the body is clicked and it is not a certain element that is clicked, close that element.
However, it works when you click the specified element (it doesn't close), but it fires the event when you click any of that element's child nodes. I'd like the if statement to include any child nodes of that parent element, as well as the parent node itself.
HTML:
<ul id="NavSocial-target" class="Nav_social">
<li class="Nav_social_item">Facebook</li>
<li class="Nav_social_item">Twitter</li>
<li class="Nav_social_item">Google</li>
</ul>
<ul class="Nav_options FlexList">
<li class="Nav_options_item FlexList_item" id="NavSocial-trigger">Social</li>
<li class="Nav_options_item FlexList_item">Curr/Lang</li>
</ul>
Javascript:
this.triggerDOM = document.getElementById('NavSocial-trigger');
this.targetDOM = document.getElementById('NavSocial-target');
// If not list item that triggers model && if not model itself
document.getElementsByTagName('body')[0].onclick = (e) => {
if(e.target !== this.triggerDOM && e.target !== this.targetDOM){
this.removeClass();
}
};
I assume this.triggerDOM is the element you want to ignore. You need to see if the click passed through the element, with a loop:
this.triggerDOM = document.getElementById('NavSocial-trigger');
// ...
document.body.addEventListener("click", e => {
var element = e.target;
while (element && element !== document.body) {
if (element === this.triggerDOM) {
// It was a click in `this.triggerDOM` or one of its
// children; ignore it
return;
}
element = element.parentNode;
}
// It wasn't a click anywhere in `this.triggerDOM`
this.removeClass();
}, false);
Side notes on the above:
document.getElementsByTagName("body")[0] is just a long way to write document.body :-)
Using onclick doesn't play nicely with others; use addEventListener (or attachEvent on IE8 and earlier, if you still need to support seriously obsolete browsers).
Arrow functions accepting a single argument don't need () around the argument list (though of course it's fine to have them there if you prefer the style).
Working example:
this.triggerDOM = document.getElementById('NavSocial-trigger');
// ...
document.body.addEventListener("click", e => {
var element = e.target;
while (element && element !== document.body) {
if (element === this.triggerDOM) {
// It was a click in `this.triggerDOM` or one of its
// children; ignore it
return;
}
element = element.parentNode;
}
// It wasn't a click anywhere in `this.triggerDOM`
this.triggerDOM.parentNode.removeChild(this.triggerDOM);
}, false);
#NavSocial-trigger {
border: 1px solid #ddd;
background-color: #dd0;
}
<!-- Note the really deep nesting -->
<div><div><div><div><div><div><div><div><div><div>
Click
<div>
anywhere
<div>
on
<div>
this
<div>
page
<div>
except
<div id="NavSocial-trigger">
here
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div></div></div></div></div></div></div></div></div>
Related
This is my HTML line:
<li onclick="return open_create_event_screen();">10</li>
I want to get the content of this < li > element (which is '10') through the JavaScript function (open_create_event_screen()) that opens once the < li > element is clicked.
Is it possible to do so?
Old way, where we put inline events into our HTML. The key is to pass this into the function, which is a reference to the element that was acted upon, you could also pass event, and determine the clicked element from the target property (two different approaches).
const open_create_event_screen = (obj) => {
console.log("opening create event screen");
console.log(obj.innerText);
}
<li onclick="open_create_event_screen(this);">10</li>
Here's a more modern approach, where our html is cleaner, and we do the work of assigning the event handlers in javascript.
const open_create_event_screen = (obj) => {
console.log("opening create event screen");
console.log(obj.innerText);
}
const lis = document.querySelectorAll(".eventThing>li");
lis.forEach(li =>
li.addEventListener("click", () => open_create_event_screen(li))
);
<ul class="eventThing">
<li>10</li>
<li>20</li>
</ul>
I have a parent container with a number of elements, it's not a well defined number of elements, it's generated by using an API once the user starts typing something in an input field. Also these elements are parents to other elements. (I'll give an example below)
The idea is that once the user clicks that element, I want to display an overlay with more information about that specific element. Sounds good, but the things I've tried didn't work so far.
I tried to add an event listener onto the container. Consider this basic HTML template.
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span>X</span></li>
<li><span>Item 4</span><span>X</span></li>
</ul>
<form>
<input type="text" id="test">
</form>
So here we have an UL which is the parent element and LI are its children. However, LI are also parents to the span tags that are supposedly showing some vital information so I cannot remove those tags.
The input field is here just to add something dyamically, to mimik the way my API works.
So like I said, I have tried to add an event listener on the UL parent >>
const items = ul.querySelectorAll('li');
items.forEach(item => item.addEventListener('click', function(e) {
console.log(this);
}));
});
});
Then I tried to add an event listener to each of the items, but instead of logging one item at a time as I click them, it's logging one, two, three and so forth.
I feel that I'm missing something here and I don't know what it is. Can you guys help me out?
Later edit: I found this solution but it doesn't seem very elegant to me and it's prone to bugs if I change stuff later on (like add more children etc).
ul.addEventListener('click', e => {
const items = ul.querySelectorAll('li');
if(e.target.tagName === 'LI' || e.target.parentElement.tagName === 'LI') {
if(e.target.tagName === 'LI') {
e.target.remove();
} else if(e.target.parentElement.tagName === 'LI') {
e.target.parentElement.remove();
}
}
});
You can use the function Element.closest() to find a parent element.
const ul = document.querySelector('ul');
ul.addEventListener('click', e => {
let li = e.target.closest('LI');
li.remove();
});
document.forms.newitem.addEventListener('submit', e => {
e.preventDefault();
let newitem = document.createElement('LI');
newitem.innerText = e.target.test.value;
ul.appendChild(newitem);
});
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span><em>X</em></span></li>
<li>Item 4 X</li>
</ul>
<form name="newitem">
<input type="text" name="test">
</form>
The reason that you got all line events every time that you've clicked in a line, is that you are add a event click in ul element and you just need it in the li element
A way very similar that you'd like:
let ul = document.querySelector("ul");
const items = ul.querySelectorAll("li");
items.forEach((item) =>
item.addEventListener("click", function (e) {
let getTargetRow = e.target.parentElement;
if (getTargetRow.nodeName === "LI"){
console.log("the line removed:",getTargetRow.textContent);
getTargetRow.remove();
}
})
);
<ul>
<li><span>Item 1</span><span>X</span></li>
<li><span>Item 2</span><span>X</span></li>
<li><span>Item 3</span><span>X</span></li>
<li><span>Item 4</span><span>X</span></li>
</ul>
<form>
<input type="text" id="test" />
</form>
I'm trying to get back children id on keydown event but so far i had only luck with click event.
When i'm doing with keydown event my solution got me parent ID back but with click event i got children ID back.
Do you have any idea how can i achieve this but with keydown event?
// Refernce the parent of all of the target nodes
var parent = document.getElementById('parent');
// Register the click event to #parent
parent.addEventListener('keydown', idNode);
// This is the callback that is invoked on each click
function idNode(e) {
/* If the node clicked (e.target) is not the
|| the registered event listener
|| (e.currentTarget = #parent)
*/
if (e.target !== e.currentTarget) {
// Get the #id of clicked node
var ID = e.target.id;
// Reference e.target by its #id
var child = document.getElementById(ID);
}
// Log the #id of each e.target at every click
console.log('The caret is located at ' + ID);
// Return the e.target as a DOM node when needed
return child;
}
<div id="parent" contenteditable="true">
<div id="child-1" tabindex="-1">
One
</div>
<div id="child-2" tabindex="-1">
Two
</div>
<div id="child-3" tabindex="-1">
Three
</div>
</div>
Check out this snippet:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="parent">
<div id="child-1" contenteditable="true">
One
</div>
<div id="child-2" contenteditable="true">
Two
</div>
<div id="child-3" contenteditable="true">
Three
</div>
</div>
<script type="text/javascript">
// Refernce the parent of all of the target nodes
var parent = document.getElementById('parent');
// Register the click event to #parent
parent.addEventListener('keydown', idNode, false);
// This is the callback that is invoked on each click
function idNode(e) {
/* If the node clicked (e.target) is not the
|| the registered event listener
|| (e.currentTarget = #parent)
*/
var ID, child;
if (e.target !== e.currentTarget) {
// Get the #id of clicked node
ID = e.target.id;
// Reference e.target by its #id
child = document.getElementById(ID);
}
// Log the #id of each e.target at every click
console.log('The caret is located at ' + ID);
// Return the e.target as a DOM node when needed
return child;
}
</script>
</body>
</html>
Basically you making parent editable, so KeyboardEvent receiving id of parent only. Instead of parent I have set contenteditable="true" on child s which make sure KeyboardEvent find out right target.
Ids in dynamic content are hard to manage, as the user can remove elements with an id, and create new elements without an id. Refer the elements in a content editable with references only.
Only focusable elements (like form control elements and an editable, also document) can fire a keydown event. An unfocusable element doesn't participate on KeyboardEvent firing in any way, the event really fires on the element it is attached, it's not bubbling up from the seemingly targeted element (the element the caret is positioned to). The seemingly targeted element isn't also included in any of the properties of KeyboardEvent object.
You've to use Selection Object to get the element where the caret lies when KeyboardEvent fires. Something like this:
const parent = document.getElementById('parent');
parent.addEventListener('keydown', idNode);
function idNode(e) {
const selection = window.getSelection(),
target = selection.anchorNode.parentElement;
console.log(target);
}
<div id="parent" contenteditable="true">
<div id="child-1">
One <span>Plus</span>
</div>
<div id="child-2">
Two
</div>
<div id="child-3">
Three
</div>
</div>
There are multiple ways to get the selected element from Selection object, the above code is just an example. As a sidenote, returning from an event handler function is not useful, as there's no place in the code where you could receive the returned value (the value is returned to the event queue).
I don't understand why when I add a click event listener on an element, its children triggers it too.
I want that the parent is triggered even if the children are clicked, which should be the normal behavior I think.
Here's the code :
var jobsList = document.querySelectorAll('.job_list .job');
for (var i = 0; i < jobsList.length; i++) {
jobsList[i].addEventListener('click', _onChangeJob, false);
}
function _onChangeJob(e){
// When The children (.job_title, .job_date) is clicked, this function is called. I want only the parent to be clicked event if the children are clicked.
}
<div class="job_list">
<div class="job active" data-job="1">
<p class="job_title">Job</p>
<p class="job_date">21.11.16</p>
</div>
<div class="job active" data-job="1">
<p class="job_title">Job</p>
<p class="job_date">21.11.16</p>
</div>
</div>
You can use e.target to check what has been clicked, then for example check if the jobsList collection contains the target element. If it does then one of the .job elements is the target, otherwise the target is a child element.
// convert to a regular array to get indexOf method
var jobsList = [].slice.call(document.querySelectorAll('.job_list .job'));
for (var i = 0; i < jobsList.length; i++) {
jobsList[i].addEventListener('click', _onChangeJob, false);
}
function _onChangeJob(e){
if( jobsList.indexOf( e.target ) !== -1 ){
console.log( 'clicked on .job')
}
else {
console.log( 'clicked on a child element')
}
}
https://jsfiddle.net/4r7hLua2/
Let's say we have next html/css: http://jsfiddle.net/rbDKm/2/
<div id="header">header</div>
<div id="content">
<div class="actions">
<div class="actions-row">
<div class="action ">home</div>
<div class="action ">search</div>
<div class="action ">settings</div>
</div>
<div class="actions-row">
<div class="action">...</div>
<div class="action">...</div>
<div class="action">...</div>
</div>
</div>
</div>
<div id="footer">footer</div>
What I need is to detect any clicks, which were made on an "empty" area.
In provided fiddle, empty area would be everything, which has white or orange color.
Is there any technique/approach for doing this?
With your markup as you have it now, I think that means you need to detect clicks outside of any div.action, #header or #footer.
To do that, you hook click on body, and then look at the path it took to get to body:
document.body.addEventListener("click", function(e) {
var elm = e.target;
while (elm !== document.body) {
if (elm.id === "header" || elm.id === "footer") {
return;
}
if (elm.tagName.toUpperCase() === "DIV" && /(?:^| )action(?:$| )/.test(elm.className)) {
return;
}
elm = elm.parentNode;
}
// If we got here, it's on one of the desired areas
});
There are a couple of variations on that you could do. For instance, instead of className.match you could use classList on modern browsers. And modern browsers also offer Element#closest which you could use instead of the loop.
Alternately, you hook click on body and hook click on the relevant other elements and prevent the click from propagating:
function stopTheEvent(e) {
e.stopPropagation();
}
document.body.addEventListener("click", function(e) {
// It's one of the desired areas
});
var list = document.querySelectorAll("#header, #footer, div.action");
var index;
for (index = 0; index < list.length; ++index) {
list[index].addEventListener("click", stopTheEvent);
}
...but that seems messier.
Note: In the above, I haven't allowed for IE8's lack of addEventListener. If you need to support IE8, look at attachEvent("onclick", ...) and use window.event.returnValue = false; to stop propagation (or use a library that abstracts these things away).
Use the "target" of the event, and check it's class/id
http://jsfiddle.net/yryQ6/
When you bind the click event, use the event, get the target and ask for it's class:
$('#content').on('click',function(e){
if ($(e.target).hasClass('action')) {
// click on an action
} else {
// click on smething that's not an action, in this case, action-row o actions, your empty area
}
}
EDIT: assuming you use jquery, you should be able to use something similar with javascript events