Javascript DOM ChildNodes property doesn't capture all children - javascript

I want to save a DOM subtree (everything under the div called "block diagram") then paint it on a div called "bus_diagram". Saving the childNodes property doesn't seem to capture all of the elements for some reason.
here's the javascript I'm using. On calling the function "dostuff()" everything under "block_diagram" should go to "bus_diagram"
var SAVED_BLOCK_DOM = null;
function save_block() {
SAVED_BLOCK_DOM = document.getElementById("block_diagram").childNodes;
}
function refresh_block() {
for (var i = 0; i < SAVED_BLOCK_DOM.length; i++) {
document.getElementById("bus_diagram").appendChild(SAVED_BLOCK_DOM[i]);
}
}
function dostuff() {
save_block();
refresh_block();
}
Here's a JSFiddle: http://jsfiddle.net/BFp5s/3/

.childNodes is a live collection of nodes and as you start doing .appendChild() on the nodes (which moves the elements to a different place in the DOM), the live collection changes while you are iterating it, causing you to miss nodes. So, when the index of your for loop is 0, you do .appendChild() on the 0th element of the list. That removes that element from the live list. You then increment your index to 1, but the next item to process is now in the 0th spot in the list causing you to process every other item.
You can either make a copy of the live list into an array (so it won't change while iterating it) or change the way you iterate the list.
For example, you can change save_block() to this:
function save_block() {
SAVED_BLOCK_DOM = Array.prototype.slice.call(document.getElementById("block_diagram").childNodes, 0);
}
This makes SAVED_BLOCK_DOM into a normal array so it won't change while you iterate it.
jsFiddle Demo: http://jsfiddle.net/jfriend00/R8c94/
Or, if you want/need to support IE6/7/8 support which won't work with the above copy mechanism, you can just copy the nodeList manually:
function save_block() {
SAVED_BLOCK_DOM = [];
var list = document.getElementById("block_diagram").childNodes;
for (var i = 0; i < list.length; i++) {
SAVED_BLOCK_DOM.push(list[i]);
}
}
If you don't need SAVED_BLOCK_DOM to continue to hold the list of nodes and want to support IE8, you can change the way you iterate like this:
function refresh_block() {
while (SAVED_BLOCK_DOM.length) {
document.getElementById("bus_diagram").appendChild(SAVED_BLOCK_DOM[0]);
}
}
jsFiddle demo: http://jsfiddle.net/jfriend00/PK7Tg/

Related

Javascript childNodes

I am trying to make a childNode be invisible so that the user will not be able to see it.
function hideLetters() {
var squares = document.querySelectorAll( "#squarearea div" );
for ( var i = 0; i < squares.length; i++ ) {
squares[ i ] = hide( squares[ i ] );
}
}
function hide( squares ) {
var nodeList = squares.childNodes;
nodeList.style.display = "none";
squares.childNodes = nodeList;
return squares;
}
I have been trying to make the child nodes found within squares invisible so that they do not appear on the screen. Please note that I am only using JavaScript, HTML, and CSS for this project.
You need to apply it to every element of the node list:
squares.childNodes.forEach(node => node.style.display = "none");
Try this one
Array.prototype.slice.call(squares.childNodes).forEach(node => node.style.display = 'none')
There were a few things incorrect about your code and I took the liberty of taking out bits that didn't merit staying in given what you were trying to do.
In no way are you manipulating squares other than looping over it. In your code you said squares[i] = hide(squares[i] - not to put to fine a point on it, but this is worthless and does nothing. The list itself is a reference to the nodes, not the nodes themselves. You can think of every item in the list like a sign-post that tells the code where to look. So when that node is changed it doesn't need to be updated in the list because the list is simply saying "this is what you want to look at", it doesn't actually contain a copy of the node.
because of the reasons listed above you don't need to return anything from your hide function.
The nodeList in your hide function needs to be iterated over and each node manipulated individually. It's worth noting that you can't say "adjust all of these" at any point in JavaScript unless you yourself create a function that allows that functionality, but under the hood you're still going through every list or array one by one.
Your nodeList is aptly named. It is a list of nodes. Most people, at least the newer people to JavaScript(no shame for that, we all learn sometime), assume that tags(a.e. <div></div>, <a></a>, <span></span>) are nodes. And yes, you're right, they are! But the text within those tags are completely separate and individual nodes as well. This means that when you iterate over all the nodes you probably aren't just getting Element Nodes you might be getting Text Nodes or Document Fragment Nodes or Entity Nodes, etc.
While we iterate over the nodeList we need to separate out the nodes that we can hide(those with a style object that's able to be manipulated) and we do this by comparing the built-in nodeType property that's in every node with the Node.ELEMENT_NODE property. If it returns true we know absolutely that the node is an Element Node.
After we've checked that what we're manipulating is an element, we simply set it's display property (which is normally "block") to the value "none" and in that way hiding it on the DOM.
The code below, I think, is what you're looking for.
function hideLetters() {
let squares = document.querySelectorAll("#squarearea div");
for(let i = 0; i < squares.length; i++) {
hide( squares[i] );
});
}
function hide(squares) {
var nodeList = squares.childNodes;
for(let i = 0; i < nodeList.length; i++) {
if (nodeList[i].nodeType === Node.ELEMENT_NODE) {
nodeList[i].style.display = "none";
}
});
}
It's worth noting that you could simply use .children instead of .childNodes to return only the elements of a parent node. I don't know if you had a reason for wanting all nodes to be searched through, but this would condense the iteration down to simply setting the style property:
var nodeList = squares.children;
nodeList.forEach(node => node.style.display = "none");
function hideLetters() {
let squares = document.querySelectorAll("#squarearea div");
for (let i = 0; i < squares.length; i++) {
hide(squares[i]);
};
}
function hide(squares) {
var nodeList = squares.childNodes;
for (let i = 0; i < nodeList.length; i++) {
if (nodeList[i].nodeType === Node.ELEMENT_NODE) {
nodeList[i].style.display = "none";
}
};
}
hideLetters();
#squarearea div {
border: solid 1px black;
width: 10px;
padding: 3px;
margin: 10px;
}
<div id="squarearea">
<div><span>a</span></div>
<div><span>b</span></div>
<div><span>c</span></div>
<div><span>d</span></div>
<div><span>e</span></div>
<div><span>f</span></div>
<div><span>g</span></div>
</div>

