How to find the index of an event target's parent node in JS? - javascript

When the page loads, an array is made that holds a group of divs and is used as a global variable catGroup. Each div has a button as a child, and when clicked, the target is saved as a global variable targ. What I'm trying to do is determine the index of the button's parent node every time it is clicked. I haven't been able to find a way to make this happen. Any help is appreciated.
i = catGroup.findIndex(node => node == targ.parentNode);

Wrap an element around all <div>s then register it to the click event:
document.querySelector('main').onclick = clickHandler;
Get the <div> of the button clicked by user:
const clicked = e.target;
const act = clicked.parentElement;
Collect all <div> that are direct descendants of <main>:
const divs = [...this.querySelectorAll('main > div')];
Remove '.active' from all <div> and then add .active to the parent <div> of clicked button:
divs.forEach(d => d.classList.remove('active'));
act.classList.add('active');
Filter the array of divs and return it's index:
idx = divs.flatMap((d, i) => d.className === 'active' ? i : []);
idx = divs.findIndex(d => d === act);
// As Peter Seliger suggested -- I brain farted
const main = document.querySelector('main');
main.onclick = getIndex;
function getIndex(e) {
let idx;
const clicked = e.target;
const divs = [...this.querySelectorAll('main > div')];
if (clicked.matches('[type="button"]') || clicked.matches('button')) {
const act = clicked.parentElement;
divs.forEach(d => d.classList.remove('active'));
act.classList.add('active');
idx = divs.findIndex(d => d === act);
}
document.querySelector('section').textContent = idx;
}
html {
font: 2ch/1 Consolas;
}
body {
position: relative;
height: 100vh
}
main {
padding: 5px 20px 10px;
}
section {
position: fixed;
top: 0;
left: 50%;
font-size: 5rem;
}
div {
width: 8.5ch;
text-align: center;
}
[type='button'], button {
display: block;
width: 10ch;
height: 3ch;
cursor: pointer;
}
<main>
<section></section>
<div><input type='button' value='0'></div>
<div><input type='button' value='1'></div>
<div><input type='button' value='2'></div>
<div><button>3</button></div>
<div><input type='button' value='4'></div>
<div><button>5</button></div>
<div><input type='button' value='6'></div>
<div><input type='button' value='7'></div>
<div><button>8</button></div>
<div><input type='button' value='9'></div>
</main>

This worked for me in a mouseover event. That's how I did it in my case:
onMouseOver={(e) => { const idx = Array.from(e.target.parentNode.children).indexOf(e.target); }}

Related

How do I get the ViewButton to not copy the old note.value

