with jQuery and having a wrapper container around the contents within each h1, I could easily hide them.
but with no wrapper container, how would one do it?
what's the best way to do something that just hides everything before the next h1?
I'm not using jQuery because this is part of a React app.
h1 {
border-bottom: solid 1px #000;
}
span {
float: right;
}
<h1>h1 <span>x</span></h1>
<p>test 1</p>
<h2>h2</h2>
<p>test 2</p>
<h1>h1-1 <span>x</span></h1>
<p>test 3</p>
<h2>h2-2</h2>
<p>test 4</p>
or wrap everything after and before the next h1 in a div tag?
I am able to do it with this css. Just add the class "start" to the h1 from which you want to start collapsing the elements and the class "upto" to the h1 upto which you want the elements to collapse.
CSS
.start ~ *:not(h1) {
display: none;
}
.upto ~ * {
display: block !important;
}
HTML
<h1 class='start'>h1 <span>x</span></h1>
...
<h1 class ='upto'>h1-1 <span>x</span></h1>
Here all the elements between start and upto will be hidden. You can have the collapse effect by placing adding the 'start' and 'upto' classes accordingly
The addition of the class to the next h1 could be also done via Javascript. So, I can have a simple js function which sets the 'start' and the 'upto'.
function collapse(startHeaderNumber, uptoHeaderNumber) {
var allHeaders = document.getElementsByTagName("h1");
var totalNoOfHeaders = allHeaders.length;
if (startHeaderNumber > totalNoOfHeaders) {
return;
}
var startHeader = allHeaders[startHeaderNumber - 1];
startHeader.classList.add('start');
if (uptoHeaderNumber <= totalNoOfHeaders) {
var uptoHeader = allHeaders[uptoHeaderNumber - 1];
uptoHeader.classList.add('upto');
}
}
And you can simply call it like
collapse(1 ,2)
And it would collapse all the items between headers 1 and 2. Or, you can call it like,
collapse(1)
which will collapse all elements from the first header till the last.
For a full demo, please see this fiddle
sounds like you want something similar to jQuery's nextUntil() function. Here is a good guide to doing that in vanilla js. The code ends up looking like this:
var nextUntil = function (elem, selector, filter) {
// Setup siblings array
var siblings = [];
// Get the next sibling element
elem = elem.nextElementSibling;
// As long as a sibling exists
while (elem) {
// If we've reached our match, bail
if (elem.matches(selector)) break;
// If filtering by a selector, check if the sibling matches
if (filter && !elem.matches(filter)) {
elem = elem.nextElementSibling;
continue;
}
// Otherwise, push it to the siblings array
siblings.push(elem);
// Get the next sibling element
elem = elem.nextElementSibling;
}
return siblings;
};
I obviously don't know the context behind what you are doing, but I reckon there's a better way around it by altering the HTML. This could potentially even let you do some of this with just css nth child selectors
You can use previousElementSibling recursively to get all of the siblings of your h1 elements. Here is working example using previousElementSibling :
const elems = document.getElementsByTagName('h1');
for (let i = 0; i < elems.length; i++) hidePrev(elems[i]);
function hidePrev(elem)
{
var pre = elem.previousElementSibling;
if (!pre) return;
pre.style.display = 'none';
hidePrev(pre);
}
h1 {
border-bottom: solid 1px #000;
}
span {
float: right;
}
<h1>h1 <span>x</span></h1>
<p>test 1</p>
<h2>h2</h2>
<p>test 2</p>
<h1>h1-1 <span>x</span></h1>
<p>test 3</p>
<h2>h2-2</h2>
<p>test 4</p>
I have commented the code. And I hope you understand what is going on here, but if not, just comment below.
function hideUntilNextSiblingWithSameName(elementName) {
/*
* #Param {elementName: String}
* Hide all elements until the next one with the same name
*/
var trackH1 = 0,
element = document.getElementsByTagName(elementName)[0],
node = element.parentNode.firstChild;
do {
// Keep truck of element with the same name.
if (node.tagName === element.tagName) trackH1 += 1;
// Stop if catch element with same name
if (trackH1 == 2) break;
// Do not hide link, script, style and text node
if (node.nodeType === 3 || node.tagName === "LINK" || node.tagName === "SCRIPT" || node.tagName === "STYLE") continue;
// Hide element
else {
node.style.visibility = "hidden";
}
} while (node = node.nextSibling)
}
hideUntilNextSiblingWithSameName("h1")
<h1>h1 <span>x</span></h1>
<p>test 1</p>
<h2>h2</h2>
<p>test 2</p>
<h1>h1-1 <span>x</span></h1>
<p>test 3</p>
<h2>h2-2</h2>
<p>test 4</p>
<link rel="stylesheet" href="">
<script></script>
<style></style>
https://codepen.io/anon/pen/QxGVNG
At the moment, I hide the elements. But if you want to even not display the elements, use node.style.display = "none"; instead of node.style.visibility = "hidden"; in the else clause.
You can do this in a simpler way like the following. The idea is to find the immediate parent of the nodes, body in this case, and find all the immediate children of it.
Then, start removing the elements until we've found the desired element. In this example, I've tried to hide the elements, but you can simply change the code the remove them instead.
Following is a working demo:
const elems = document.querySelectorAll('body > *');
const elemBefore = document.querySelectorAll('h1')[1];
for (let i = 0; i < elems.length; i++) {
let elem = elems[i];
if (elem === elemBefore) {
break;
}
elem.style.display = 'none';
}
h1 {
border-bottom: solid 1px #000;
}
span {
float: right;
}
<h1>h1 <span>x</span></h1>
<p>test 1</p>
<h2>h2</h2>
<p>test 2</p>
<h1>h1-1 <span>x</span></h1>
<p>test 3</p>
<h2>h2-2</h2>
<p>test 4</p>
Maybe you can use a trick and hide the behind the h1 and h2 via css:
const selector = document.querySelectorAll('h1, h2');
const clickH = function (event) {
const h = event.target;
const attr = h.getAttribute('class');
if (attr === '') {
h.setAttribute('class', 'closed')
return undefined;
}
h.setAttribute('class', '')
}
selector.forEach((h) => {
h.setAttribute('class', 'closed')
h.addEventListener('click', clickH);
})
h1, h2 {
position: relative;
border-bottom: solid 1px #000;
background-color: #fff;
height: 50px;
border-bottom: solid 1px #000;
display: block;
z-index: 2;
margin-bottom: 0;
}
.closed {
margin-bottom: -50px;
}
p {
height: 30px;
}
span {
float: right;
}
<h1>h1 <span>x</span></h1>
<p>test 1</p>
<h2>h2</h2>
<p>test 2</p>
<h1>h1-1 <span>x</span></h1>
<p>test 3</p>
<h2>h2-2</h2>
<p>test 4</p>
Related
Everywhere I looked, it seemed that this problem has only been solved using jQuery, which I'm trying to remove completely from my project.
Here's the HTML:
<div class="codeblock"></div>
<div class="codeblock"></div>
<div class="codeblock"></div>
<p></p>
<div class="codeblock"></div>
<div class="codeblock"></div>
<p></p>
<div class="codeblock"></div>
desired result:
<div class="contentBox">
<div class="codeblock"></div>
<div class="codeblock"></div>
<div class="codeblock"></div>
</div>
<p></p>
<div class="contentBox">
<div class="codeblock"></div>
<div class="codeblock"></div>
</div>
<p></p>
<div class="contentBox">
<div class="codeblock"></div>
</div>
And here's how this can be done using jQuery, thanks to the many answers I've found on the topic
const e = '.codeblock';
$(e).not(e + '+' + e).each(function () {
$(this).nextUntil(':not(' + e + ')').addBack().wrapAll('<div class="contentBox" />');
});
Is there a way to replicate this same functionality using vanilla Javascript? I've tried using Element.nextElementSibling and checking if the class matches, but this approach wasn't very elegant and resulted in much more code than the jQuery solution.
Shortest version I could come up with:
let firstDivs = document.querySelectorAll('.codeblock:first-child, :not(.codeblock) + .codeblock');
firstDivs.forEach(function(div) {
let wrapper = document.createElement("div");
wrapper.className = 'wrapper';
div.parentNode.insertBefore(wrapper, div);
while(div.nextElementSibling && div.nextElementSibling.className == 'codeblock') {
wrapper.appendChild(div.nextElementSibling);
}
wrapper.insertBefore(div, wrapper.firstChild);
});
First, select the first .codeblock element out of each "group" - by selecting the element with that class that is the first child of its parent, and all those that do not have a .codeblock element before them.
For each of those elements, insert a new wrapper div before that element, then loop through the following element siblings, as long as they have that same class - and append those to the wrapper. And then afterwards, insert the first item to the beginning of the group. (If we did it before, the following elements would stop being siblings at this point.)
You could do something like this:
// Find all elements that match the class
document.querySelectorAll(`.${e}`).forEach(
// For each elemnt
elem => {
// If it's not the first of the group, skip it
if (elem.previousElementSibling!==null && elem.previousElementSibling.classList.contains(e)){
return;
}
// Find all adjacent elements with the same class
let o = [elem];
while (o[o.length - 1].nextElementSibling.classList.contains(e)) {
o.push(o[o.length - 1].nextElementSibling);
}
// Create a new wrapper element and give it a proper class
let wrapper = document.createElement('div');
wrapper.classList.add('contentBox');
// Insert the new wrapper immediatly before the group
elem.insertAdjacentElement('beforebegin', wrapper);
// Move the contents of the group to inside the wrapper element
wrapper.replaceChildren(...o);
}
)
It's a bit more code, but you can loop through all div and p, check every element and when matched append it to a new or existing div.codeBlock.
const isTargeted = el => el.classList.contains(`codeblock`);
const createWrap = (beforeEl) => beforeEl.insertAdjacentElement(`beforebegin`,
Object.assign(document.createElement(`div`), {className: `contentBox`}));
const divsAndPs = document.querySelectorAll(`div, p`);
divsAndPs.forEach(
(elem, i, self) => {
if (!i || isTargeted(elem)) {
const wrap = i && self[i-1].closest(`.contentBox`) ||
createWrap(elem);
wrap.appendChild(elem);
}
}
);
.contentBox {
color: green;
}
.contentBox .codeblock {
margin-left: 2rem;
}
.contentBox:before {
content: 'I am the great contentBox, here are my codeblocks:';
color: grey;
}
<div class="codeblock">x</div>
<div class="codeblock">x</div>
<div class="codeblock">x</div>
<p>paragraph</p>
<div class="codeblock">x</div>
<div class="codeblock">x</div>
<p>paragraph</p>
<div class="codeblock">x</div>
This can be a helper function (see also this stackblitz project):
const wrapIt = () => wrapAll(
document.querySelectorAll(`.codeblock, .codeblock + :not(.codeblock)`),
`codeblock`,
Object.assign(document.createElement(`div`), {className: `contentBox`}) );
setTimeout( wrapIt, 1000 );
function wrapAll(elems2Wrap, groupByClass, wrapperElement) {
const wrap = elem =>
elem.classList?.contains(groupByClass) && (elem
.previousElementSibling?.closest(`.${wrapperElement.className}`) ||
elem.insertAdjacentElement(`beforebegin`, wrapperElement.cloneNode())
).appendChild(elem);
elems2Wrap.forEach(wrap);
}
.contentBox {
color: green;
}
.contentBox .codeblock {
margin-left: 2rem;
}
.contentBox:before {
content: 'Wrapped!';
color: grey;
}
<div class="codeblock otherClass">x</div>
<div class="codeblock">x</div>
<div class="codeblock">x</div>
<p>Just a paragraph</p>
<div class="codeblock">x</div>
<div class="codeblock otherClass">x</div>
<p>Just a paragraph</p>
<div class="codeblock">x</div>
I have a page structured like this:
<body>
<div class="one">
<div class="two">
<p class="three">Some text</p>
</div>
</div>
<div class="four">
<div class="five">
<p class="six">Some other text</p>
</div>
</div>
</body>
Given a selector, such as .five, I want to remove all elements from the DOM while preserving the hierarchy of .four > .five > .six. In other words, after deleting all the elements, I should be left with:
<body>
<div class="four">
<div class="five">
<p class="six">Some other text</p>
</div>
</div>
</body>
I came up with the following solution to this problem:
function removeElementsExcept(selector) {
let currentElement = document.querySelector(selector)
while (currentElement !== document.body) {
const parent = currentElement.parentNode
for (const element of parent.children) {
if (currentElement !== element) {
parent.removeChild(element)
}
}
currentElement = parent
}
}
This works well enough for the above case, for which I've created a JSfiddle.
However, when I try run it on a more complex web page such as on https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild with a call such as removeElementsExcept('#sect1'), I'd expect only the blue div containing the text "Note: As long as a reference ..." and its inner contents to be kept on the page. However, if you try to run this, lots of other elements are kept on the page along with the blue div as well.
What am I doing incorrectly in my function?
This happens because you are modifying the collection which is being iterated. You can work around this by manually adjusting the index being used to look at the children.
function removeElementsExcept(selector) {
let currentElement = document.querySelector(selector)
while (currentElement !== document.body) {
const parent = currentElement.parentNode;
let idx = 0;
while (parent.children.length > 1) {
const element = parent.children[idx];
if (currentElement !== element) {
parent.removeChild(element)
} else {
idx = 1;
}
}
currentElement = parent
}
}
parent.removeChild(element) changes the length of the iterated collection so elements are skipped. You can use [...parent.children] to spread the HTMLCollection into an array, making it safe for removals.
Another approach is building a set of nodes you want to keep by traversing all child nodes and all parent nodes from the target element. Then remove all other nodes that aren't in the set. I haven't run a benchmark.
const removeElementsExcept = el => {
const keptEls = new Set();
for (let currEl = el; currEl; currEl = currEl.parentNode) {
keptEls.add(currEl);
}
for (const childEl of [...el.querySelectorAll("*")]) {
keptEls.add(childEl);
}
for (const el of [...document.querySelectorAll("body *")]) {
if (!keptEls.has(el)) {
el.remove();
}
}
};
removeElementsExcept(document.querySelector(".five"));
.four {
background: red;
height: 100px;
padding: 1em;
}
.five {
background: blue;
height: 100px;
padding: 1em;
}
.six {
background: yellow;
height: 100px;
padding: 1em;
}
<div class="one">
<div class="two">
<p class="three">Some text</p>
</div>
</div>
<div class="four">
<div class="five">
<p class="six">Some other text</p>
</div>
</div>
I have a code written like this. I would like the myDIV1 to display when I click the first section with 'HELLO' quote, and myDIV2 when second section 'HI' is clicked.
Tried using event.target to change javascript var X into myDIV1 or myDIV2 but not so sure if my syntax used is correct or if the logic I tried can be possible.
I would like to change var x = document.getElementbyId( 'either of the two divs here' )
when 1st section or 2nd section is click. (sorry for the bad grammar)
html
<section class="news"> HELLO
</section>
<section class="news"> HI
</section>
<div class="divelement "id="myDIV1">
This is my DIV1 element.
</div>
<div class="divelement "id="myDIV2">
This is my DIV2 element.
javascript
<script>
function myFunction() {
var x = document.getElementById("myDIV");
if (x.style.display === "none") {
x.style.display = "block";
y.style.display = "block";
} else {
x.style.display = "none";
y.style.display = "none";
}
}
</script>
You can do it by this way using jQuery.
jQuery('.news').click(function(){
var target = jQuery(this).attr('data-target');
jQuery('.divelement').hide(); /*hide all divelement*/
jQuery(target).toggle(); /*Show the targeted divelement*/
console.log('Show' + target);
});
.divelement{
display: none; /* By default divelement will be hidden */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section class="news" data-target="#myDIV1"> HELLO
</section> <!-- set targeted div id in data-target attribute -->
<section class="news" data-target="#myDIV2"> HI
</section>
<div class="divelement "id="myDIV1">
This is my DIV1 element.
</div>
<div class="divelement "id="myDIV2">
This is my DIV2 element.
</div>
Here you don't need id-s at all. Can make simple tabs with already existing classes:
let news = document.querySelectorAll('.news');
let divs = document.querySelectorAll('.divelement');
// saved element lists to variables
// `news[0]` — the first element, `news[1]` — second, etc.
let prev = 0; // index of previous clicked section
for( let i = 0; i < news.length; i++ ){
// Google → for loop, JS for loop let
news[i].addEventListener('click', function(){
news[prev].classList.remove('active');
divs[prev].classList.remove('active');
// removing class from last section and div
news[i].classList.add('active');
divs[i].classList.add('active');
// adding to current clicked, by index ( `i` - exact number, got from `for` )
prev = i; // updating previous clicked index
});
}
.divelement {
display: none;
margin: 5px;
padding: 5px;
border: 2px solid orange;
}
.news { cursor: pointer; margin: 5px; }
.divelement.active {
display: block;
}
.news.active {
color: red;
}
<section class="news">HELLO</section>
<section class="news">HI</section>
<section class="news">BUBU</section>
<div class="divelement">HELLO</div>
<div class="divelement">HI</div>
<div class="divelement">BUBU</div>
If you are 100% sure, that there will be only two elements, can use such trick:
let news = document.querySelectorAll('.news');
let divs = document.querySelectorAll('.divelement');
setDivToggle(0);
setDivToggle(1);
function setDivToggle(index) {
news[index].addEventListener('click', function(){
let hide = +!index; // (*1)
divs[index].style.display = "block";
divs[hide].style.display = "none";
});
}
.news { cursor: pointer; margin: 10px; }
.divelement { display: none; }
<section class="news"> HELLO </section>
<section class="news"> HI </section>
<div class="divelement"id="myDIV1">DIV1 element.</div>
<div class="divelement"id="myDIV2">DIV2 element.</div>
(*1) let hide = +!index; — clicked index always will be 0 or 1.
In boolean expressions, 0 == false, 1 == true.
! — logical "NOT" operator. Converts false → true and vice versa.
+ converts the result into number.
Little demo:
console.log( +false ) // 0
console.log( +true ) // 1
console.log( !0 ) // true
console.log( !1 ) // false
console.log( +!0 ) // 1
console.log( +!1 ) // 0
In fact, it's just twisting 1 with 0 and 0 with 1.
Have a problem and can't get to solve it. Tried to use QuerySelectorAll and comma separating with GetElementsByClassName, but that didn't work, so I am wondering how to solve this problem.
I have this HTML:
<div class="area">Test title
<div class="some content" style="display: none">blablbala
<input></input>
</div>
<div class="two">This should be clickable too</div>
</div>
<div class="area">
Test title
<div class="some content">
blablbala
<input></input>
</div>
<div class="two">This should be clickable too</div>
</div>
JS:
function areaCollapse() {
var next = this.querySelector(".content");
if (this.classList.contains("open")) {
next.style.display = "none";
this.classList.remove("open");
} else {
next.style.display = "block";
this.classList.add("open");
}
}
var classname = document.getElementsByClassName("area");
for (var i = 0; i < classname.length; i++) {
classname[i].addEventListener('click', areaCollapse, true);
}
http://jsfiddle.net/1BJK903/nb1ao39k/6/
CSS:
.two {
position: absolute;
top: 0;
}
So now, the div with classname "area" is clickable. I positioned the div with class "two" absolute and now the whole div is clickable, except where this other div is. If you click on the div with classname "two", it doesn't work (it does not collapse or open the contents). How can I make this work, without changing the structure?
One way is using a global handler, where you can handle more than one item by checking its id or class or some other property or attribute.
Below snippet finds the "area" div and pass it as a param to the areaCollapse function. It also check so it is only the two or the area div (colored lime/yellow) that was clicked before calling the areaCollapse.
Also the original code didn't have the "open" class already added to it (the second div group), which mean one need to click twice, so I change the areaCollapse function to check for the display property instead.
function areaCollapse(elem) {
var next = elem.querySelector(".content");
if (next.style.display != "none") {
next.style.display = "none";
} else {
next.style.display = "block";
}
}
window.addEventListener('click', function(e) {
//temp alert to check which element were clicked
//alert(e.target.className);
if (hasClass(e.target,"area")) {
areaCollapse(e.target);
} else {
//delete next line if all children are clickable
if (hasClass(e.target,"two")) {
var el = e.target;
while ((el = el.parentElement) && !hasClass(el,"area"));
if (targetInParent(e.target,el)) {
areaCollapse(el);
}
//delete next line if all children are clickable
}
}
});
function hasClass(elm,cln) {
return (" " + elm.className + " " ).indexOf( " "+cln+" " ) > -1;
}
function targetInParent(trg,pnt) {
return (trg === pnt) ? false : pnt.contains(trg);
}
.area {
background-color: lime;
}
.two {
background-color: yellow;
}
.area:hover, .two:hover {
background-color: green;
}
.some {
background-color: white;
}
.some:hover {
background-color: white;
}
<div class="area">Test title clickable 1
<div class="some content" style="display: none">blablbala NOT clickable 1
</div>
<div class="two">This should be clickable too 1</div>
</div>
<div class="area">Test title clickable 2
<div class="some content">blablbala NOT clickable 2
</div>
<div class="two">This should be clickable too 2</div>
</div>
<div class="other">This should NOT be clickable</div>
You need to find your two elements while you're binding classname, and bind that as well.
var classname = document.getElementsByClassName("area");
for(var i=0; i < classname.length; i++){
classname[i].addEventListener('click', areaCollapse, true);
var twoEl = classname[i].getElementsByClassName("two")[0];
twoEl.addEventListener('click', function(e) { console.log('two clicked'); });
}
If you want to use jQuery:
$('.two').click(function(){
//action here
});
this is my html with css and javascript all in one place
JS
var links = document.getElementsByTagName("a"); //get the links
var len = links.length;
for(var i = 0; i<len; i++) {
links[i].onclick = handleClick; // add onclick handler
}
function handleClick(e){
var target = e.target;
var id = target.id + "content";
document.getElementById(id).style.zIndex = 10;
}
HTML
<div id="tabbed">
Tabe1
<div class="section" id="tabe1content">
<div>
<p> content1 </p>
</div>
</div>
Tabe2
<div class="section" id="tabe2content">
<div>
<p> content2 </p>
</div>
</div>
Tabe3
<div class="section" id="tabe3content">
<div>
<p> content3 </p>
</div>
</div>
</div>
CSS
.section{
position:absolute;
float:left;
width:500px;
background-color:gray;
left:0;
top:0;
height:300px;
margin-top:30px;
}
#tabbed{
position:relative;
}
a {
margin-right:10px;
float:left;
display:block;
}
When I test it, it only works once. The second time when I click on table 1 it still shows table 3. Please take a look at what is wrong and is there any other way which is better then mine?
delegate on parent
track the biggest z-index value
http://jsfiddle.net/3LuC4/6/
.section{
position:absolute;
float:left;
width:500px;
background-color:gray;
left:0;
top:0;
height:300px;
margin-top:30px;
z-index: 1;
}
#tabbed{
position:relative;
}
a {
margin-right:10px;
float:left;
display:block;
}
<div id="tabbed">
Tabe1
<div class="section" id="tabe1content">
<div>
<p> content1 </p>
</div>
</div>
Tabe2
<div class="section" id="tabe2content">
<div>
<p> content2 </p>
</div>
</div>
Tabe3
<div class="section" id="tabe3content">
<div>
<p> content3 </p>
</div>
</div>
</div>
var root = document.getElementById("tabbed");
var veryTop = 2;
root.onclick = handleClick;
function handleClick(e){
var target = e.target;
if ( target.tagName !== 'A' ) { e.preventDefault(); return; }
var tab = document.getElementById( target.id + 'content' );
tab.style.zIndex = ( veryTop++ ).toString();
e.preventDefault();
}
PS. Should solve your 'works once' problem.
When you click on a tab, near the end of your JS, you set the z-index of each tab to 10. After you've done this, they do NOT change back.
Inside the onclick handler you need to build a string that sets all OTHER z-indexes to a lower number; I'm not sure of the exact way to do this, but you seem to know enough to complete that part.
You can iterate over your section class, reset the z-index for all, then set your new one:
var elems = document.getElementsByClassName("section");
for (var i = 0; i < elems.length; i++) {
elem[i].style.zIndex = 0; //or whatever your default is
}
var target = e.target;
var id = target.id + "content";
document.getElementById(id).style.zIndex = 10;
Your z-indexes all end up the same. Add a little function to sort them:
function sortZindex(){
for(var i = 0; i<len; i++) {
var id = links[i].id + "content";
document.getElementById(id).style.zIndex = 9;
}
}
http://jsfiddle.net/U2qfj/