JavaScript: How to skip over current item in array during a for loop? (continue?)

EDIT: I don't want to skip index 1. I want to skip the current (clicked on) element. Also, see below for more of the code as requested. You'll see that I have a class CatListItem and five instances of that class in an array allCatListItems.
Here's some context for the question: I have a list of cats. When I click on a cat's name (list item), I want that cat's picture and other info to be appended to the page (got that down). When a cat is clicked, I also want any other cat that is being displayed to be hidden (that way there is only one cat on the screen at a time).
I'm trying to accomplish this with a for loop, but obviously if it iterates over every item in the array, then when I click an item, the cat being clicked will be hidden as well.
I want to skip the current item in the array and only run the code on the other items. Using continue, I know I can skip a specific item (item 1 in the below example). This will run my code on every item in the array except that at index one. But how can I make that continue dynamic? Meaning... how can I hide all of the cats, except the one being currently clicked?
Here's the loop that skips index 1:
CatListItem.prototype.hideCats = function() {
allCatListItems.forEach(function(cat) {
cat.a.addEventListener('click', function() {
for (var i = 0; i < allCatListItems.length; i++) {
if (i === 1) {
continue;
}
allCatListItems[i].img.className = 'hide';
};
});
});
}
var allCatListItems = [
catListItem1 = new CatListItem('El', 'images/el.jpg', 'el'),
catListItem2 = new CatListItem('Widdle Baby', 'images/widdle-baby.jpg', 'widdle-baby'),
catListItem3 = new CatListItem('Mama', 'images/mama.jpg', 'mama'),
catListItem4 = new CatListItem('Legion', 'images/legion.jpg', 'legion'),
catListItem5 = new CatListItem('Boy', 'images/boy.jpg', 'boy'),
];
EDIT: Here's a fiddle.JSFIDDLE Click the names to see the functionality without the hideCats function. Then uncomment where it says to to see my issue.
I'm starting to think maybe a for loop isn't the best option?
In that case compare the event.target(its the element clicked)
EDIT: allCatListItems[i] needs it's .a property attached to it in the if statement (this is what contains the anchor element). This is because the event listener is grabbing an anchor tag, so e.target will be returning an anchor tag as well. The if statement will never return as true if you aren't comparing the same type of element.
cat.a.addEventListener('click', function(e) {
for (var i = 0; i < allCatListItems.length; i++) {
if (allCatListItems[i].a === e.target) {
continue;
}
allCatListItems[i].img.className += ' hide';
}
});
Here is a jsfiddle, it doesn't use the same element names, but it should be doing what you want. https://jsfiddle.net/5qb4rwzc/
$('li').on('click', function() {
var index = $(this).index();
var items = document.getElementsByTagName('li');
for(var i = 0; i < items.length; i++) {
if(i === index) continue;
items[i].style = "display:none;";
}
});
Its really depend on how you call the function "hideCat". Realizing that each time that function is called, more eventListeners are add to every cat item. Each time a cat is clicked, more than one event fired. Perhaps you should re-structure how to attach eventListeners to each cat item.