//javascript, this is where I'm having the issue
const form = document.querySelector("#Form");
const note = document.querySelector("#Note");
const table = document.querySelector("#noteTable");
const count = 0;
form.addEventListener("submit", (e) => {
e.preventDefault();
if (note.value !== '') {
const btn = document.createElement("button");
btn.innerHTML = "View Details";
const row = table.insertRow();
const noteRow = row.insertCell();
const viewD = row.insertCell();
noteRow.innerHTML = note.value;
viewD.append(btn);
model.append(note.value);
note.value = "";
btn.addEventListener("click", () => {
model.classList.add("show");
});
const close = document.querySelector("#close");
close.addEventListener("click", () => {
model.classList.remove("show");
});
} else {
alert("Write a note!");
}
});
css button {
color: black;
background-color: green;
}
body {
background-color: rgb(182, 215, 227);
min-height: 100vh;
margin: 0;
}
.model-container {
background-color: rgba(245, 222, 179, 0.38);
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0;
left: 0;
height: 100vh;
width: 100vh;
opacity: 0;
pointer-events: none;
}
.model {
background-color: white;
}
.model-container.show {
pointer-events: auto;
opacity: 1;
}
.open:hover {
cursor: pointer;
color: white;
}
td {
display: block;
}
<h2>NOTE TAKER</h2>
<h6>Add A New Note:</h6>
<form id="Form" action="#">
<label class="ntag" for="note">Note:</label>
<input class="ntag" name="note" id="Note" type="text" placeholder="Write a note">
<button id="Add">Add Note</button>
</form>
<div class="theTable">
<table id="noteTable">
<tr id="Headers" class="headers">
<th></th>
</tr>
</table>
</div>
<div class="model-container" id="model">
<div class="model">
<button class="close" id="close">Close Me</button>
</div>
</div>
I'm not sure why but the notes repeat in the model-container, after you make another note the first one is still there with the second one right after it.
I thought that it could be the placement, so i put it in the btn function, but it duplicated as well; also sorry for how ugly this is, I'm just focused on the JavaScript
If you inspect the source after adding a few notes, you'll notice that it looks like this, assuming I added notes "one", "two" and "three":
It's putting it there because of this line of javascript:
model.append(note.value);
The .append() method doesn't wipe out anything in the <div id="model">, it just adds on to whatever is in there by dumping it after the button or dumping after any existing text.
To avoid erasing the "Close Me" button you'll probably want another div specifically for the text so that instead of using .append() you can just set the .textContent of the element each time. This would be destructive in a way that you wouldn't want to do this on the parent element, because it would wipe out the button. .append() is what is retaining the previous stuff when you click "View Detail."
<div class="model-container" id="model">
<div class="model">
<button class="close" id="close">Close Me</button>
</div>
<div id="txtDetails"><div>
</div>
So instead of using .append() just set the text of <div id="txtDetails"> to what you want it to say by setting the .textContent.
I also added a "data-text" attribute to the button on creation so it would be easier to fish out the text instead of navigating the parent elements and across elements.
Finally, on the click listener event I made it take whatever is stored in the "data-text" attribute and place that into <div id="txtDetails"> so that each "View Details" click would only show what is relevant for that particular note. This method is destructive in that it wipes out and replaces anything in <div id="txtDetails"> with each click but leaves the button in the modal alone.
const form = document.querySelector("#Form");
const note = document.querySelector("#Note");
const table = document.querySelector("#noteTable");
const count = 0;
form.addEventListener("submit", (e) => {
e.preventDefault();
if(note.value !== '')
{
const btn = document.createElement("button");
btn.innerHTML = "View Details";
btn.setAttribute('data-text', note.value);
const row = table.insertRow();
const noteRow = row.insertCell();
const viewD = row.insertCell();
noteRow.innerHTML = note.value;
viewD.append(btn);
note.value = "";
btn.addEventListener("click", () => {
document.getElementById('txtDetails').textContent = btn.getAttribute('data-text');
model.classList.add("show");
});
const close = document.querySelector("#close");
close.addEventListener("click", () => {
model.classList.remove("show");
});
}
else{
alert("Write a note!");
}
});
https://jsfiddle.net/tnqp8L0x/

VanillaJS - find middle element in the container

