What is the easiest way to swap the order of child nodes?
For example I want childNode[3] to be childNode[4] and vice-versa.
There is no need for cloning. You can just move one node before the other. The .insertBefore() method will take it from its current location and insert it somewhere else (thus moving it):
childNode[4].parentNode.insertBefore(childNode[4], childNode[3]);
You get the parent of the node. You then call the insertBefore method on the parent and you pass it the childNode[4] node and tell it you want it inserted before childNode[3]. That will give you the result of swapping their order so 4 will be before 3 when it's done.
Reference documentation on insertBefore.
Any node that is inserted into the DOM that is already in the DOM is first removed automatically and then inserted back so there is no need to manually remove it first.
Use .before or .after!
This is vanilla JS!
childNode[3].before(childNode[4]);
or
childNode[4].after(childNode[3]);
For more durability swapping, try:
function swap(node1, node2) {
const afterNode2 = node2.nextElementSibling;
const parent = node2.parentNode;
node1.replaceWith(node2);
parent.insertBefore(node1, afterNode2);
}
This should work, even if the parents don't match
Can I Use - 95% Jul '21
Answer by jfriend00 does not really swap elements (it "swaps" only elements which are next to each other and only under the same parent node). This is ok, since this was the question.
This example swaps elements by cloning it but regardless of their position and DOM level:
// Note: Cloned copy of element1 will be returned to get a new reference back
function exchangeElements(element1, element2)
{
var clonedElement1 = element1.cloneNode(true);
var clonedElement2 = element2.cloneNode(true);
element2.parentNode.replaceChild(clonedElement1, element2);
element1.parentNode.replaceChild(clonedElement2, element1);
return clonedElement1;
}
Edit: Added return of new reference (if you want to keep the reference, e. g. to access attribute "parentNode" (otherwise it gets lost)). Example: e1 = exchangeElements(e1, e2);
I needed a function to swap two arbitrary nodes keeping the swapped elements in the same place in the dom. For example, if a was in position 2 relative to its parent and b was in position 0 relative to its parent, b should replace position 2 of a's former parent and a should replace child 0 of b's former parent.
This is my solution which allows the swap to be in completely different parts of the dom. Note that the swap cannot be a simple three step swap. Each of the two elements need to be removed from the dom first because they may have siblings that would need updating, etc.
Solution: I put in two holder div's to hold the place of each node to keep relative sibling order. I then reinsert each of the nodes in the other's placeholder, keeping the relative position that the swapped node had before the swap. (My solution is similar to Buck's).
function swapDom(a,b)
{
var aParent = a.parentNode;
var bParent = b.parentNode;
var aHolder = document.createElement("div");
var bHolder = document.createElement("div");
aParent.replaceChild(aHolder,a);
bParent.replaceChild(bHolder,b);
aParent.replaceChild(b,aHolder);
bParent.replaceChild(a,bHolder);
}
For a real Swap of any nodes without cloneNode:
<div id="d1">D1</div>
<div id="d2">D2</div>
<div id="d3">D3</div>
With SwapNode function (using PrototypeJS):
function SwapNode(N1, N2) {
N1 = $(N1);
N2 = $(N2);
if (N1 && N2) {
var P1 = N1.parentNode;
var T1 = document.createElement("span");
P1.insertBefore(T1, N1);
var P2 = N2.parentNode;
var T2 = document.createElement("span");
P2.insertBefore(T2, N2);
P1.insertBefore(N2, T1);
P2.insertBefore(N1, T2);
P1.removeChild(T1);
P2.removeChild(T2);
}
}
SwapNode('d1', 'd2');
SwapNode('d2', 'd3');
Will produce:
<div id="d3">D3</div>
<div id="d1">D1</div>
<div id="d2">D2</div>
Use a dummy sibling as a temporary position marker and then .before (or .after).
It works for any siblings (not only adjacent) and also maintains event handlers.
function swap(a, b) {
let dummy = document.createElement("span")
a.before(dummy)
b.before(a)
dummy.replaceWith(b)
}
<div id="div1">A</div>
<div id="div2">B</div>
<p> parent<div id="div3">C</div>
</p>
<button onclick="swap(div1, div3)">swap</button>
Just like temporary variables are used to swap variables, if more sophicated methods are missing.
Best way to do this is to create a temporary node
function swapNodes(node1, node2) {
const temp = document.createComment('')
node2.replaceWith(temp)
node1.replaceWith(node2)
temp.replaceWith(node1)
}
Try this method:
Get the parent element
Store the two elements you want to swap
Store the .nextSibling of the node that is last in order
eg: [1,2,3,4] => we want to swap 3 & 2 then store nextSibling of 3, '4'.
.insertBefore(3,2);
.insertBefore(2,nextSibling);
Code Explanation
val & val2 are the 2 nodes/elements to be swapped
equiv(index) gets the present node/element in DOM at index passed as the paramter
NOTE: It will count comment & text elements so take care xD
Hopes this helps :)
function equiv(index){
return Array.prototype.slice.call( document.querySelectorAll("*"))[index];
}
function swap (val,val2){
let _key = val.key;
let _key_ = val2.key;
_key_ = _key < _key_ ? _key_+1:_key_;
let _parent_ = val2.parentElement.valueOf();
if (val.parentElement.children.length ==1)
val.parentElement.appendChild(val2);
else
val.parentElement.insertBefore(val2,val);
if (_parent_.children.length ==0)
_parent_.appendChild(val);
else{
let _sibling_ = equiv(_key_);
_parent_.insertBefore(val,_sibling_);}
}
A solution that works without cloning, given the indices of the two elements to swap:
function swapChildren(parentElement, index1, index2) {
if (index1 === index2)
return
if (index1 > index2) {
const temp = index1
index1 = index2
index2 = temp
}
const { [index1]: element1, [index2]: element2 } = parentElement.childNodes
if (index2 === index1 + 1) {
parentElement.insertBefore(element2, element1)
} else {
const reference = element2.nextSibling
parentElement.replaceChild(element2, element1)
parentElement.insertBefore(element1, reference)
}
}
You can swap a DOM element with its next sibling like that:
el.parentNode.insertBefore(el, el.nextElementSibling)
Or with its previous sibling like this:
el.parentNode.insertBefore(el, el.previousElementSibling)
And if your content is dynamic, you might want to check that el.nextElementSibling or el.previousElementSibling is not null.
Related
const divArr = [];
for (let i = 0; i < 5; i++) {
document.getElementById("h").innerHTML += `<div id=${i}>hello</div>`;
divArr.push(document.getElementById(`${i}`));
}
console.log(divArr);
let divArrIndex = 0;
setInterval(() => {
document.getElementById(`${divArrIndex}`).style.backgroundColor = 'green';
if (divArrIndex > 4) {
divArrIndex = 0;
}
divArrIndex += 1;
}, 1000 / 1);
<div id="h">alsdjf;lkajsdf</div>
The code above successfully turns all the divs green.
But, when I use
divArr[divArrIndex].style.backgroundColor = "green"
instead of
document.getElementById(`${divArrIndex}`).style.backgroundColor='green';
I only get the last div green.
Why?
codepen: https://codepen.io/sai-nallani/pen/KKopOXZ?editors=1111
By reassignment to innerHTML, you are destroying and recreating the contents of #h in each iteration of the loop. You create #0, then discard it and create #0 and #1, then discard those and create #0, #1, #2... So the elements you push into the array don't exist any more in your document (though references to them in divArr still keep the garbage collector from destroying them outright).
When you change the colour of divArr[0], you are changing the colour of an element that only exists in memory, but is not in DOM any more. However, #4 is still the original #4, it has not been discarded, since you have performed no further assignments to innerHTML.
One solution is to gather all the divs after you have constructed them all. You can use another loop, but the easiest way would be:
const divArr = Array.from(document.querySelectorAll('#h > div'));
(Depending on what you are doing with it next, you may not need Array.from since NodeList should suffice for many purposes.)
Another solution is to construct elements in their own right, not by changing the parent's innerHTML:
const hEl = document.querySelector('#h');
for (let i = 0; i < 5; i++) {
const divEl = document.createElement('div');
divEl.textContent = 'Hello';
divEl.id = i;
hEl.appendChild(divEl);
divArr.push(divEl);
}
This way, every element is created and added to #h without affecting any other elements already there.
What is the easiest way to swap the order of child nodes?
For example I want childNode[3] to be childNode[4] and vice-versa.
There is no need for cloning. You can just move one node before the other. The .insertBefore() method will take it from its current location and insert it somewhere else (thus moving it):
childNode[4].parentNode.insertBefore(childNode[4], childNode[3]);
You get the parent of the node. You then call the insertBefore method on the parent and you pass it the childNode[4] node and tell it you want it inserted before childNode[3]. That will give you the result of swapping their order so 4 will be before 3 when it's done.
Reference documentation on insertBefore.
Any node that is inserted into the DOM that is already in the DOM is first removed automatically and then inserted back so there is no need to manually remove it first.
Use .before or .after!
This is vanilla JS!
childNode[3].before(childNode[4]);
or
childNode[4].after(childNode[3]);
For more durability swapping, try:
function swap(node1, node2) {
const afterNode2 = node2.nextElementSibling;
const parent = node2.parentNode;
node1.replaceWith(node2);
parent.insertBefore(node1, afterNode2);
}
This should work, even if the parents don't match
Can I Use - 95% Jul '21
Answer by jfriend00 does not really swap elements (it "swaps" only elements which are next to each other and only under the same parent node). This is ok, since this was the question.
This example swaps elements by cloning it but regardless of their position and DOM level:
// Note: Cloned copy of element1 will be returned to get a new reference back
function exchangeElements(element1, element2)
{
var clonedElement1 = element1.cloneNode(true);
var clonedElement2 = element2.cloneNode(true);
element2.parentNode.replaceChild(clonedElement1, element2);
element1.parentNode.replaceChild(clonedElement2, element1);
return clonedElement1;
}
Edit: Added return of new reference (if you want to keep the reference, e. g. to access attribute "parentNode" (otherwise it gets lost)). Example: e1 = exchangeElements(e1, e2);
I needed a function to swap two arbitrary nodes keeping the swapped elements in the same place in the dom. For example, if a was in position 2 relative to its parent and b was in position 0 relative to its parent, b should replace position 2 of a's former parent and a should replace child 0 of b's former parent.
This is my solution which allows the swap to be in completely different parts of the dom. Note that the swap cannot be a simple three step swap. Each of the two elements need to be removed from the dom first because they may have siblings that would need updating, etc.
Solution: I put in two holder div's to hold the place of each node to keep relative sibling order. I then reinsert each of the nodes in the other's placeholder, keeping the relative position that the swapped node had before the swap. (My solution is similar to Buck's).
function swapDom(a,b)
{
var aParent = a.parentNode;
var bParent = b.parentNode;
var aHolder = document.createElement("div");
var bHolder = document.createElement("div");
aParent.replaceChild(aHolder,a);
bParent.replaceChild(bHolder,b);
aParent.replaceChild(b,aHolder);
bParent.replaceChild(a,bHolder);
}
For a real Swap of any nodes without cloneNode:
<div id="d1">D1</div>
<div id="d2">D2</div>
<div id="d3">D3</div>
With SwapNode function (using PrototypeJS):
function SwapNode(N1, N2) {
N1 = $(N1);
N2 = $(N2);
if (N1 && N2) {
var P1 = N1.parentNode;
var T1 = document.createElement("span");
P1.insertBefore(T1, N1);
var P2 = N2.parentNode;
var T2 = document.createElement("span");
P2.insertBefore(T2, N2);
P1.insertBefore(N2, T1);
P2.insertBefore(N1, T2);
P1.removeChild(T1);
P2.removeChild(T2);
}
}
SwapNode('d1', 'd2');
SwapNode('d2', 'd3');
Will produce:
<div id="d3">D3</div>
<div id="d1">D1</div>
<div id="d2">D2</div>
Use a dummy sibling as a temporary position marker and then .before (or .after).
It works for any siblings (not only adjacent) and also maintains event handlers.
function swap(a, b) {
let dummy = document.createElement("span")
a.before(dummy)
b.before(a)
dummy.replaceWith(b)
}
<div id="div1">A</div>
<div id="div2">B</div>
<p> parent<div id="div3">C</div>
</p>
<button onclick="swap(div1, div3)">swap</button>
Just like temporary variables are used to swap variables, if more sophicated methods are missing.
Best way to do this is to create a temporary node
function swapNodes(node1, node2) {
const temp = document.createComment('')
node2.replaceWith(temp)
node1.replaceWith(node2)
temp.replaceWith(node1)
}
Try this method:
Get the parent element
Store the two elements you want to swap
Store the .nextSibling of the node that is last in order
eg: [1,2,3,4] => we want to swap 3 & 2 then store nextSibling of 3, '4'.
.insertBefore(3,2);
.insertBefore(2,nextSibling);
Code Explanation
val & val2 are the 2 nodes/elements to be swapped
equiv(index) gets the present node/element in DOM at index passed as the paramter
NOTE: It will count comment & text elements so take care xD
Hopes this helps :)
function equiv(index){
return Array.prototype.slice.call( document.querySelectorAll("*"))[index];
}
function swap (val,val2){
let _key = val.key;
let _key_ = val2.key;
_key_ = _key < _key_ ? _key_+1:_key_;
let _parent_ = val2.parentElement.valueOf();
if (val.parentElement.children.length ==1)
val.parentElement.appendChild(val2);
else
val.parentElement.insertBefore(val2,val);
if (_parent_.children.length ==0)
_parent_.appendChild(val);
else{
let _sibling_ = equiv(_key_);
_parent_.insertBefore(val,_sibling_);}
}
A solution that works without cloning, given the indices of the two elements to swap:
function swapChildren(parentElement, index1, index2) {
if (index1 === index2)
return
if (index1 > index2) {
const temp = index1
index1 = index2
index2 = temp
}
const { [index1]: element1, [index2]: element2 } = parentElement.childNodes
if (index2 === index1 + 1) {
parentElement.insertBefore(element2, element1)
} else {
const reference = element2.nextSibling
parentElement.replaceChild(element2, element1)
parentElement.insertBefore(element1, reference)
}
}
You can swap a DOM element with its next sibling like that:
el.parentNode.insertBefore(el, el.nextElementSibling)
Or with its previous sibling like this:
el.parentNode.insertBefore(el, el.previousElementSibling)
And if your content is dynamic, you might want to check that el.nextElementSibling or el.previousElementSibling is not null.
My client has asked for the letter 4 to appear in red, wherever it is used in his website navigation.
For instance, where he has 'bikes4kids' as a menu item.
Unfortunately, I am using a 'mega menu' style plugin for his Magento site that only allows for plain text menu items - I cannot use HTML code in the menu item title box, which takes away the chance of me using <span>.
Is there a way of achieving this with JS? I assume not with CSS alone.
EDIT: The mega menu I am working with can be seen here: http://www.magentech.com/extensions/commercial-extensions/item/246-sm-mega-menu-responsive-magento-module
I did it.
Please have a look at this Link
<div class="title">menu1</div>
<div class="title">bike4kids</div>
<div class="title">menu2</div>
var avno = $(".title:nth-child(2)").text();
var avn = avno.split('4');
var item = avn[0]+"<span style='color:red'>4</span>"+avn[1];
$(".title:nth-child(2)").html(item);
No, within “plain text menu items” (as described in the question) you cannot style one character differently from others (except in a few very special cases, which do not apply here: styling the first letter, and setting the font of some characters different from others). JavaScript won’t help, because you would still need to make the character an element, and anything containing an element is by definition not plain text.
So you need to consider other approaches, like menus with items that allow some markup.
If you can process the document after it's finished loading, or sometime after magento has finished doing its thing, you can try the following. It will wrap a provided character in a span with a supplied class. A root element can be provided to limit the scope of the replace. If no root is provided, it searches the entire document.
// Simple function to convert NodeList to Array
// Not suitable for general application
function toArray(obj) {
var a = [];
for (var i=0, iLen=obj.length; i<iLen; i++) {
a[i] = obj[i];
}
return a;
}
// Highlight character c by wrapping in a span with class className
// starting with element root. If root not provided, document.body is used
function highlightChar(c, className, root) {
if (!root) root = document.body;
var frag, idx, t;
var re = new RegExp(c);
// Add tag names to ignore
var ignoreTags = {'script':'script'};
// Child nodes is a live NodeList, convert to array
// so don't have to deal with changing as nodes are added
var node, nodes = toArray(root.childNodes);
var span = document.createElement('span');
span.appendChild(document.createTextNode(c));
span.className = 'highlightChar';
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
// If node is a text node and contains the chacter, highlight it
if (node.nodeType == 3 && re.test(node.data)) {
t = node.data.split(re);
frag = document.createDocumentFragment();
// Insert higlight spans after first but not after last
for (var j=0, jLen = t.length-1; j<jLen; j++) {
frag.appendChild(document.createTextNode(t[j]));
frag.appendChild(span.cloneNode(true));
}
// Append last text node
if (j > 0 && t[j]) {
frag.appendChild(document.createTextNode(t[j]));
}
// Replace the original text node with higlighted fragment
node.parentNode.replaceChild(frag, node);
// Otherwise, if node is an element, process it
} else if (node.nodeType == 1 && !(node.tagName.toLowerCase() in ignoreTags)) {
highlightChar(c, className, node);
}
}
}
It can be used to process the entire document using:
window.onload = function() {
highlightChar('4','highlightChar');
};
Edit:
Modified to find menu-items in 'mega menu'... I hope. In the demo site the "$" variable isn't jQuery so I modified the answer as well to use the jQuery function.
Testing in the demo site I found that the letter I modified did color yellow, but there was a bullet added to the left of it - apparently their css adds a bullet to the left (ie. :before) every span...
After the plugin completes its DOM modifications - simply run over the menu items and search-and-replace "4" with a colored span
eg.
// loop over all dom elements with class 'menu-item'
// - I assume here below them exist only text
jQuery('.sm-megamenu-child span').each(function() {
var $item = jQuery(this);
var text = $item.text();
var modified = text.replace(/4/g, "<span style='color:yellow'>4</span>");
$item.html(modified);
})
Could you please look at this jsFiddle example, and tell me why the number '11' is alerted rather than '5' (the number of <li> elements)?
From jsFiddle:
HTML
<ul id="list">
<li>milk</li>
<li>butter</li>
<li>eggs</li>
<li>orange juice</li>
<li>bananas</li>
</ul>
JavaScript
var list = document.getElementById('list');
var list_items = list.childNodes;
alert(list_items.length);
The childNodes, depending on the browser used, will return the text nodes, as well as the tags that are children of the parent node. So technically, the whitespace in between the <li> tags will also be counted among the childNodes.
To avoid processing them, you may check that nodeType != 3. Here is a list of node types.
var list = document.getElementById('list');
var list_items = list.childNodes;
var li_items = [];
for (var i=0; i<list_items.length; i++) {
console.log(list_items[i].nodeType);
// Add all the <li> nodes to an array, skip the text nodes
if (list_items[i].nodeType != 3) {
li_items.push(list_items[i]);
}
}
You have text nodes there.
You can skip them while iterating with...
for (var i = 0, length = list_items.length; i < length; i++) {
if (list_items[i].nodeType != 1) {
continue;
}
// Any code here that accesses list_items[i] will sure to be an element.
}
jsFiddle.
Alternatively, you could do it in a more functional way...
list_items = Array.prototype.filter.call(list_items, function(element) {
return element.nodeType == 1;
});
jsFiddle.
You must use convert it to a proper array to use the filter() method. childNodes property returns a NodeList object.
As others have pointed out, the childNode count inclues the text nodes, generated by the whitespace between the <li> elements.
<ul id="list"><li>milk</li><li>butter</li><li>eggs</li><li>orange juice</li><li>bananas</li></ul>
That will give you 5 childNodes because it omits the whitespace.
Text nodes are included in the child nodes count. To get the proper value, you'd need to strip out text nodes, or make sure they are not in your code. Any white space between code is considered a space and a text node, so your count is the total number of text nodes.
I cobbled together a solution for this that I like. (I got the idea from this blog post.)
1) First I get the number of child elements nodes by using:
nodeObject.childElementCount;
2) Then I wrote a function that will return any child element node by index number. I did this by using firstElementChild and nextElementSibling in a for loop.
function getElement(x, parentNode){
var item = parentNode.firstElementChild
for (i=0;i<x;i++){
item = item.nextElementSibling;
}
return item;
}
This returns the child element I need for anything I want to pull from it. It skips the problem with childNodes retuning all the different nodes that are not helpful when trying to parse just the elements. I am sure someone more experienced than me could clean this up. But I found this so helpful that I had to post it.
Use obj.children instead.
var list = document.getElementById('list');
var list_items = list.children;
alert(list_items.length);
The difference between this children and childNodes, is that childNodes contain all nodes, including text nodes and comment nodes, while children only contain element nodes.
from w3schools.
This question already has answers here:
How to get the text node of an element?
(11 answers)
Closed 6 months ago.
I have a mild preference in solving this in pure JS, but if the jQuery version is simpler, then jQuery is fine too. Effectively the situation is like this
<span id="thisone">
The info I want
<span id="notthisone">
I don't want any of this nonsense
</span>
</span>
I effectively want to get
The info I want
but not
The info I want I don't want any of this nonsense
and I especially don't want
The info I want <span id="notthisone"> I don't want any of this nonsense </span>
which is unfortunately what I am getting right now...
How would I do this?
With js only:
Try it out: http://jsfiddle.net/g4tRn/
var result = document.getElementById('thisone').firstChild.nodeValue;
alert(result);
With jQuery:
Try it out: http://jsfiddle.net/g4tRn/1
var result = $('#thisone').contents().first().text();
alert(result);
Bonus:
If there are other text nodes in the outer <span> that you want to get, you could do something like this:
Try it out: http://jsfiddle.net/g4tRn/4
var nodes = document.getElementById('thisone').childNodes;
var result = '';
for(var i = 0; i < nodes.length; i++) {
if(nodes[i].nodeType == 3) { // If it is a text node,
result += nodes[i].nodeValue; // add its text to the result
}
}
alert(result);
If you just want the first child then it's rather simple. If you are looking for the first text-only element then this code will need some modification.
var text = document.getElementById('thisone').firstChild.nodeValue;
alert(text);
Have you tried something like this?
var thisone = $("#thisone").clone();
thisone.children().remove();
var mytext = thisone.html();
FROM: http://viralpatel.net/blogs/2011/02/jquery-get-text-element-without-child-element.html
$("#foo")
.clone() //clone the element
.children() //select all the children
.remove() //remove all the children
.end() //again go back to selected element
.text(); //get the text of element
Pure JavaScript
In this pure JavaScript example, I account for the possibility of multiple text nodes that could be interleaved with other kinds of nodes. Pass a containing NodeList in from calling / client code.
function getText (nodeList, target)
{
var trueTarget = target - 1;
var length = nodeList.length; // Because you may have many child nodes.
for (var i = 0; i < length; i++) {
if ((nodeList[i].nodeType === Node.TEXT_NODE) && (i === trueTarget)) {
return nodeList.childNodes[i].nodeValue;
}
}
return null;
}
You might use this function to create a wrapper function that uses this one to accumulate multiple text values.
To get a string of the child text nodes and not the element or other child nodes from a given element:
function getTextNodesText(el) {
return Array.from(el.childNodes)
.filter((child) => child.nodeType === Node.TEXT_NODE)
.map((child) => child.textContent)
.join("");
}