For loop only iterates once when trying to remove classes from elements

In Javascript I have a function that should find the elements on the page that have the "connected" class, and when a button is clicked the classes for these elements are cleared. I have written this code:
var prev_connected = document.getElementsByClassName("connected");
if (prev_connected.length > 0) {
for (var j = 0; j < prev_connected.length; j++) {
prev_connected[j].removeAttribute("class");
}
}
However, it only ever deletes the class attribute of the first "connected" element on the page. When I have two "connected" elements, I have confirmed that the "prev_connected" array does hold 2 values, but for some reason the for loop never reaches the 2nd one. Is there something I'm doing wrong? Thanks.
The result of getElementsByClassName is live, meaning that as you remove the class attribute it will also remove that element from the result. Using querySelectorAll is more widely supported and returns a static NodeList.
Also, you can more easily iterate the list using a for...in loop.
I would not recommend making an extra copy of the live list just to make it static, you should use a method that returns a static NodeList instead.
var prev_connected = document.querySelectorAll(".connected");
document.getElementById('demo').onclick = function() {
for(var i in Object.keys(prev_connected)) {
prev_connected[i].removeAttribute("class");
}
}
.connected {
background: rgb(150,200,250);
}
<div class="connected">Hello</div>
<div class="connected">Hello</div>
<div class="connected">Hello</div>
<div class="connected">Hello</div>
<div class="connected">Hello</div>
<button id="demo">Remove the classes!</button>
This is due to prev_connected being a live nodelist. When you update the element with that class it removes it from the nodelist which means the length of the nodelist reduces by one which means j is trying to find element 2 in an nodelist of length 1 which is why it doesn't work after the first iteration.
You can see this happening in the console in this demo.
One way you can fix this is by converting the nodelist to an array:
var prev_connected = [].slice.call(document.getElementsByClassName("connected"));
You should iterate in the opposite direction and use elem[i].classList.remove('name') for removing class name from each element Demo
document.getElementById("button").onclick = function () {
var prev_connected = document.getElementsByClassName("connected");
console.log(prev_connected);
for (var i = prev_connected.length - 1; i >= 0; i--) {
prev_connected[i].classList.remove("connected");
console.log(i, prev_connected[i - 1]);
}
}
There are another answers: https://stackoverflow.com/a/14409442/4365315

how can I know what index is an html element in its parent children [duplicate]