So I have a setup like this
<div class=“container”>
<div class=“segment segment1”></div>
<div class=“segment segment2”></div>
<div class=“segment segment3”></div>
.
.
.
<div class=“segmentN”></div>
</div>
Where N is an number defined by user so list is dynamical. For container I have applied styles to display it as grid, so EVERY time list has 3 items displayed, list is scrollable. My problem is, how can I via VanillaJS find element which is in the middle of container ? If there are 3 elements in the page, it should select 2nd one, when scrolling down it should select element which is in the middle of container every time to apply some styles to it in addition to grab it’s id. If there are 2 elements, it should select 2nd item as well. I was thinking about checking height of container, divide it by half and checking position of element if it’s in range. So far I was able to write this code in js
function findMiddleSegment() {
//selecting container
const segmentListContainer = document.querySelector(`.container`);
const rect = segmentListContainer.getBoundingClientRect();
//selecting all divs
const segments = document.querySelectorAll(`.segment`);
segments.forEach( (segment) => {
const location = segment.getBoundingClientRect();
if( (location.top >= rect.height / 2) ){
segment.classList.add(`midsegment`);
} else {
segment.classList.remove(`midsegment`);
}
});
}
But it doesn’t work. It finds element in the middle as should, but also applies style for every other element beneath middle segment. I’ve read some answers on stackoverflow, but couldn’t find any idea how to solve my problem.
EDIT
In addition to my problem I add additional function to show how I invoke it.
function handleDOMChange() {
findMiddleSegment(); //for "first run" when doc is loaded
const segmentListContainer = document.querySelector(`.container`);
segmentListContainer.addEventListener('scroll', findMiddleSegment);
}
A very easy way to do it is using the Intersection Observer:
const list = document.querySelector('ul'),
idDisplay = document.querySelector('p b');
const observer = new IntersectionObserver(
highlightMid,
{
root: list,
rootMargin: "-33.33% 0%",
threshold: .5
}
);
function makeList() {
list.innerHTML = '';
observer.disconnect();
const N = document.querySelector('input').value;
for (let i = 0; i < N;) {
const item = document.createElement('li');
item.id = `i_${++i}`;
item.textContent = `Item #${i}`;
list.append(item);
observer.observe(item);
}
};
function highlightMid(entries) {
entries.forEach(entry => {
entry.target.classList
.toggle('active', entry.isIntersecting);
})
const active = list.querySelector('.active');
if (active) idDisplay.textContent = '#' + active.id;
}
ul {
width: 50vw;
height: 50vh;
margin: auto;
padding: 0;
overflow-y: auto;
border: solid 1px;
}
li {
box-sizing: border-box;
height: 33.33%;
padding: .3em 1em;
list-style: none;
transition: .3s;
}
.active {
background: #6af;
}
<i>Make a list of:</i>
<input type="number" min="2" placeholder="number of items">
<button onclick="makeList()">make</button>
<p>Active id is <b>yet to set</b></p>
<ul></ul>
If container has only a list of segments inside, it's easer to count the element's children and find the mid element.
const segmentListContainer = document.querySelector(`.segmentListContainer`);
const midSegmentIndex = Math.floor(segmentListContainer.children.length / 2) - 1;
let midSegment = segmentListContaner.children[midSegmentIndex];
midSegment.classList.add('midsegment');
P.S.
The reason why your code adds 'mdsegment' to each element's class name after the real midsegment element is because of this conditional statement line you wrote.
if(location.top >= rect.height / 2){
segment.classList.add(`midsegment`);
}
Something like this. You can use Math.round, Math.ceil or Math.floor like I did. This works because querySelectorAll returns an array and you can use array.length to count the total number of items in the array then use a for loop to loop over all the segments and place the class based on the Math.(round, floor or ceil) based on your needs.
const container = document.querySelector(".container");
const segments = container.querySelectorAll(".segment");
const middleSegment = Math.floor(segments.length / 2);
for (let index = 0; index < segments.length; index++) {
segments[middleSegment].classList.add("middle-segment");
}
.middle-segment{
background-color: red;
}
<div class="container">
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
</div>
You don't need javascript for this. CSS will do
.container {
width: 350px;
}
.container .segment {
width: 100px;
height: 100px;
float: left;
background-color: #EEE;
border: 1px dotted gray;
margin: 3px;
text-align: center;
color: silver;
}
.segment:nth-child(3n-1) {
background-color: aquamarine;
color: black;
}
<div class="container">
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
<div class="segment">segment</div>
</div>

Prevent focus on Expand More button after content is inserted in React

