I am traversing a HTML document using javascript DOM. I want make a list (an array actually) of all nodes/elements and their values. I found a script for traversing DOM, but how do I store each node value in an array. I can't seem to find the unique identifier for a node. Anyone has any pointers? I was thinking of xpath or something.
Is it a good idea to consider xpath for node as the unique identifier. If so how do I get xpath of a element while traversing the DOM?
As programmer born and brought up in the world of C and C++, my first answer to this kind of question would have been "store their addresses in the array!". But after a couple years of messing around with the web way of things, I can give the right answer:
In javascript, you can directly store the references to the objects in the array.
And no, xpath is not a good idea for this; using references is simpler and better.
So a direct answer to your question is: there is no unique identifier for a DOM element/node except itself.
In javascript, all objects are passed around by reference. So here's a sample code for how to do it:
var theArray = [];
var theNodeToTraverse = document.getElementById('domelementtosearch');
traverseAndStore(theNodeToTraverse);
function traverseAndStore( node )
{
if( node==null) return;
theArray[ theArray.length ] = node;
for( i=0; i<node.childNodes.length; i++ )
traverseAndStore( node.childNodes[i] );
}
You can get something similar to xpath with something like this. It traverses the dom upwards from the input element through the parentNode property.
https://gist.github.com/sebjwallace/3c0a6f7493ce23134516
It will output a string like this.
"#document/HTML/BODY/DIV"
var getElementPath = function(el){
var path = el.nodeName;
var parent = el.parentNode;
while(parent){
path = parent.nodeName + '/' + path;
parent = parent.parentNode;
}
return path;
}
EDIT:
The question seems to point to a simple flatmap solution. I think my original answer was aimed at generating an address for each node in the DOM. This solution almost as basic as flatmap. Well, the DOM is a tree with N children per node. Given a snapshot of the DOM you can generate an address of each element given the child index. As an example of stackoverflow's DOM, grabbing the one of the nodes 5 levels deep - the address is 01001. Each address will be unique for every element in the DOM. This won't work if you need a static address for a dynamic web app however.
Related
Good day everyone,
I am currently trying to append a metadata file. Sorry in advance if I did anything wrong, I am unfamiliar with editing XML codes in JS.. Thanks!
Currently, I am having difficulty getting the results that I expected. I am trying to insert 2 new nodes one nested over the other into the newParentTestNode.
I want to add a couple of nodes within the TestNode as seen in the results I want.. I can't seem to find a solution online. Please do help thanks!
I am currently getting this result:
<gmd:MTTEST><TESTNODE2/></gmd:MTTEST>
But the result I want is:
<gmd:MTTEST>
<gmd:TestNode>
<gmd:TestNode2>
</gmd:TestNode2>
</gmd:TestNode>
</gmd:MTTEST>
xmlTest: function (evt) {
if(this.item.metadata_standard_name == "Correct Data"){
xmlString = this.item.sys_xml_clob;
var metadataXmlString = jQuery.parseXML(xmlString);
let newParentTestNode = metadataXmlString.getElementsByTagName("gmd:MTTEST")
newNode = metadataXmlString.createElement("TestNode")
newNode2 = metadataXmlString.createElement("TestNode2")
let addMe = newNode.appendChild(newNode2)
newParentTestNode[0].appendChild(addMe)
xmlString = (new XMLSerializer()).serializeToString(metadataXmlString);
console.log(xmlString)
}
appendChild() returns the node that is appended not the parent node.
This means that newNode.appendChild(newNode2) returns newNode2, which you'll then append to your root node, effectively removing TestNode2 from TestNode and appending it directly to MTTEST.
You don't need to assign the result of appendChild to a new addMe variable because appendChild modifies the structure in-place, so you gain nothing from the return value (as you already have variables referencing both the parent and the child element). So in the end you just need to append newNode (which will already contain newNode2) to newParentTestNode.
I've imported some XML files inside InDesign (you can see the structure in the picture below) and I've also created a script to get some statistics concerning this hierarchy.
For example, to count the "free" elements:
var items = app.activeDocument.xmlElements.everyItem();
var items1 = items.xmlElements.itemByName("cars");
var cars = items1.xmlElements.everyItem();
var c_free = cars.xmlElements.itemByName("free");
var cars_free = c_free.xmlElements.count().length;
I also have apartments in my structure that's why I'm using itemByName.
The code above returns the correct number of free cars in my structure.
What I'm trying to do - without any luck so far - is to target those free items (inside cars) and either delete all of them or a specific number.
My last attempt was using:
var del1 = myInputGroup2.add ("button", undefined, "Delete All");
del1.onClick = function () {
cars.xmlElements.everyItem().remove();
}
inside a dialog I've created.
Any suggestions will be appreciated cause I'm really stuck here.
I would probably use XPath for this. You can use evaluateXPathExpression to create an array of the elements you want to target. Assuming your root element is cars and it contains elements called cars1, and you want to delete all free elements within a cars1 element, you could do something like:
var myDoc = app.activeDocument;
//xmlElements[0] is your root element, in this case "cars". The xPath expression is evaluated from cars.
//evaluateXPathExpression returns an array of all of the free elements that are children of cars.
var myFrees = myDoc.xmlElements[0].evaluateXPathExpression("cars1/free");
for (var i = myFrees.length - 1; i>=0; i--){
myFrees[i].remove();
}
Tweaking this would require some knowledge of xPath, but it's not terribly hard to learn the basics and it does seem like the simplest approach.
I think your main problem was that XMLElements hasn't a itemByName method. You can only reference XMLElements through their indeces or ids.
Secondly you assume that you got xmlElements from XPath expression but it's likely that you got nothing as your xpath seems uncorrect.
var myFrees = myDoc.xmlElements[0].evaluateXPathExpression("./cars1/free");
var n = myFrees.length;
if ( !n ) {
alert("Aucun élément trouvé");
}
else {
while (n--) myFrees[n].remove();
}
You need to start your expression by setting the origin of your xpath. Here a dot "./" is used to tell you want to look for cars1/free xml elements at the "root" of the xmlelement. Using "//" on the contrary would have returned any cars/free items unregardingly of their locations.
So I have some HTML that I would like to be cloned several times and appended to the document. It's just typical HTML form stuff. But since things like <label for="something"> works based on element IDs, it would be nice if each HTML element within the jquery clone()'d element could have a unique ID, apart from all their cloned counterparts. Just as a guess to how I could do this, I wonder if it would be possible to make the IDs within my initial element all contain some unique string. I could then somehow traverse the element and replace that string with a _1,_2,_3 etc.
I haven't gotten very far, but it seems like something like this should work.
$(document).ready(function(){
var toReplace = "containerUniqueCharacterString1234";
var copy = $('#containerUniqueCharacterString1234').clone(true);
copy[0].style.display = "block"; //so i can reference the first element like this
console.log(copy[0]); //[object HTMLDivElement]
$('body').append(copy);
$.each(copy, function(index, value){
var children = copy[index].childNodes;
console.log(children); //[object NodeList]
var len = children.length;
if (copy[index].childNodes.length > 0){
if (copy[index].childNodes.hasOwnProperty('id')){
//replace the toReplace string with an incremented number here(?)
//annnnd this is where i've gotten in too deep and made this overly complex
}
}
$('#otherResults').append(len);
});
});
http://jsbin.com/ecojub/1/edit
Or perhaps there's a much much simpler way to do this.
Thanks!
If you are copying HTML elements many times for display only, maybe you should consider using a templating engine rather than copying the DOM elements, which are expensive and less maintainable. underscore has a pretty easy to use function, http://documentcloud.github.com/underscore/#template
I've searched quite a bit on both google and stackoverflow, but a lack of knowledge on how to ask the question (or even if I'm asking the right question at all) is making it hard to find pertinent information.
I have a simple block of code that I am experimenting with to teach myself javascript.
var studio = document.getElementById('studio');
var contact = document.getElementById('contact');
var nav = document.getElementById('nav');
var navLinks = nav.getElementsByTagName('a');
var title = navLinks.getAttribute('title');
I want to grab the title attribute from the links in the element with the ID 'nav'.
Whenever I look at the debugger, it tells me that Object #<NodeList> has no method 'getAttribute'
I have no idea where I'm going wrong.
The nodetype and nodevalue for navLinks comes back as undefined, which I believe may be part of the problem, but I'm so new to this that I honestly have no idea.
The getElementsByTagName method returns an array of objects. So you need to loop through this array in order to get individual elements and their attributes:
var navLinks = nav.getElementsByTagName('a');
for (var i = 0; i < navLinks.length; i++) {
var link = navLinks[i];
var title = link.title;
}
Calling nav.getElementsByTagName('a') returns list of objects. And that list doesn't have getAttribute() method. You must call it on ONE object.
When you do:
navLinks[0].getAttribute('title')
then it should work - you will get title of the first matched element.
var navLinks = nav.getElementsByTagName('a');
getElementsByTagName returns multiple elements (hence Elements), because there can be multiple elements on one page with the same tag name. A NodeList (which is a collection of nodes as returned by getElementsByTagName) does not have a getAttribute method.
You need to access the property of the element that you actually need. My guess is that this will be the first element you find.
var title = navLinks[0].getAttribute('title');
I so miss jQuery. I'm working on a project where I need to get my hands dirty with good 'ol plain Javascript again.
I have this scenario:
parent
child1
child2
child3
Via javascript, I want to be able to insert a new node before or after any of those children. While javascript has an insertBefore, there is no insertAfter.
Insert before would work fine on the above to insert a node before any one of those:
parent.insertBefore(newNode, child3)
But how does one insert a node AFTER child3? I'm using this at the moment:
for (i=0,i<myNodes.length,i++){
myParent.insertBefore(newNode, myNodes[i+1])
}
That is inserting my newNode before the next sibling node of each of my nodes (meaning it's putting it after each node).
When it gets to the last node, myNodes[i+1] become undefined as I'm now trying to access a array index that doesn't exist.
I'd think that'd error out, but it seems to work fine in that in that situation, my node is indeed inserted after the last node.
But is that proper? I'm testing it now in a few modern browsers with no seemingly ill effects. Is there a better way?
Pure JavaScript actually has a method for what you want:
parent.appendChild(child_to_be_last)
The functionality of insertBefore(newNode, referenceNode) is described as:
Inserts the specified node before a reference node as a child of the current node. If referenceNode is null, then newNode is inserted at the end of the list of child nodes.
And since myNodes[i+1] is outside of the array bounds, it returns undefined, which is treated as null in this case. This means you get your desired behavior.
Edit: Link to the W3 specification of insertBefore()
Modern Solution
If you want to position based on child, simply use before or after
child1.before(newNode) // [newNode, child1, child2]
// or
child1.after(newNode) // [child1, newNode, child2]
If you want to position based on parent, use prepend or append
parent.prepend(newNode) // [newNode, child1, child2]
// or
parent.append(newNode) // [child1, child2, newNode]
Advanced usage
You can pass multiple values (or use spread operator ...).
Any string value will be added as a text element.
Examples:
child1.after(newNode, "foo") // [child1, newNode, "foo", child2]
const list = ["bar", newNode]
parent.prepend(...list, "fizz") // ["bar", newNode, "fizz", child1, child2]
Mozilla Docs
before - after
prepend - append
Can I Use - 95% Mar 2021
To insert item as a last node use :parentNode.insertBefore(newNode, null);
Also when not iterating through children (just inserting after referenceNode) this might be useful:
parentNode.insertBefore(newNode, referenceNode.nextSibling);
I got this code is work to insert a link item as the last child
var cb=function(){
//create newnode
var link=document.createElement('link');
link.rel='stylesheet';link.type='text/css';link.href='css/style.css';
//insert after the lastnode
var nodes=document.getElementsByTagName('link'); //existing nodes
var lastnode=document.getElementsByTagName('link')[nodes.length-1];
lastnode.parentNode.insertBefore(link, lastnode.nextSibling);
};
var raf=requestAnimationFrame||mozRequestAnimationFrame||
webkitRequestAnimationFrame||msRequestAnimationFrame;
if (raf)raf(cb);else window.addEventListener('load',cb);