Normally I'm doing it this way:
for(i=0;i<elem.parentNode.length;i++) {
if (elem.parentNode[i] == elem) //.... etc.. etc...
}
function getChildIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.childNodes, node);
}
This seems to work in Opera 11, Firefox 4, Chromium 10. Other browsers untested. It will throw TypeError if node has no parent (add a check for node.parentNode !== undefined if you care about that case).
Of course, Array.prototype.indexOf does still loop, just within the function call. It's impossible to do this without looping.
Note: If you want to obtain the index of a child Element, you can modify the function above by changing childNodes to children.
function getChildElementIndex(node) {
return Array.prototype.indexOf.call(node.parentNode.children, node);
}
Option #1
You can use the Array.from() method to convert an HTMLCollection of elements to an array. From there, you can use the native .indexOf() method in order to get the index:
function getElementIndex (element) {
return Array.from(element.parentNode.children).indexOf(element);
}
If you want the node index (as oppose to the element's index), then replace the children property with the childNodes property:
function getNodeIndex (element) {
return Array.from(element.parentNode.childNodes).indexOf(element);
}
Option #2
You can use the .call() method to invoke the array type's native .indexOf() method. This is how the .index() method is implemented in jQuery if you look at the source code.
function getElementIndex(element) {
return [].indexOf.call(element.parentNode.children, element);
}
Likewise, using the childNodes property in place of the children property:
function getNodeIndex (element) {
return [].indexOf.call(element.parentNode.childNodes, element);
}
Option #3
You can also use the spread operator:
function getElementIndex (element) {
return [...element.parentNode.children].indexOf(element);
}
function getNodeIndex (element) {
return [...element.parentNode.childNodes].indexOf(element);
}
You could count siblings...
The childNodes list includes text and element nodes-
function whichChild(elem){
var i= 0;
while((elem=elem.previousSibling)!=null) ++i;
return i;
}
There is no way to get the index of a node within its parent without looping in some manner, be that a for-loop, an Array method like indexOf or forEach, or something else. An index-of operation in the DOM is linear-time, not constant-time.
More generally, if list mutations are possible (and the DOM certainly supports mutation), it's generally impossible to provide an index-of operation that runs in constant time. There are two common implementation tactics: linked lists (usually doubly) and arrays. Finding an index using a linked list requires a walk. Finding an index using an array requires a scan. Some engines will cache indexes to reduce time needed to compute node.childNodes[i], but this won't help you if you're searching for a node. Not asking the question is the best policy.
I think you've got it, but:
make sure that variable "i" is declared with var
use === instead of == in the comparison
If you have a collection input elements with the same name (like <textarea name="text_field[]"…) in your form and you want to get the exact numeric index of the field that triggered an event:
function getElementIdxFromName(elem, parent) {
var elms = parent[elem.name];
var i = 0;
if (elms.length === undefined) // there is only one element with this name in the document
return 0;
while((elem!=elms[i])) i++;
return i;
}
Getting numeric id of an element from a collection of elements with the same class name:
function getElementIdxFromClass(elem, cl) {
var elems = document.getElementsByClassName(cl);
var i = 0;
if (elems.length > 0) {
while((elem!=elems[i])) i++;
return i;
}
return 0;
}
Try this:
let element = document.getElementById("your-element-id");
let indexInParent = Array.prototype.slice.call(element.parentNode.parentNode.children).indexOf(element.parentNode));

Sorting Divs in jQuery by Custom Sort Order