I need to list out a long name list inside my page while showing all names at first is not desirable.
So I try to add an expand more button on it.
However, using a button will keep the browser focus on that button after it's pressed, left the button position unchanged on the screen while the name was inserted before that button.
On the other hand, using any, not focusable element (eg. div with onclick function) will do the desired behavior but lost the accessibility at all. Making the "button" only clickable but not focusable.
How do I make the button flushed to list bottom like the snippet div block does? Or is there a better choice to expand the existing list?
const myArray = [
'Alex',
'Bob',
'Charlie',
'Dennis',
'Evan',
'Floron',
'Gorgious',
'Harris',
'Ivan',
'Jennis',
'Kurber',
'Lowrance',
]
const ExpandList = (props) => {
const [idx, setIdx] = React.useState(8)
const handleExpand = e => {
setIdx(idx + 1)
}
return <div className='demo'>
<h1>Name List</h1>
{myArray.slice(0,idx).map(
name => <p key={name}>{name}</p>
)}
<div>
<button onClick={handleExpand} children='Button Expand' className='pointer' />
<div onClick={handleExpand} className='pointer'>Div Expand</div>
</div>
</div>
}
ReactDOM.render(<ExpandList/>, document.getElementById('root'))
.demo>p {
display: block;
padding: 20px;
color: #666;
background: #3331;
}
.demo>div>div {
display: flex;
padding: 15px;
margin-left: auto;
color: #666;
background: #3331;
}
.pointer {
cursor: pointer;
}
.pointer:hover {
background-color: #6663;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id='root' class='demo'>hello</div>
Removing focus from the button in the click handler is probably the most elegant approach: e.target.blur(). It will work on any HTML element, whether it is focusable or not (as with the div in your case).
const myArray = [
'Alex',
'Bob',
'Charlie',
'Dennis',
'Evan',
'Floron',
'Gorgious',
'Harris',
'Ivan',
'Jennis',
'Kurber',
'Lowrance',
]
const ExpandList = (props) => {
const [idx, setIdx] = React.useState(8)
const handleExpand = e => {
e.target.blur()
setIdx(idx + 1)
}
return <div className='demo'>
<h1>Name List</h1>
{myArray.slice(0,idx).map(
name => <p key={name}>{name}</p>
)}
<div>
<button onClick={handleExpand} children='Button Expand' className='pointer' />
<div onClick={handleExpand} className='pointer'>Div Expand</div>
</div>
</div>
}
ReactDOM.render(<ExpandList/>, document.getElementById('root'))
.demo>p {
display: block;
padding: 20px;
color: #666;
background: #3331;
}
.demo>div>div {
display: flex;
padding: 15px;
margin-left: auto;
color: #666;
background: #3331;
}
.pointer {
cursor: pointer;
}
.pointer:hover {
background-color: #6663;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id='root' class='demo'>hello</div>
Inspired by #MiKo, temporally unmount the button after click and set a timeout to add it back seems to do the work. Since browser lose the focus on original expand button, this will keep content flush down without focusing the original button:
const ExpandList = (props) => {
const [idx, setIdx] = React.useState(8)
const [showBtn, setShowBtn] = React.useState(true)
const handleExpand = e => {
setShowBtn(false)
setIdx(idx + 1)
setTimeout(() => setShowBtn(true), 10)
}
return <div className='demo'>
<h1>Name List</h1>
{myArray.slice(0,idx).map(
name => <p key={name}>{name}</p>
)}
{showBtn?
<div>
<button onClick={handleExpand} children='Button Expand' className='pointer' />
<div onClick={handleExpand} className='pointer'>Div Expand</div>
</div> :
<div></div>
}
</div>
}
But I'm still looking a method that doesn't need to 'unmount' a thing which should be there all time.

Giving a div a style on click

applying a class to an element only when clicked
You could make 2 different click functions. One for trap and one for the rest.
For that you need to know which ones are the other ( safe ones ). See otherDivsIds in the below code. You find the other id's using the filter function in the idArray and then loop through them ( with forEach or something else ) and add event listeners to each of them.
I would also suggest to ' swap ' the naming of the variables trapBox and trapId. Vice versa would be better
See code below
var idArray = ['one','two','three','four'];
var trapBox = idArray[Math.floor(Math.random() * idArray.length)];
var trapId= document.getElementById(trapBox);
trapId.addEventListener('click', boomClickFunction, false);
var otherDivsIds = idArray.filter(id => id !== trapBox);
otherDivsIds.forEach(id => {
safeBox = document.getElementById(id);
safeBox.addEventListener('click', safeClickFunction, false)
})
var timeoutId = window.setTimeout(ticker, 5000);
function ticker() {
document.getElementById('timesUp').innerHTML = "Time's up!";
document.body.style.backgroundColor = "black";
}
function boomClickFunction() {
this.classList.add('boom')
}
function safeClickFunction() {
this.classList.add('safe')
}
div {
width: 20px;
height: 20px;
background-color: green;
margin: 20px;
float: left;
}
.boom {
background-color: red;
}
.safe {
background-color: lightblue;
}
#timesUp {
color: white;
}
<div id='one'>
</div>
<div id='two'>
</div>
<div id='three'>
</div>
<div id='four'>
</div>
<span id="timesUp">
</span>
You can add a class to an element by using classList.add('classToBeAdded').
In your case, you could put it in your clickFunction:
trapId.classList.add('boom');

jQuery - prev/next navigation between non-siblings

I have a bunch of elements of the same type that have different parents, but I would like to be able to seamlessly navigate/cycle through all of them as if they were together.
<div>
<a href="#" class="open></a>
</div>
<div>
</div>
<div>
</div>
‹
›
I've managed to get this far: https://jsfiddle.net/pj0ecxge/
Currently it doesn't function as intended, as prev() and next() are only meant to target sibling elements, so the arrows don't work if the previous or next element is in another parent.
A single element will always be open by default, but it won't always be the same element as shown in the example. Also, only one element can be open at the same time.
If it makes a difference, I can add a single class to all children elements, but I can't change the HTML structure i.e put them all inside the same parent.
It would be nice if the navigation is infinite - i.e clicking next while the last element is open will show the first element and vice versa, but this is not required if it's too complex to do.
Thanks in advance and any help will be very appreciated!
You can check whether there are next/previous elements, if not then you can move a layer up/down like
$('.prev').click(function(e) {
e.preventDefault();
var current = $('.open');
var prev = current.prev();
if (!prev.length) {
prev = current.parent().prev('div').children('a:last-child')
}
if (prev.length) {
current.removeClass('open');
prev.addClass('open');
}
});
$('.next').click(function(e) {
e.preventDefault();
var current = $('.open');
var next = current.next();
if (!next.length) {
next = current.parent().next('div').children('a:first-child')
}
if (next.length) {
current.removeClass('open');
next.addClass('open');
}
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
text-align: center;
}
div {
font-size: 0;
}
div a {
width: 150px;
height: 150px;
border: 2px solid black;
}
a {
display: inline-block;
}
.open {
background: red;
}
.prev,
.next {
font-size: 100px;
text-decoration: none;
margin: 50px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
</div>
<div>
</div>
<div>
</div>
‹
›
Find the next set when the current set has reached either end. And the if the set is the last one then go back to the first one (and vice-versa).
$('.prev').click(function(e) {
e.preventDefault();
var current = $('.open');
var prev = current.prev();
if (!prev.length) {
prev = current.parent().prev('div').children('a:last-of-type');
if (!prev.length) {
prev = $('div:last-of-type').children('a:last-of-type');
}
}
current.removeClass('open');
prev.addClass('open');
});
$('.next').click(function(e) {
e.preventDefault();
var current = $('.open');
var next = current.next();
if (!next.length) {
next = current.parent().next('div').children('a:first-of-type');
if (!next.length) {
next = $('div:first-of-type').children('a:first-of-type');
}
}
current.removeClass('open');
next.addClass('open');
});

Categories