I'm trying to get the closest ancestor element to a clicked element, from an array of provided elements. When a user clicks on a tab, I need to find out where on the page that tab is.
I have something kind of working, however this script thinks main is the closest tag, when it should be article.
If I put article before main in the array it works just fine, however I want this to work regardless of array order. Any ideas?
// Tab Click
var tabs = Array.from(document.querySelectorAll('details.company-details'));
function handleTabClick(e) {
var tabNode = e.target;
tabLabel = tabNode.innerText;
const tgt = e.target;
var location;
var elements = ['section', 'header', 'main', 'nav', 'article'];
for (let element of elements) {
if (tgt.closest(element)) {
location = element;
break;
};
}
console.log(location);
};
tabs.forEach(function(tab) {
tab.addEventListener('click', handleTabClick);
});
<main class="company-main-wrapper" id="maincontent">
<article>
<div class="company-grid-row">
<div class="company-grid-column-two-thirds">
<div class="company-page-content">
<div class="block-expander">
<details class="company-details company-expander" company-polyfilled="true" id="company-details0" open="">
<summary class="company-details__summary" role="button" aria-controls="company-details__text0" tabindex="0" aria-expanded="true">
<span class="company-details__summary-text">
sampoe page
</span>
</summary>
</details>
</div>
</div>
</div>
</div>
</article>
</main>
See the comments inline below:
// Don't need Array.from() because the node list returned from
// .querySelectorAll() supports .forEach().
document.querySelectorAll('details.company-details').forEach(function(tab) {
tab.addEventListener('click', handleTabClick);
});
var elements = ['section', 'header', 'main', 'nav', 'article'];
let found = false;
let parent = null;
function handleTabClick(e) {
// Start at the parent of the tab
parent = this.parentNode;
// Keep looping until we find an ancestor that matches
while (found === false){
// Check the name of the parent element to see if its lower
// cased name is in the array.
if(elements.includes(parent.nodeName.toLowerCase())){
found = true; // Found a match
} else {
// No match. Get the next parent and keep looping
parent = getParent(parent);
}
}
console.log(parent);
};
function getParent(element){
return element.parentNode;
}
<main class="company-main-wrapper" id="maincontent">
<article>
<div class="company-grid-row">
<div class="company-grid-column-two-thirds">
<div class="company-page-content">
<div class="block-expander">
<details class="company-details company-expander" company-polyfilled="true" id="company-details0" open="">
<summary class="company-details__summary" role="button" aria-controls="company-details__text0" tabindex="0" aria-expanded="true">
<span class="company-details__summary-text">
sampoe page
</span>
</summary>
</details>
</div>
</div>
</div>
</div>
</article>
</main>
Related
I have these elements on my page:
<div id="123test"><p>test</p></div>
<div id="123test"><p>othertext</p></div>
And I am trying to remove the div if it contains "test" text inside, using Java Script, but it does not seem to work.
Here is my JS:
var container = document.getElementById('123test');
if (container.textContent=='test') {
container.style.display="none";
};
var container = document.getElementById('123test');
if (container.textContent == 'test') {
container.style.display = "none";
};
<div id="123test"><p>test</p></div>
<div id="123test"><p>othertext</p></div>
I also tried using :contains selector way, but the result was the same. The style of the container does not change at all. What do I do wrong? Is there another approach possible? This code is a simplified version of my project, but neither of these two work. I would be very gratefull if someone would help me to overcome the issue.
Make sure your HTML looks exactly like this:
<div id="123test"><p>test</p></div>
<!-- no whitespace or line breaks before or after <p>test</p> -->
and not like this
<div id="123test">
<p>test</p>
</div>
To avoid this problem, call trim() on container.textContent:
var container = document.getElementById('123test');
if (container.textContent.trim() == 'test') {
container.style.display = "none";
};
<div id="123test">
<p>test</p>
</div>
<div id="123test2">
<p>othertext</p>
</div>
And I am trying to remove the div if it contains "test" text inside, using Java Script [...]
If it is sufficient that test is contained, check for includes('test') instead:
var container = document.getElementById('123test');
if (container.textContent.includes('test')) {
container.style.display = "none";
};
<div id="123test">
<p>test123</p>
</div>
<div id="123test2">
<p>othertext</p>
</div>
Important sidenote: You cannot have more than one element with the same id.
Sidenote 2: :contains only exists in jQuery, not in CSS.
Sidenote 3 about using innerText: This had been my first approach, but for some strange reason on Safari/MacOS it won't hide the container:
var container = document.getElementById('123test');
if (container.innerText == 'test') {
container.style.display = "none";
};
console.log(container.innerText.length); // 6 (!) on Safari/MacOS
<div id="123test">
<p>test</p>
</div>
<div id="123test2">
<p>othertext</p>
</div>
Here is an example of checking an arbitrary DOM tree for text nodes that contain the string "test" and then hiding them by setting display: none;.
The function hideTestNodes accepts NodeList and primarily makes use of nodeType and textContent.
const wrapper = document.getElementById('wrapper')
const hideTestNodes = (children) => {
for (const child of children) {
if (child.nodeType === 1) {
// If the node is another Element, check its child nodes
hideTestNodes(child.childNodes)
}
// If the node is a text node and the content includes 'test'
// then hide the parent element containing the text node
if (child.nodeType === 3 && /test/i.test(child.textContent)) {
child.parentElement.style.display = 'none'
}
}
}
hideTestNodes(wrapper.childNodes)
<div id="wrapper">
<div>
<p>test</p>
</div>
<div>
<p>some other text</p>
</div>
<div>
<p>some deeply <span> nested test</span> text</p>
</div>
<div>
<p>this <span>includes</span> test.</p>
</div>
</div>
If you want to do that to only one specific div, then it's very simple:
NOTE: I added two seconds to let you see how it happens. you can remove the timer.
let test1 = document.getElementById("test1");
if (test1.innerHTML.includes("test")) {
setTimeout(() => {
test1.style.display = "none";
}, 2000);
}
<div id="container">
<div id="test1"><p>test</p></div>
<div id="test2"><p>othertext</p></div>
</div>
If you want to check all divs, it's still simple and you just need to make an array from them then use forEach loop to check all divs in that container:
let divs = document.getElementById("container").children;
[...divs].forEach(div => {
if (div.textContent.includes("test")) {
setTimeout(() => {
div.style.display = "none";
}, 2000);
}
});
<div id="container">
<div id="test1"><p>test</p></div>
<div id="test2"><p>othertext</p></div>
</div>
I have a javascript loop that contains a conditional statement.
I need the condition to check whether the current item in the loop contains any element with a specific class name.
This is what I currently have, however as you'll see this is not correct.
Currently my condition checks whether the current element has a specific class name, HOWEVER, I need it to check whether ANY element inside the element has a specific class name.
selectionList.forEach(selectionItem => {
if (selectionItem.classList.contains('myclass')) {
// do something
} else {
// do something different
}
}
EDIT:
I'm editing this post because it seems that people are misunderstanding.
I just need help with the following line in my code:
if (selectionItem.classList.contains('myclass')) {
The above checks whether the current element has a specific class name, but I need it to check whether the current element contains any CHILD element with a specific class name.
You can use .querySelector(selector) to search for children of some element.
!!element.querySelector(selector) will return true if it finds something matching selector inside that element, and will return false otherwise.
let elements = document.querySelectorAll('.outer');
elements.forEach(el => {
let hasChildren = !!el.querySelector('.searchClass');
console.log(`${el.classList[1]} has children with .searchClass: ${hasChildren}`);
});
<div class="outer outer-1">
<div class="inner searchClass"></div>
</div>
<div class="outer outer-2">
<div class="inner"></div>
</div>
<div class="outer outer-3">
<div class="inner">
<div class="inner searchClass"></div>
</div>
</div>
You can use Element.querySelector(selector) or either Element.querySelectorAll(selector).length in order to check if there is a child with the given selector:
yourElements.forEach(element => {
if (element.querySelector(yourSelector)) {
// do something
} else {
// do something different
}
})
// if there is no "something different" to do would be like this
yourElements.filter(elem => !!elem.querySelector(yourSelector))
.forEach(element => {
// do something
})
In your case could be something like
selectionList.forEach(selectionItem => {
if (selectionItem.querySelector('.myclass')) {
// do something
} else {
// do something different
}
})
Instead of a loop, just call .querySelectorAll() with the class name passed in (prefixed with a .) on the element you wish to examine to return a collection of all the elements that contain that class. You can check the resulting collection's .length to see how many elements were found and you can loop over the collection (with .forEach()) to enumerate them individually.
// Find every element that has the myclass class and put them in a collection
let matchingElements = document.querySelectorAll(".match");
let count = matchingElements.length; // Count how many there are
// Loop over the collection
matchingElements.forEach(function(element){
// Do whatever you want with the current element being enumerated
console.log(element.textContent);
});
<div class="needsChecking">
<p>something</p>
<p class="match">bingo</p>
stuff
<div>
<div class="match">bingo
<h1 class="match">bingo</h1>
<h2 class="nomatch"></h2>
</div>
</div>
<div class="needsChecking">
<p>something</p>
<p class="match">bingo</p>
stuff
<div>
<div class="match">bingo
<h1 class="match">bingo</h1>
<h2 class="nomatch"></h2>
</div>
</div>
<div class="needsChecking">
<p>something</p>
<p class="match">bingo</p>
stuff
<div>
<div class="match">bingo
<h1 class="match">bingo</h1>
<h2 class="nomatch"></h2>
</div>
</div>
You can use the children property.
See my snippet below, I've included some notes.
//get parent element
var parent = document.querySelector('.parent');
//get children
var children = parent.children;
//run a loop and check if children have a specific class name
for (let i = 0; i < children.length; i++) {
if (children[i].classList.contains('child1')) {
//log the children with class name child1
console.log(children[i])
}
}
<div class="parent">
<p class="child1"></p>
<p class="child1"></p>
<p class="child2"></p>
<p class="child3"></p>
</div>
I'm trying to make active tabs that show content depending on which tab you click.
I absolutely need the first tab to be active by default so i put the "active" class manually in the HTML so the base content is shown.
The issue is, i do that through an empty variable to hold the state but after you've already clicked once. Here is the code :
function homeLoaded() {
menuLoaded();
function menuLoaded() {
const sideMenuChildren = document.getElementById("sidemenu").childNodes;
let currentTab;
for(let i = 0; i < sideMenuChildren.length; i++) {
const tab = sideMenuChildren[i];
tab.addEventListener(
"click",
function() {
if(currentTab) {
sideMenuChildren[currentTab].classList.remove("liactive");
}
currentTab = i;
sideMenuChildren[currentTab].classList.add("liactive");
showContent(tab.id);
console.log("I clicked " + i);
}
);
}
}
let currentPage;
function showContent(menuName) {
const pageContainer = document.getElementById("content");
const element = pageContainer.querySelector("." + menuName);
if(currentPage) {
currentPage.classList.remove("selected");
}
currentPage = element;
element.classList.add("selected");
}
}
Here is the HTML :
<div id="main" style="background-image: url(./images/bg2.png);">
<div id="box">
<ul id="sidemenu">
<li id="profil" class="liactive">Profil</li>
<li id="parcours">Parcours</li>
<li id="contact">Contact</li>
</ul>
<div id="content">
<div class="profil selected"><p>Blabla</p></div>
<div class="parcours"><p>Blablabla</p></div>
<div class="contact"><p>Blablablabla</p></div>
</div>
<div id="bottomnav">
<div id="bottomnavcontent">
</div>
</div>
</div>
</div>
What this does sadly is that when i click another tab...it does not remove the first active tab, so i have 2 active tabs until i re-click the first tab to store it in the variable. I don't know how to fix this.
demo of working codeYou can get event inside the event listener function. There you can get the target.
So,first Remove active class of current tab. Then, set active class for target element.
i.e
tab.addEventListener(
"click",
function (ev) {
const currentab = document.querySelector(".liactive");
currentab.classList.remove("liactive")
ev.target.classList.add("liactive")
}
)
I have DOM elements as shown below. I want to sort it on the basis of href attribute.
This is what I have tried in JS but more need to be done.
document.addEventListener("DOMContentLoaded", function () {
let elems = Array.from(document.querySelectorAll(".house-senate a"));
elems.sort((a, b) => a.textContent.localeCompare(b.textContent));
});
Problem Statement:
I am wondering what JS code I need to add so that it sorts everything on the basis of href attributes.
You're close, but:
You need to actually move them in the DOM.
You're potentially sorting ones that aren't in the same parent (though they all are in your example HTML).
blex pointed out to me that you want to sort by the category in the href, not by the href itself. In your example, it comes to the same thing because the text prior to the category in all the hrefs is the same, but still, perhaps better to extract it.
This is blex's function for extracting it:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
Or if you want to be more rigorous about extracting that parameter from the query string, this collaborative answer originally by Code Spy shows how to do that.
See comments for more:
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
});
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
If you need to add more sorting criteria (per your comment under the question), just add them in the sort callback; this question has answers showing how to sort an array of objects on multiple criteria.
I've assumed above that there aren't hundreds of these links. If there are, and if you see a performance problem with the above, you can remove the container from the DOM before moving the links around within it, then put it back:
Live Example:
function getLinkCategory(a) {
const matches = a.href.match(/category=([a-z]+)/i);
return matches ? matches[1] : '';
}
document.addEventListener("DOMContentLoaded", function () {
// Get the container
const container = document.querySelector(".house-senate");
// Remember its parent and following sibling and remove it
const parent = container.parentNode;
const sibling = container.nextSibling;
parent.removeChild(container);
// Get its immediate child `a` elements
const elems = [...container.children].filter(child => child.tagName === "A");
// Sort them
elems.sort((a, b) => getLinkCategory(a).localeCompare(getLinkCategory(b)));
// Add them back, which moves them
for (const el of elems) {
container.appendChild(el);
}
// Put the container back -- note this works even if the
// container was the last child in the parent
// and `sibling` is `null`.
parent.insertBefore(container, sibling);
});
<div>This is before the <code>div</code> with the links in it.</div>
<div class="house-senate widget widget-cpac-depth -horizontal">
<h1 class="widget__title">Committees</h1>
<a href="/en/?s=&category=BOIE">
<div class="committee">
<div class="color-green">BOIE</div>
<p>Board of Internal Economy</p>
</div>
</a>
<a href="/en/?s=&category=CACN">
<div class="committee">
<div class="color-green">CACN</div>
<p>Canada-China Relations</p>
</div>
</a>
<a href="/en/?s=&category=CHPC">
<div class="committee">
<div class="color-green">CHPC</div>
<p>Canadian Heritage</p>
</div>
</a>
<a href="/en/?s=&category=CIIT">
<div class="committee">
<div class="color-green">CIIT</div>
<p>International Trade</p>
</div>
</a>
</div>
<div>This is after the <code>div</code> with the links in it.</div>
Note: You're using modern language features, but the above relies on a modern browser feature (NodeList being iterable). If you're transpiling, it may not be that all the browsers you're targeting have the necessary feature, but for anything even vaguely modern, you can polyfill it; see my answer here for details.
I'm trying to add an event listener on some repeating innerHTML. I.E for every lot of HTML added by innerHTML, I'll also need to add a click event onto it.
To complicate things I'm also importing a data set from another JS file imported under the name data. As you can see in the code I need the data inside the event listener to be specific to the for loop iteration of the innerHTML so that when I fire the event listener I can see the correct, corresponding data.
This is my attempt:
JS:
import data from './data.js';
import img from './images.js';
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
for (let i = 0; i < data.lists.length; i++) {
let obj = eval(data.lists[i]);
let totalReminders = getTotalReminders(obj);
lists.innerHTML += `
<div class="flex-between row list">
<h4>${obj.name}</h4>
<div class="flex-between sections">
<h4 class="number-width flex-c">${obj.items.length}</h4>
<h4 class="number-width flex-c">${totalReminders}</h4>
<div class="text-width flex-end">
<h4 class="date">${obj.created}</h4>
<img class="img-radius" src="${img.symbols[3]}" alt="Delete Bin">
</div>
</div>
</div>
`;
const list = document.querySelector('.list');
list.addEventListener('click', () => { // click event
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
};
};
const getTotalReminders = passed => { // find total reminders
let total = 0;
for (let i = 0; i < passed.items.length; i++) {
total += passed.items[i].reminders;
};
return total;
};
At the moment ONLY the first iteration of innerHTML += has an event listener attached and when I click on it I see the data that should be corresponding the last iteration.
What am I doing wrong here?
You need to move the code that sets up the event handlers so that it is outside of your for loop and runs after that loop is finished. Then, instead of .querySelector(), which only returns the first matching element, you need .querySelectorAll() to return all matching elements. After that, you'll loop through all those elements and set up the handler.
You'll also need to change how your obj variable is declared so that it will be in scope outside of the for loop. Do this by declaring it just before the loop, but assigning it inside the loop:
let obj = null; // Now, obj is scoped so it can be accessed outside of the loop
for (let i = 0; i < data.lists.length; i++) {
obj = eval(data.lists[i]);
And, put the following just after the for loop finishes:
// Get all the .list elements into an Array
const list = Array.prototype.slice.call(document.querySelectorAll('.list'));
// Loop over the array and assign an event handler to each array item:
list.forEach(function(item){
item.addEventListener('click', () => {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
});
});
With all this said, your approach here is really not very good. There is almost always another option than to use eval() for anything and using .innerHTML is usually something to avoid due to its security and performance implications. Using it in a loop is almost always a bad idea. You really should be using the DOM API to create new elements, configure them and inject them into the DOM. If you must use .innerHTML, then build up a string in your loop and after the loop, inject the string into the DOM via .innerHTML, just once.
One options is to look at event delegation/bubbling. The basic principle here is you add the event handler to a parent object, in this case <div id="lists"></div>. Then when the event is fired you query the target of that event to see if it matches your element.
Using this technique you don't have to re-bind event handlers when new items are added, particularly useful if the items are added by user interaction.
In your case it would look something like:
export const lists = () => {
const main = document.getElementById('main');
main.innerHTML = `
<div class="main-container">
<div class="flex-between row border-bottom">
<div class="flex new-list">
<img class="create-img img-radius" src="${img.symbols[0]}" alt="Delete Bin">
<h3>New List</h3>
</div>
<div class="flex-between sections">
<h3 class="text-width flex-c">Items:</h3>
<h3 class="text-width flex-c">Reminders:</h3>
<h3 class="text-width flex-end">Created:</h3>
</div>
</div>
<div id="lists"></div>
</div>
`;
const lists = document.getElementById('lists');
//Now that the parent element is added to the DOM
//Add the event handler
lists.addEventListener("click",function(e) {
// e.target was the clicked element
if (e.target && e.target.matches(".list")) {
listNav.listNav(obj.name);
listSidebarL.listSidebarL();
listSidebarR.listSidebarR();
listMain.listMain(obj.items);
}
//Add Items etc
});
NOTE Scots comments re eval and innerHTML apply equally to this answer.