Javascript: look for xpath inside element - javascript

I get all the elements via the xpath below. I want to look inside each TD, and search for another xpath inside each TD. I am not sure if the below approach works but this is something I'd like to achieve. I know you can do //td//a but I want to know how I programmatically search each element for more xpaths.
var tds = document.evaluate('//td', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (++index < tds.snapshotLength){
//this link may or may not exist
var link = document.evaluate('//a', tds[index].document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);
}

Its sensible to define two helper functions that do the heavy lifting and return results that are easy to work with:
function selectNodes(path, contextNode) {
var result, item, nodes = [];
result = document.evaluate(path, contextNode || document, null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
while ( item = result.iterateNext() ) nodes.push(item);
return nodes;
}
function selectSingleNode(path, contextNode) {
return selectNodes(path, contextNode)[0];
}
Now using XPath becomes straight-forward:
var tds = selectNodes('//td'), i, a;
for (i = 0; i < tds.length; i++) {
a = selectSingleNode('.//a', tds[i]);
console.log(a.href);
}
Note the relative path ('.//a'). You need to use relative paths when you want correct results with a context node - a plain // always starts at the document root.

Your approach seems fine, but your for syntax is incorrect and you need to use snapshotItem to access the results of tds:
var tds = document.evaluate('//td', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var i;
for (i=0; i < tds.snapshotLength; i++) {
var link = document.evaluate('//a', tds.snapshotItem(i), null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
if(link.snapshotLength) {
console.log(link.snapshotItem(0));
}
}
jsFiddle

Related

How do I iterate through a javascript XPathResult that is of type ORDERED_NODE_SNAPSHOT_TYPE

I need to get an XPathResult with javascript and iterate through it, cloning each node of the result. Initially, I tried the following with the result as ORDERED_NODE_ITERATOR_TYPE:
childNodesXPath = '//div[#id="'+subcat_id+'" and #parentid="'+subcat_parent_id+'"]';
subcat_child_nodes = document.evaluate(childNodesXPath, document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
while (next_child_node = subcat_child_nodes.iterateNext()) {
new_child_node = next_child_node.cloneNode(true);
new_child_node.setAttribute('parentid', target_id);
new_child_node.setAttribute('grandparentid', target_parentid);
new_length = new_subcat_child_nodes.push(new_child_node);
}
Of course I discovered that the iterator became invalid as soon as the first node was cloned because the DOM changed, so then I tried this with the result as ORDERED_NODE_SNAPSHOT_TYPE:
childNodesXPath = '//div[#id="'+subcat_id+'" and #parentid="'+subcat_parent_id+'"]';
subcat_child_nodes = document.evaluate(childNodesXPath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (i=0; i<subcat_child_nodes.length; i++) {
new_child_node = subcat_child_nodes[i].cloneNode(true);
new_child_node.setAttribute('parentid', target_id);
new_child_node.setAttribute('grandparentid', target_parentid);
new_length = new_subcat_child_nodes.push(new_child_node);
}
This did not work because there is no length property for the XPathResult object. I also tried subcat_child_nodes.forEach() and that did not work, nor does iterateNext().
How do I iterate through an XPathResult that is of type ORDERED_NODE_SNAPSHOT_TYPE in a way that allows me to clone each node? If that is not possible, is there a way to clone an entire XPathResult that is a list of nodes?
So, just in case anyone else is searching for the answer to my question above, Jaromanda's answer in the comments pointed me to a reference resource (archive) and this is what I ended up using.
subcat_child_nodes = document.evaluate(childNodesXPath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (i=0; i<subcat_child_nodes.snapshotLength; i++) {
new_child_node = subcat_child_nodes.snapshotItem(i).cloneNode(true);
new_child_node.setAttribute('parentid', target_id);
new_child_node.setAttribute('grandparentid', target_parentid);
new_length = new_subcat_child_nodes.push(new_child_node);
}

How to use document.evaluate() and XPath to get a list of elements?

I'm using the document.evaluate() JavaScript method to get an element pointed to by an XPath expression:
var element = document.evaluate(
path,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
But how do I get a list of elements in case the XPath expression points to more than one element on the page?
I tried the following code, but it is not working:
var element = document.evaluate(
path,
document,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE,
null
);
I found the following solution in the book I am currently reading. It says that the code is from the Prototype library.
function getElementsByXPath(xpath, parent)
{
let results = [];
let query = document.evaluate(xpath, parent || document,
null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
results.push(query.snapshotItem(i));
}
return results;
}
Use it like this:
let items = getElementsByXPath("//*"); // return all elements on the page
From the documentation
var iterator = document.evaluate('//phoneNumber', documentNode, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
try {
var thisNode = iterator.iterateNext();
while (thisNode) {
alert( thisNode.textContent );
thisNode = iterator.iterateNext();
}
}
catch (e) {
dump( 'Error: Document tree modified during iteration ' + e );
}
Try this:
function getListOfElementsByXPath(xpath) {
var result = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);
return result;
}
Then call it:
var results = getListOfElementsByXPath("//YOUR_XPATH");
while (node = results.iterateNext()) {
console.log(node);
}
In Chrome, there is a simpler solution, described in this document, at least in the console:
$x(path)
It does the same as the getElementsByXPath function above, but much easier for debugging.
I was working hard with the same problem some weeks ago. I found out, that the result already represents a list of elements (if any) and one can iterate trough it. I needed to build a jQuery plugin for realize a search of partial or full text strings, which means the inner text of any DOM element like LI or H2. I got the initial understanding on his page : Document.evaluate() | MDN
After some hours I got the plugin running: Search for the word "architecture" only in "p" elements, find partial matching strings ("true" for <p>todays architecture in Europe</p>) instead of matches of entire text (<h2>architecture</h2>).
var found = $('div#pagecontent').findtext('architecture','p',true);
Found results are regular jQuery objects, which can be used as usual.
found.css({ backgroundColor: 'tomato'});
The example of usage above may be altered like this for search trough entire document and all node types like this (partial results)
var found = $('body').findtext('architecture','',true);
or only exact matches
var found = $('div#pagecontent').findtext('architecture');
The plugin itself shows a variable "es" which is the plural of a single "e" for "element". And you can see, how the results are iterated, and collected into a bunch of objects with f = f.add($(e)) (where "f" stands for "found"). The beginning of the function deals with different conditions, like full or partial search ("c" for condition) and the document range for the search ("d").
It may be optimized whereever needed, may not represent the maximum of possibilities, but it represents my best knowledge at the moment, is running without errors and it may answer your question, hopefully. And here is it:
(function($) {
$.fn.findtext = function(s,t,p) {
var c, d;
if (!this[0]) d = document.body;
else d = this[0];
if (!t || typeof t !== 'string' || t == '') t = '*';
if (p === true) c = './/'+t+'[contains(text(), "'+s+'")]';
else c = './/'+t+'[. = "'+s+'"]';
var es = document.evaluate(c, d, null, XPathResult.ANY_TYPE, null);
var e = es.iterateNext();
var f = false;
while (e) {
if (!f) f = $(e);
else f = f.add($(e));
e = es.iterateNext();
}
return f || $();
};
})(jQuery);

XPathResult and invalid iterator state

I have a relatively large (500-100 rows) HTML table with a bunch of <a> elements. I would like to add a <select> to the top of the page, and populate it by creating an <option> for each <a> in the table.
My first approach looked something like this:
var initSelect = function () {
var select = document.getElementById('mySelect');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
select.appendChild(elem);
item = items.iterateNext();
}
};
window.onload = initSelect;
As soon as I tried to appendChild() to the <select> I got an UncaughtInvalidStateError. I figured that modifying the DOM was invalidating my XPathResult iterator, so I tried to add all of the <option> elements to an array first, and then appending them after iterating through all of the results.
var initSelect = function () {
var select = document.getElementById('src_select');
var items = document.evaluate('//a', document, null, XPathResult.ANY_TYPE, null);
var elems = [];
var item = items.iterateNext();
while (item) {
var elem = document.createElement("option");
var val = document.createAttribute("value");
val.value = elem.nodeValue;
elem.setAttributeNode(val);
elem.innerHTML = item.innerHTML;
elems.push(elem);
item = items.iterateNext();
}
for (var i = 0; i < elems.length; i++) {
select.appendChild(elems[i]);
}
};
window.onload = initSelect;
If I step through the code in the debugger, I see items.invalidIteratorState go to true after executing the elem.innerHTML = item.innerHTML line. Then I get the same error on the next call to items.iterateNext().
The first thing I'd like to get working is to just see the <select> populated. After that, the goal is to be able to select an element in the drop down, and have the page navigate to the same link that the corresponding <a> element would have taken me to.
This is the first JavaScript I've written, so I appreciate any and all feedback. At this point, I'm looking for a pure JavaScript solution. Once I get it working I'm going to try to pull JQuery in and revise it.
document.links gives you all a href elements in the document so there is no need to use the XPath API to access those elements. And if you are looking for elements in a particular parent then use e.g. document.getElementById('foo').getElementsByTagName('a') to find all a elements in that element with id attribute being foo. I don't see why you would need the DOM Level 3 XPath API for that, which is not supported in IE anyways. And neither document.links nor the result of getElementsByTagName can be invalidated like an XPath iterator result.
If you really want to use the XPath API then try a snapshot as the result type, it should not fail the way the iterator fails due to document manipulation.

Get Element ID by String it Contains using Plain Javascript

How can I get an elemnts ID based on the string it contains?
<span id="th67">This the string I need to match</span>
I can't make use of JQuery or any other Javascript library to do this.
I need to do this for a selenium test.
I didn't realise how useless I am in JS without my libraries!
Thanks all for any help.
Well, if you know what kind of tag you're looking for, you can just do:
var spans = document.getElementsByTagName('span'), targetId;
for (var i = 0; i < spans.length; ++i) {
if (spans[i].innerText === stringToMatch) {
// found it ...
targetId = spans[i].id;
break;
}
}
if (targetId) {
// ... do whatever ...
}
If you want to get fancy you could construct an xpath query, I guess.
If the browsers you're targeting support XPath you can do a simple XPath query:
// Find an element by the text it contains, optionally
// starting at specified parent element.
function getElementByText( text, ctx)
{
return document.evaluate("//*[.='"+text+"']",
ctx || document, null, XPathResult.ANY_TYPE, null).iterateNext();
}
Then just run
var myElement = getElementByText( "This is the string I need to match" );
if ( myElement )
{
// do something with myElement.id
}
Here's a simple recursive function that will do it:
function findByText(node, text) {
if(node.nodeValue == text) {
return node.parentNode;
}
for (var i = 0; i < node.childNodes.length; i++) {
var returnValue = findByText(node.childNodes[i], text);
if (returnValue != null) {
return returnValue;
}
}
return null;
}
Use it as:
var target = findByText(document, "This the string I need to match");
This will end up with either target being null, or it being a DOM node whose id you can get with target.id.
See it in action.

trouble creating nested dom nodes in javascript

I've a function that takes an object as a parameter, and uses the structure of the object to create nested DOM nodes, but I receive the following error:
http://new.app/:75NOT_FOUND_ERR: DOM Exception 8: An attempt was made to reference a Node in a context where it does not exist.
What I would like my function to do, is, when supplied with a suitable object as a parameter, example:
var nodes = {
tweet: {
children: {
screen_name: {
tag: "h2"
},
text: {
tag: "p"
}
},
tag: "article"
}
};
It would create the following DOM nodes:
<article>
<h2></h2>
<p></p>
</article>
Here is my attempt so far:
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
if(obj[i].children) {
tmp.appendChild(create(obj[i].children)); /* error */
};
document.getElementById("tweets").appendChild(tmp);
};
};
I'm already struggling!
Ideally I'd like to eventually add more child key's to each object, not just tag, but also id, innerHTML, class etc.
Any hel would be much appreciated, though please: I'm sure a framework or library could do this for me in just a few lines of code, or something similar, but I'd prefer not to use one for this particular project.
If you could briefly explain your answers too it'd really help me learn how this all works, and where I went wrong!
Thank you!
NB: I've changed and marked the line in my function that the error message is talking about.
I changed it from:
mp.appendChild(obj[i].children);
to:
mp.appendChild(create(obj[i].children));
This is because I want any nested keys in the children object to also be created, so screen_name had a children key, they too would be created. Sorry, I hope you can understand this!
I'm looking at http://jsperf.com/create-nested-dom-structure for some pointers, this may help you too!
Your "create" function is going to have to be written recursively.
To create a node from your data (in general), you need to:
Find the "tag" property and create a new element
Give the element the "id" value of the element (taken from the data)
For each element in "children", make a node and append it
Thus:
function create(elementDescription) {
var nodes = [];
for (var n in elementDescription) {
if (!elementDescription.hasOwnProperty(n)) continue;
var elem = elementDescription[n];
var node = document.createElement(elem.tag);
node.id = n; // optional step
var cnodes = create(elem.children);
for (var c = 0; c < cnodes.length; ++c)
node.appendChild(cnodes[c]);
nodes.push(node);
}
return nodes;
}
That will return an array of document elements created from the original "specification" object. Thus from your example, you'd call:
var createdNodes = create(nodes);
and "createdNodes" would be an array of one element, an <article> tag with id "tweets". That element would have two children, an <h2> tag with id "screen_name" and a <p> tag with id "text". (Now that I think of it, you might want to skip the "id" assignment unless the node description has an explicit "id" entry, or something.)
Thus if you have a <div> in your page called "tweets" (to use your example, though if so you'd definitely want to cut out the "id" setting part of my function), you'd add the results like this:
var createdNodes = create(nodes), tweets = document.getElementById('tweets');
for (var eindex = 0; eindex < createdNodes.length; ++eindex)
tweets.appendChild(createdNodes[eindex]);
I added a function appendList that accepts a list of elements, and the container to append to. I removed the append to "tweets" part out of the create function to more effectively separate your code.
function create(obj) {
var els = [];
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
var childEls = create(children);
appendList(childEls, tmp);
}
els.push(tmp);
};
return els;
};
function appendList(list, container){
for(var i = 0, el; el = list[i]; i++){
container.appendChild(el);
}
};
// gets an array of root elements populated with children
var els = create(nodes);
// appends the array to "tweets"
appendList(els, document.getElementById("tweets"));
Building on the previous answer:
I think you still need to create the element you're trying to append:
tmp.appendChild(children[prop].tag);
should be
tmp.appendChild(document.createElement(children[prop].tag));
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
for(var prop in children)
tmp.appendChild(document.createElement(children[prop].tag));
}
document.getElementById("tweets").appendChild(tmp);
};
};

Categories