I'm trying to re-sort the child elements of the tag input by comparing
their category attribute to the category order in the Javascript
variable category_sort_order. Then I need to remove divs whose category attribute
does not appear in category_sort_order.
The expected result should be:
any
product1
product2
download
The code:
<div id="input">
<div category="download">download</div>
<div category="video">video1</div>
<div category="video">video2</div>
<div category="product">product1</div>
<div category="any">any</div>
<div category="product">product2</div>
</div>
<script type="text/javascript">
var category_sort_order = ['any', 'product', 'download'];
</script>
I really don't even know where to begin with this task but if you could please provide any assistance whatsoever I would be extremely grateful.
I wrote a jQuery plugin to do this kind of thing that can be easily adapted for your use case.
The original plugin is here
Here's a revamp for you question
(function($) {
$.fn.reOrder = function(array) {
return this.each(function() {
if (array) {
for(var i=0; i < array.length; i++)
array[i] = $('div[category="' + array[i] + '"]');
$(this).empty();
for(var i=0; i < array.length; i++)
$(this).append(array[i]);
}
});
}
})(jQuery);
and use like so
var category_sort_order = ['any', 'product', 'download'];
$('#input').reOrder(category_sort_order);
This happens to get the right order for the products this time as product1 appears before product2 in the original list, but it could be changed easily to sort categories first before putting into the array and appending to the DOM. Also, if using this for a number of elements, it could be improved by appending all elements in the array in one go instead of iterating over the array and appending one at a time. This would probably be a good case for DocumentFragments.
Just note,
Since there is jQuery 1.3.2 sorting is simple without any plugin like:
$('#input div').sort(CustomSort).appendTo('#input');
function CustomSort( a ,b ){
//your custom sort function returning -1 or 1
//where a , b are $('#input div') elements
}
This will sort all div that are childs of element with id="input" .
Here is how to do it. I used this SO question as a reference.
I tested this code and it works properly for your example:
$(document).ready(function() {
var categories = new Array();
var content = new Array();
//Get Divs
$('#input > [category]').each(function(i) {
//Add to local array
categories[i] = $(this).attr('category');
content[i] = $(this).html();
});
$('#input').empty();
//Sort Divs
var category_sort_order = ['any', 'product', 'download'];
for(i = 0; i < category_sort_order.length; i++) {
//Grab all divs in this category and add them back to the form
for(j = 0; j < categories.length; j++) {
if(categories[j] == category_sort_order[i]) {
$('#input').append('<div category="' +
category_sort_order[i] + '">'
+ content[j] + '</div>');
}
};
}
});
How it works
First of all, this code requires the JQuery library. If you're not currently using it, I highly recommend it.
The code starts by getting all the child divs of the input div that contain a category attribute. Then it saves their html content and their category to two separate arrays (but in the same location.
Next it clears out all the divs under the input div.
Finally, it goes through your categories in the order you specify in the array and appends the matching child divs in the correct order.
The For loop section
#eyelidlessness does a good job of explaining for loops, but I'll also take a whack at it. in the context of this code.
The first line:
for(i = 0; i < category_sort_order.length; i++) {
Means that the code which follows (everything within the curly brackets { code }) will be repeated a number of times. Though the format looks archaic (and sorta is) it says:
Create a number variable called i and set it equal to zero
If that variable is less than the number of items in the category_sort_order array, then do whats in the brackets
When the brackets finish, add one to the variable i (i++ means add one)
Then it repeats step two and three until i is finally bigger than the number of categories in that array.
A.K.A whatever is in the brackets will be run once for every category.
Moving on... for each category, another loop is called. This one:
for(j = 0; j < categories.length; j++) {
loops through all of the categories of the divs that we just deleted from the screen.
Within this loop, the if statement checks if any of the divs from the screen match the current category. If so, they are appending, if not the loop continues searching till it goes through every div.
Appending (or prepending) the DOM nodes again will actually sort them in the order you want.
Using jQuery, you just have to select them in the order you want and append (or prepend) them to their container again.
$(['any', 'product', 'video'])
.map(function(index, category)
{
return $('[category='+category+']');
})
.prependTo('#input');
Sorry, missed that you wanted to remove nodes not in your category list. Here is the corrected version:
// Create a jQuery from our array of category names,
// it won't be usable in the DOM but still some
// jQuery methods can be used
var divs = $(['any', 'product', 'video'])
// Replace each category name in our array by the
// actual DOM nodes selected using the attribute selector
// syntax of jQuery.
.map(function(index, category)
{
// Here we need to do .get() to return an array of DOM nodes
return $('[category='+category+']').get();
});
// Remove everything in #input and replace them by our DOM nodes.
$('#input').empty().append(divs);
// The trick here is that DOM nodes are selected
// in the order we want them in the end.
// So when we append them again to the document,
// they will be appended in the order we want.
I thought this was a really interesting problem, here is an easy, but not incredibly performant sorting solution that I came up with.
You can view the test page on jsbin here: http://jsbin.com/ocuta
function compare(x, y, context){
if($.inArray(x, context) > $.inArray(y, context)) return 1;
}
function dom_sort(selector, order_list) {
$items = $(selector);
var dirty = false;
for(var i = 0; i < ($items.length - 1); i++) {
if (compare($items.eq(i).attr('category'), $items.eq(i+1).attr('category'), order_list)) {
dirty = true;
$items.eq(i).before($items.eq(i+1).remove());
}
}
if (dirty) setTimeout(function(){ dom_sort(selector, order_list); }, 0);
};
dom_sort('#input div[category]', category_sort_order);
Note that the setTimeout might not be necessary, but it just feels safer. Your call.
You could probably clean up some performance by storing a reference to the parent and just getting children each time, instead of re-running the selector. I was going for simplicity though. You have to call the selector each time, because the order changes in a sort, and I'm not storing a reference to the parent anywhere.
It's seems fairly direct to use the sort method for this one:
var category_sort_order = ['any', 'product', 'download'];
// select your categories
$('#input > div')
// filter the selection down to wanted items
.filter(function(){
// get the categories index in the sort order list ("weight")
var w = $.inArray( $(this).attr('category'), category_sort_order );
// in the sort order list?
if ( w > -1 ) {
// this item should be sorted, we'll store it's sorting index, and keep it
$( this ).data( 'sortindex', w );
return true;
}
else {
// remove the item from the DOM and the selection
$( this ).remove();
return false;
}
})
// sort the remainder of the items
.sort(function(a, b){
// use the previously defined values to compare who goes first
return $( a ).data( 'sortindex' ) -
$( b ).data( 'sortindex' );
})
// reappend the selection into it's parent node to "apply" it
.appendTo( '#input' );
If you happen to be using an old version of jQuery (1.2) that doesn't have the sort method, you can add it with this:
jQuery.fn.sort = Array.prototype.sort;

Categories

Resources