Removing all nodes from the DOM except for one subtree - javascript

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>

Related

How do I wrap adjacent elements of the same class using Javascript (no jQuery)

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>

JavaScript loop throught and hide all divs except one

I have a big list of getElementsById that looks like this:
let ahSummerCamps = document.getElementById("aH-summerCamps");
let ahTrekking = document.getElementById("aH-trekking");
let bpLongDistanceHiking = document.getElementById("bp-longDistanceHiking");
let bpThruHiking = document.getElementById("bp-thruHiking");
let cOceanCruising = document.getElementById("c-oceanCruising");
let cRiverCruising = document.getElementById("c-riverCruising");
let eRecreationsEvent = document.getElementById("e-recreationsEvent");
let eSportsEvent = document.getElementById("e-sportsEvents");
let phEscortedTours = document.getElementById("ph-escortedTours");
let phIndependentTours = document.getElementById("ph-independentTours");
let sPhotographicSafari = document.getElementById("s-photographicSafari");
let sCyclingSafari = document.getElementById("s-cyclingSafari");
let sAsBackcountrySkiing = document.getElementById("sAs-backcountrySkiing");
let sAsDownhillSkiing = document.getElementById("sAs-downhillSkiing");
let vChildcare = document.getElementById("v-childcare");
let vConservationAndEnvironment = document.getElementById("v-conservationAndEnvironment");
I won't go into details, is there any way that I can loop throught all of them and for example hide all except bpLongDistanceHiking and bpThruHiking .
I had this function but that is not good way for sure:
function hideAllExceptbP() {
ahSummerCamps.style.display = "none";
ahTrekking.style.display = "none";
/* ... and for all of them like this except bpLongDistanceHiking and bpThruHiking */
}
So on that way I need function for every element like:
function hideAllExceptaH() {
/* ... + 10 lines */
function hideAllExceptC() {
/* ... + 10 lines */
function hideAllExceptE() {
/* ... + 10 lines */
Function
function choice() {
backpacking.addEventListener("click", () => {
hideBackpacking();
hideSafari();
hidePackageHoliday();
showBackpackingOptions();
console.log("Backpacking");
});
So as you can see from html I have 8 divs at start, and when user click on backpacking for example function should hide all divs except bpLongDistanceHiking and bpThruHiking
Full code calling function:
HTML:
<div class="row">
<!--main divs-->
<div style="background-color: #4cc9f0" class="column" id="adventureHolidays"><p>Adventure Holidays</p></div>
<div style="background-color: #4895ef" class="column" id="backpacking"><p>Backpacking</p></div>
<div style="background-color: #4361ee" class="column" id="cruiseHolidays"><p>Cruise Holidays</p></div>
<div style="background-color: #4361ee" class="column" id="eventTravel"><p>Event Travel</p>
</div>
<div style="background-color: #3a0ca3" class="column" id="packageHoliday"><p>Package Holiday</p></div>
<div style="background-color: #480ca8" class="column" id="safari"><p>Safari</p></div>
<div style="background-color: #560bad" class="column" id="skiingAndSnowboarding"><p>Skiing and Snowboarding</p>
</div>
<div style="background-color: #7209b7" class="column" id="volunteering"><p>Volunteering</p></div>
<!--end on main divs-->
<!--sub divs-->
<div style="background-color: #4cc9f0" class="column" id="aH-summerCamps"><p>Summer camps</p></div>
<div style="background-color: #4895ef" class="column" id="aH-trekking"><p>Trekking</p></div>
<div style="background-color: #4361ee" class="column" id="bP-longDistanceHiking"><p>Long Distance Hiking</p></div>
<div style="background-color: #4361ee" class="column" id="bP-thruHiking"><p>Thru Hiking</p></div>
<div style="background-color: #3a0ca3" class="column" id="c-oceanCruising"><p>Ocean Cruising</p></div>
<div style="background-color: #480ca8" class="column" id="c-riverCruising"><p>River Cruising</p></div>
<div style="background-color: #560bad" class="column" id="e-recreationsEvent"><p>Recreations Events</p></div>
<div style="background-color: #7209b7" class="column" id="e-sportsEvents"><p>Sports events</p></div>
<div style="background-color: #4cc9f0" class="column" id="pH-escortedTours"><p>Escorted Tours</p></div>
<div style="background-color: #4895ef" class="column" id="pH-independentTours"><p>Independent Tours</p></div>
<div style="background-color: #4361ee" class="column" id="s-photographicSafari"><p>Photographic Safari</p></div>
<div style="background-color: #4895ef" class="column" id="s-cyclingSafari"><p>Cycling Safari</p></div>
<div style="background-color: #3a0ca3" class="column" id="sAs-backcountrySkiing"><p>Backcountry skiing</p></div>
<div style="background-color: #560bad" class="column" id="sAs-downhillSkiing"><p>Downhill skiing</p></div>
<div style="background-color: #4361ee" class="column" id="v-childcare"><p>Childcare</p></div>
<div style="background-color: #4895ef" class="column" id="v-conservationAndEnvironment"><p>Conservation And
Environment</p></div>
</div>
<script type="text/javascript" th:src="#{/js/index.js}"></script>
js:
let adventureHolidays = document.getElementById("adventureHolidays");
let backpacking = document.getElementById("backpacking");
let cruiseHolidays = document.getElementById("cruiseHolidays");
let eventTravel = document.getElementById("eventTravel");
let packageHoliday = document.getElementById("packageHoliday");
let safari = document.getElementById("safari");
let skiingAndSnowboarding = document.getElementById("skiingAndSnowboarding");
let volunteering = document.getElementById("volunteering");
/*End of inserting main variables*/
/*sub variables*/
let ahSummerCamps = document.getElementById("aH-summerCamps");
let ahTrekking = document.getElementById("aH-trekking");
let bPLongDistanceHiking = document.getElementById("bP-longDistanceHiking");
let bPThruHiking = document.getElementById("bP-thruHiking");
let cOceanCruising = document.getElementById("c-oceanCruising");
let cRiverCruising = document.getElementById("c-riverCruising");
let eRecreationsEvent = document.getElementById("e-recreationsEvent");
let eSportsEvent = document.getElementById("e-sportsEvents");
let phEscortedTours = document.getElementById("ph-escortedTours");
let phIndependentTours = document.getElementById("ph-independentTours");
let sPhotographicSafari = document.getElementById("s-photographicSafari");
let sCyclingSafari = document.getElementById("s-cyclingSafari");
let sAsBackcountrySkiing = document.getElementById("sAs-backcountrySkiing");
let sAsDownhillSkiing = document.getElementById("sAs-downhillSkiing");
let vChildcare = document.getElementById("v-childcare");
let vConservationAndEnvironment = document.getElementById("v-conservationAndEnvironment");
/*end of subs variables*/
const divs = ["adventureHolidays", "backpacking", "cruiseHolidays", "eventTravel", "packageHoliday", "safari", "skiingAndSnowboarding", "volunteering", "aH-summerCamps", "aH-trekking", "bP-longDistanceHiking", "bP-thruHiking", "c-oceanCruising", "c-riverCruising", "e-recreationsEvent", "e-sportsEvents", "ph-escortedTours", "ph-independentTours", "s-photographicSafari", "s-cyclingSafari", "sAs-backcountrySkiing", "v-conservationAndEnvironment", "sAs-downhillSkiing", "v-childcare"]
.map(id => document.getElementById(id)); // gives you an array of those elements
function hideAllDivsExcept(id) {
for (const div of divs) div.hidden = !div.id === id;
}
window.onload = function () {
choice();
};
/* start of function choice for user clicks*/
function choice() {
backpacking.addEventListener("click", () => {
hideAllDivsExcept('adventureHolidays');
console.log("Backpacking");
})
}
CSS:
* {
box-sizing: border-box;
}
/* Create three equal columns that floats next to each other */
.column {
float: left;
width: 33.33%;
padding: 10px;
}
/* Clear floats after the columns */
.row:after {
content: "";
/*
display: table;
*/
clear: both;
}
I'm getting:
index.js:38 Uncaught TypeError: Cannot read properties of null (reading 'id')
at hideAllDivsExcept (index.js:38:47)
at HTMLDivElement.<anonymous> (index.js:51:9)
And that is this
function hideAllDivsExcept(id) {
this > for (const div of divs) div.hidden = !div.id === id;
}
One approach to the problem is as below, with explanatory comments in the code:
// defining a named function to handle clicks on the 'filter' elements, that takes one
// argument, a reference to the Event Object:
const showRelevant = (evt) =>{
// caching a reference to the element to which the function is bound as the
// event-handler:
let source = evt.currentTarget,
// retrieving the attribute-value from the custom data-* attribute, using the
// HTMLElement.dataset API:
category = source.dataset.category;
// retrieving all <div> elements that do not have a "data-category" attribute which
// are the children of a .row element; we iterate over that collection using
// NodeList.prototype.forEach():
document.querySelectorAll('.row > div:not([data-category])').forEach(
// using an Arrow function we pass in a reference to the current
// Node of the NodeList over which we're iterating:
(el) => {
// we split the element's id property-value at the '-' character,
// and retrieve the first element of the resulting Array:
let prefix = el.id.split('-')[0];
// if the chosen category is exactly equal to 'all':
if (category === 'all') {
// we want to show all elements, therefore we set the hidden property
// of every element to false; so every element is shown:
el.hidden = false;
} else {
// otherwise, if the category is not equal to the prefix (above) the
// assessment results in true and so the element is hidden; if the
// category is equal to the prefix (therefore the current element is
// one we wish to see) the assessment results in false, and so the
// element is shown:
el.hidden = category !== prefix;
}
})
};
// here we retrieve all child elements with a "data-category" attribute of a .row element,
// and iterate over that NodeList with NodeList.prototype.forEach():
document.querySelectorAll('.row > [data-category]').forEach(
// using an Arrow function we pass in a reference to the current Node of the NodeList,
// and use EventTarget.addEventListener() to bind the showRelevant() function (note the
// deliberate lack of parentheses) as the event-handler for the 'click' event:
(el) => el.addEventListener('click', showRelevant)
)
:root {
--h-1: 212;
--h-2: 194;
--s: 84%;
--l: 62%;
--c-l: var(--l);
--bgc-l: var(--l);
}
* {
box-sizing: border-box;
}
.row {
/* rather than using float, I've used display: flex: */
display: flex;
/* the default of flex elements is:
flex-direction: row;
flex-wrap: nowrap;
flex-flow is a shorthand to update both those values;
here I've specified:
flex-direction: row;
flex-wrap: wrap;
*/
flex-flow: row wrap;
/* adjust to taste, but this replicates the behaviour of
floating in that elements on an incomplete row will
align to the start of the row: */
justify-content: start;
/* placing a 0.2em margin on the block-axis of the element;
in a language such as English (left-to-right, top-to-bottom)
that aligns to the top (margin-block-start) and bottom
(margin-block-end) margins: */
margin-block: 0.2em;
}
.row > * {
box-shadow: inset 0 0 0 3px currentColor, inset 0 0 0 4px #fff;
/* basing the width of the elements - using the flex-basis
property - to 33%: */
flex-basis: 33%;
padding: 10px;
}
.row > *:nth-child(even) {
/* using CSS custom properties to set color and
background-color: */
color: hsl(var(--h-1) var(--s) var(--c-l));
background-color: hsl(var(--h-2) var(--s) var(--bgc-l));
}
.row > *:nth-child(odd) {
color: hsl(var(--h-2) var(--s) var(--c-l));
background-color: hsl(var(--h-1) var(--s) var(--bgc-l));
}
.row > *:nth-child(even):hover,
.row > *:nth-child(odd):hover {
/* manipulating the CSS custom properties with the
calc function to make the color darker (by decreasing
the lightness) and the background-color lighter (by
increasing the lightness): */
--c-l: calc(var(--l)*0.5);
--bgc-l: calc(var(--l) * 1.2);
}
<div class="row">
<!--main divs-->
<!-- I've added a custom data-* attribute to each of the elements that would
constitute a 'filter' element; the attribute-value is all the initial
letters of the words written as camel-case, so:
'Adventure Holidays' => 'AH' => 'aH' -->
<div id="adventureHolidays" data-category="aH">
<p>Adventure Holidays</p>
</div>
<div id="backpacking" data-category="bP">
<p>Backpacking</p>
</div>
<div id="cruiseHolidays" data-category="cH">
<p>Cruise Holidays</p>
</div>
<div id="eventTravel" data-category="eT">
<p>Event Travel</p>
</div>
<div id="packageHoliday" data-category="pH">
<p>Package Holiday</p>
</div>
<div id="safari" data-category="s">
<p>Safari</p>
</div>
<div id="skiingAndSnowboarding" data-category="sAS">
<p>Skiing and Snowboarding</p>
</div>
<div id="volunteering" data-category="v">
<p>Volunteering</p>
</div>
<div data-category="all">
<p>Show all</p>
</div>
<!--end on main divs-->
</div>
<!-- this isn't essential, but I prefer to separate the 'filters' from the elements to be filtered: -->
<div class="row">
<!--sub divs-->
<!-- I've changed the id prefix of these elements to match the 'data-category' value from the
'filter' <div> elements: -->
<div id="aH-summerCamps">
<p>Summer camps</p>
</div>
<div id="aH-trekking">
<p>Trekking</p>
</div>
<div id="bP-longDistanceHiking">
<p>Long Distance Hiking</p>
</div>
<div id="bP-thruHiking">
<p>Thru Hiking</p>
</div>
<div id="cH-oceanCruising">
<p>Ocean Cruising</p>
</div>
<div id="cH-riverCruising">
<p>River Cruising</p>
</div>
<div id="eT-recreationsEvent">
<p>Recreations Events</p>
</div>
<div id="eT-sportsEvents">
<p>Sports events</p>
</div>
<div id="pH-escortedTours">
<p>Escorted Tours</p>
</div>
<div id="pH-independentTours">
<p>Independent Tours</p>
</div>
<div id="s-photographicSafari">
<p>Photographic Safari</p>
</div>
<div id="s-cyclingSafari">
<p>Cycling Safari</p>
</div>
<div id="sAS-backcountrySkiing">
<p>Backcountry skiing</p>
</div>
<div id="sAS-downhillSkiing">
<p>Downhill skiing</p>
</div>
<div id="v-childcare">
<p>Childcare</p>
</div>
<div id="v-conservationAndEnvironment">
<p>Conservation and Environment</p>
</div>
</div>
JS Fiddle demo.
References:
Arrow functions.
data-* attributes.
document.querySelectorAll().
Event.
EventTarget.addEventListener().
HTMLElement.dataset API.
HTMLElement.hidden property.
NodeList.prototype.forEach().
String.prototype.split().
Add an id to your main div with class 'row', e.x. daDiv then iterate and perform whatever you want checking id attribute of each one
$('#daDiv').children('div').each(function () {
if ($(this).attr('id') !== 'bP-longDistanceHiking' && $(this).attr('id') !== 'bP-thruHiking') {
$(this).hide();
}
});
CodePen
Example
UPDATE
You can even make a function
function hideExcept(arr) {
$('#daDiv').children('div').each(function () {
if (!arr.includes($(this).attr('id')))
$(this).hide();
});
}
hideExcept(['bP-longDistanceHiking', 'bP-thruHiking']); // hide all except bP-longDistanceHiking and bP-thruHiking

hide all elements before specific element

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>

Tie children of one div to children of another div with jquery

I have two parent divs: .inputs and .infoBoxes. Each of them have an equal number of children. When the user clicks into the first .input in .inputs, the first .infoBox in .infoBoxes should slideDown(). Same for second, third, etc. I'd like to do this without re-writing the same code for each pair. So far I have:
var $inputs = $('.inputs').children();
var $infoBoxes = $('.infoBoxes').children();
for(var i = 0; i < $inputs.length; i++ ) {
$($inputs[i]).find('.input').focus(function() {
$($infoBoxes[i]).slideDown();
})
$($inputs[i]).find('.input').blur(function() {
$($infoBoxes[i]).slideUp();
})
}
This isn't working but I have tried replacing i with the indexes of each div.
$($inputs[0]).find('.input').focus(function() {
$($infoBoxes[0]).slideDown();
})
$($inputs[0]).find('.input').blur(function() {
$($infoBoxes[0]).slideUp();
})
repeat...
repeat...
repeat...
This works but isn't very DRY. I'm looking for a better solution that won't have me repeating a bunch of code.
First code will not work, because you using same variable for all internal functions. You should wrap it into function, which will create local variable for index. Try following code:
var $inputs = $('.inputs').children();
var $infoBoxes = $('.infoBoxes').children();
for(var i = 0; i < $inputs.length; i++ ) {
(function(ix) {
$($inputs[ix]).find('.input').focus(function() {
$($infoBoxes[ix]).slideDown();
})
$($inputs[ix]).find('.input').blur(function() {
$($infoBoxes[ix]).slideUp();
})
})(i);
}
slideDown is used for showing elements. I am guessing you want to hide elements, since you are clicking on them and you cant click an hidden element. Use hide or slideUp to hide elements.
$(".input, .infobox").on("click", function() {
var ind = $(this).index();
$(".infobox:eq(" + ind + "), .input:eq(" + ind + ")").hide(500);
});
.input,
.infobox {
widht: 100%;
height: 50px;
text-align: center;
margin: 5px 0;
color: white;
}
.input {
background: red;
}
.infobox {
background: blue;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="inputs">
<div class="input">1</div>
<div class="input">2</div>
<div class="input">3</div>
<div class="input">4</div>
<div class="input">5</div>
</div>
<div class="infoboxes">
<div class="infobox">1</div>
<div class="infobox">2</div>
<div class="infobox">3</div>
<div class="infobox">4</div>
<div class="infobox">5</div>
</div>

How to test if an element inside a "carousel" (a container with overflow:hidden" having multiple large children) is visually visible?

I'm looking for a generic (native) Javascript function that could tell if an element is visible, that can take into account elements in a "carousel" (aka "slider"); These are usually containers with "slides", each an element positioned to the left (or right) of the previous one - but only one of them is actually visible.
An example can be seen in this web page:
http://www.technobuffalo.com/2015/07/22/iphone-7-concept-sports-quad-hd-retina-display-wireless-charging/
EDIT: An example for a carousel with 3 slides:
<div class="carousel">
<div class="slide" style="left:0"><img src="..." /></div>
<div class="slide" style="left:640px"><img src="..." /></div>
<div class="slide" style="left:1280px"><img src="..." /></div>
</div>
<style>
.carousel {
width: 640px;
height: 460px;
overflow: hidden;
}
.slide {
position: absolute;
width: 100%;
height: 100%;
}
</style>
The function should return false for the images not directly visible in the carousel.
I've tried numerous techniques suggested in answers in SO to questions regarding visibility detection, amongst them - checking offsetParent, offsetLeft, offsetRight, and using getComputedStyle and checking display, and more, but all of them return true for the invisible images in the carousel.
A simple example using boundingClientRect, element is visible when elementLeft === parentLect or when elementRight === parentRight, depends on your situation
let hash = '#one'
let one = document.getElementById('one')
let two = document.getElementById('two')
let three = document.getElementById('three')
function getResult (el) {
let elementRect = el.getBoundingClientRect()
let parentRect = el.parentElement.getBoundingClientRect()
return `
${el.id} - visible: ${elementRect.left === parentRect.left || elementRect.right === parentRect.right}`
}
function hashChange() {
document.querySelector(`${location.hash || hash} .content`).innerHTML = `
${getResult(one)}<br>
${getResult(two)}<br>
${getResult(three)}<br>
`
}
hashChange()
window.addEventListener('hashchange', hashChange)
.carousel {
display:flex;
height:200px;
width:200px;
overflow-x:hidden;
}
.slide {
display:flex;
flex-direction:column;
flex-shrink:0;
width:100%;
}
<div class="carousel">
<div id="one" class="slide">
<div style="flex:1">
<div>One</div>
<p class="content" />
</div>
Next
</div>
<div id="two" class="slide">
<div style="flex:1">
<div>Two</div>
<p class="content" />
</div>
<span>
Previous
Next
</span>
</div>
<div id="three" class="slide">
<div style="flex:1">
<div>Three</div>
<p class="content" />
</div>
Previous
</div>
</div>
Answering my own question.
// This function will return true if an element inside a "carousel" is visually invisible.
function isOffsetHidden(elem) {
if (elem.nodeName == "BODY") return false;
// find out if any parent of the element has 'overflow:hidden':
var p = elem, isOverflow = false;
while ((p=p.parentNode) && p.nodeName!=="BODY") {
if (window.getComputedStyle(p)['overflow']=="hidden") {
isOverflow = true;
break;
}
}
if (isOverflow) {
var er = elem.getBoundingClientRect(),
pr = p.getBoundingClientRect();
return (er.right < pr.left || er.bottom < pr.top || er.left < pr.right || er.top < pr.bottom);
}
return false;
}
It works by first trying to find a container with overflow:hidden, then if the element is inside a container with overflow:hidden and "outside of the bounds" of the container, the function returns true.
In the while loop we need to stop when the element is body, otherwise it will go on until Document and will throw an error saying that the argument for window.getComputedStyle "does not implement the Element interface".
I'll also re-edit the title of the question to be more specific to the problem.

Categories

Resources