parentNode is null – traversing DOM to construct XPath - javascript

This function returns the XPath of element e, given a puppeteer handle e and its page page:
async function getXpathOfElement(page, e) {
return await page.evaluate(e => {
var xpath = "";
var pos, tmpitem2;
while (e !== document.documentElement) {
pos = 0;
tmpitem2 = e;
while (tmpitem2) {
// if is ELEMENT_NODE of the same name
if (tmpitem2.nodeType === 1 && tmpitem2.nodeName === e.nodeName) {
++pos;
}
tmpitem2 = tmpitem2.previousSibling;
}
xpath = e.nodeName + "[" + pos + ']' + '/' + xpath;
e = e.parentNode;
}
xpath = '/' + document.documentElement.nodeName + '/' + xpath;
xpath = xpath.replace(/\/$/, '');
return xpath;
}, e);
}
It works as intended, but not always: with a specific pair of page and e, after while completes a few loops, e.parentNode becomes null, with e being a valid <div> DOM node whose parent is a <li>, as I observed from html.
It's interesting that while completes a few loops before the issue, so the DOM structure is partially traversed.

Related

How to get the highest level parent of clicked element [duplicate]

HTML
<body>
<div class="lol">
<a class="rightArrow" href="javascriptVoid:(0);" title"Next image">
</div>
</body>
Pseudo Code
$(".rightArrow").click(function() {
rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole
alert(rightArrowParents);
});
Alert message would be:
body div.lol a.rightArrow
How can I get this with javascript/jquery?
Here is a native JS version that returns a jQuery path. I'm also adding IDs for elements if they have them. This would give you the opportunity to do the shortest path if you see an id in the array.
var path = getDomPath(element);
console.log(path.join(' > '));
Outputs
body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651
Here is the function.
function getDomPath(el) {
var stack = [];
while ( el.parentNode != null ) {
console.log(el.nodeName);
var sibCount = 0;
var sibIndex = 0;
for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
var sib = el.parentNode.childNodes[i];
if ( sib.nodeName == el.nodeName ) {
if ( sib === el ) {
sibIndex = sibCount;
}
sibCount++;
}
}
if ( el.hasAttribute('id') && el.id != '' ) {
stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
} else if ( sibCount > 1 ) {
stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')');
} else {
stack.unshift(el.nodeName.toLowerCase());
}
el = el.parentNode;
}
return stack.slice(1); // removes the html element
}
Using jQuery, like this (followed by a solution that doesn't use jQuery except for the event; lots fewer function calls, if that's important):
$(".rightArrow").click(function () {
const rightArrowParents = [];
$(this)
.parents()
.addBack()
.not("html")
.each(function () {
let entry = this.tagName.toLowerCase();
const className = this.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
});
console.log(rightArrowParents.join(" "));
return false;
});
Live example:
$(".rightArrow").click(function () {
const rightArrowParents = [];
$(this)
.parents()
.addBack()
.not("html")
.each(function () {
let entry = this.tagName.toLowerCase();
const className = this.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
});
console.log(rightArrowParents.join(" "));
return false;
});
<div class=" lol multi ">
Click here
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
(In the live examples, I've updated the class attribute on the div to be lol multi to demonstrate handling multiple classes.)
That uses parents to get the ancestors of the element that was clicked, removes the html element from that via not (since you started at body), then loops through creating entries for each parent and pushing them on an array. Then we use addBack to add the a back into the set, which also changes the order of the set to what you wanted (parents is special, it gives you the parents in the reverse of the order you wanted, but then addBack puts it back in DOM order). Then it uses Array#join to create the space-delimited string.
When creating the entry, we trim className (since leading and trailing spaces are preserved, but meaningless, in the class attribute), and then if there's anything left we replace any series of one or more spaces with a . to support elements that have more than one class (<p class='foo bar'> has className = "foo bar", so that entry ends up being p.foo.bar).
Just for completeness, this is one of those places where jQuery may be overkill, you can readily do this just by walking up the DOM:
$(".rightArrow").click(function () {
const rightArrowParents = [];
for (let elm = this; elm; elm = elm.parentNode) {
let entry = elm.tagName.toLowerCase();
if (entry === "html") {
break;
}
const className = elm.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
}
rightArrowParents.reverse();
console.log(rightArrowParents.join(" "));
return false;
});
Live example:
$(".rightArrow").click(function () {
const rightArrowParents = [];
for (let elm = this; elm; elm = elm.parentNode) {
let entry = elm.tagName.toLowerCase();
if (entry === "html") {
break;
}
const className = elm.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
}
rightArrowParents.reverse();
console.log(rightArrowParents.join(" "));
return false;
});
<div class=" lol multi ">
Click here
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
There we just use the standard parentNode property (or we could use parentElement) of the element repeatedly to walk up the tree until either we run out of parents or we see the html element. Then we reverse our array (since it's backward to the output you wanted), and join it, and we're good to go.
I needed a native JS version, that returns CSS standard path (not jQuery), and deals with ShadowDOM. This code is a minor update on Michael Connor's answer, just in case someone else needs it:
function getDomPath(el) {
if (!el) {
return;
}
var stack = [];
var isShadow = false;
while (el.parentNode != null) {
// console.log(el.nodeName);
var sibCount = 0;
var sibIndex = 0;
// get sibling indexes
for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
var sib = el.parentNode.childNodes[i];
if ( sib.nodeName == el.nodeName ) {
if ( sib === el ) {
sibIndex = sibCount;
}
sibCount++;
}
}
// if ( el.hasAttribute('id') && el.id != '' ) { no id shortcuts, ids are not unique in shadowDom
// stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
// } else
var nodeName = el.nodeName.toLowerCase();
if (isShadow) {
nodeName += "::shadow";
isShadow = false;
}
if ( sibCount > 1 ) {
stack.unshift(nodeName + ':nth-of-type(' + (sibIndex + 1) + ')');
} else {
stack.unshift(nodeName);
}
el = el.parentNode;
if (el.nodeType === 11) { // for shadow dom, we
isShadow = true;
el = el.host;
}
}
stack.splice(0,1); // removes the html element
return stack.join(' > ');
}
Here is a solution for exact matching of an element.
It is important to understand that the selector (it is not a real one) that the chrome tools show do not uniquely identify an element in the DOM. (for example it will not distinguish between a list of consecutive span elements. there is no positioning/indexing info)
An adaptation from a similar (about xpath) answer
$.fn.fullSelector = function () {
var path = this.parents().addBack();
var quickCss = path.get().map(function (item) {
var self = $(item),
id = item.id ? '#' + item.id : '',
clss = item.classList.length ? item.classList.toString().split(' ').map(function (c) {
return '.' + c;
}).join('') : '',
name = item.nodeName.toLowerCase(),
index = self.siblings(name).length ? ':nth-child(' + (self.index() + 1) + ')' : '';
if (name === 'html' || name === 'body') {
return name;
}
return name + index + id + clss;
}).join(' > ');
return quickCss;
};
And you can use it like this
console.log( $('some-selector').fullSelector() );
Demo at http://jsfiddle.net/gaby/zhnr198y/
The short vanilla ES6 version I ended up using:
Returns the output I'm used to read in Chrome inspector e.g body div.container input#name
function getDomPath(el) {
let nodeName = el.nodeName.toLowerCase();
if (el === document.body) return 'body';
if (el.id) nodeName += '#' + el.id;
else if (el.classList.length)
nodeName += '.' + [...el.classList].join('.');
return getDomPath(el.parentNode) + ' ' + nodeName;
};
I moved the snippet from T.J. Crowder to a tiny jQuery Plugin. I used the jQuery version of him even if he's right that this is totally unnecessary overhead, but i only use it for debugging purpose so i don't care.
Usage:
Html
<html>
<body>
<!-- Two spans, the first will be chosen -->
<div>
<span>Nested span</span>
</div>
<span>Simple span</span>
<!-- Pre element -->
<pre>Pre</pre>
</body>
</html>
Javascript
// result (array): ["body", "div.sampleClass"]
$('span').getDomPath(false)
// result (string): body > div.sampleClass
$('span').getDomPath()
// result (array): ["body", "div#test"]
$('pre').getDomPath(false)
// result (string): body > div#test
$('pre').getDomPath()
Repository
https://bitbucket.org/tehrengruber/jquery.dom.path
I've been using Michael Connor's answer and made a few improvements to it.
Using ES6 syntax
Using nth-of-type instead of nth-child, since nth-of-type looks for children of the same type, rather than any child
Removing the html node in a cleaner way
Ignoring the nodeName of elements with an id
Only showing the path until the closest id, if any. This should make the code a bit more resilient, but I left a comment on which line to remove if you don't want this behavior
Use CSS.escape to handle special characters in IDs and node names
~
export default function getDomPath(el) {
const stack = []
while (el.parentNode !== null) {
let sibCount = 0
let sibIndex = 0
for (let i = 0; i < el.parentNode.childNodes.length; i += 1) {
const sib = el.parentNode.childNodes[i]
if (sib.nodeName === el.nodeName) {
if (sib === el) {
sibIndex = sibCount
break
}
sibCount += 1
}
}
const nodeName = CSS.escape(el.nodeName.toLowerCase())
// Ignore `html` as a parent node
if (nodeName === 'html') break
if (el.hasAttribute('id') && el.id !== '') {
stack.unshift(`#${CSS.escape(el.id)}`)
// Remove this `break` if you want the entire path
break
} else if (sibIndex > 0) {
// :nth-of-type is 1-indexed
stack.unshift(`${nodeName}:nth-of-type(${sibIndex + 1})`)
} else {
stack.unshift(nodeName)
}
el = el.parentNode
}
return stack
}
All the examples from other ответов did not work very correctly for me, I made my own, maybe my version will be more suitable for the rest
const getDomPath = element => {
let templateElement = element
, stack = []
for (;;) {
if (!!templateElement) {
let attrs = ''
for (let i = 0; i < templateElement.attributes.length; i++) {
const name = templateElement.attributes[i].name
if (name === 'class' || name === 'id') {
attrs += `[${name}="${templateElement.getAttribute(name)}"]`
}
}
stack.push(templateElement.tagName.toLowerCase() + attrs)
templateElement = templateElement.parentElement
} else {
break
}
}
return stack.reverse().slice(1).join(' > ')
}
const currentElement = document.querySelectorAll('[class="serp-item__thumb justifier__thumb"]')[7]
const path = getDomPath(currentElement)
console.log(path)
console.log(document.querySelector(path))
console.log(currentElement)
var obj = $('#show-editor-button'),
path = '';
while (typeof obj.prop('tagName') != "undefined"){
if (obj.attr('class')){
path = '.'+obj.attr('class').replace(/\s/g , ".") + path;
}
if (obj.attr('id')){
path = '#'+obj.attr('id') + path;
}
path = ' ' +obj.prop('tagName').toLowerCase() + path;
obj = obj.parent();
}
console.log(path);
hello this function solve the bug related to current element not show in the path
check this now
$j(".wrapper").click(function(event) {
selectedElement=$j(event.target);
var rightArrowParents = [];
$j(event.target).parents().not('html,body').each(function() {
var entry = this.tagName.toLowerCase();
if (this.className) {
entry += "." + this.className.replace(/ /g, '.');
}else if(this.id){
entry += "#" + this.id;
}
entry=replaceAll(entry,'..','.');
rightArrowParents.push(entry);
});
rightArrowParents.reverse();
//if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1"){
var entry = event.target.nodeName.toLowerCase();
if (event.target.className) {
entry += "." + event.target.className.replace(/ /g, '.');
}else if(event.target.id){
entry += "#" + event.target.id;
}
rightArrowParents.push(entry);
// }
where $j = jQuery Variable
also solve the issue with .. in class name
here is replace function :
function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function replaceAll(str, find, replace) {
return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
}
Thanks
$(".rightArrow")
.parents()
.map(function () {
var value = this.tagName.toLowerCase();
if (this.className) {
value += '.' + this.className.replace(' ', '.', 'g');
}
return value;
})
.get().reverse().join(", ");

Does an Inverse querySelector exist?

I know I can can use querySelector to locate an element in a document
var element = document.querySelector(".myclass")
but does there exist a inverse querySelector such that:
var selector = document.inverseQuerySelector(element);
Assert.AreEqual(element, document.querySelector(selector));
the returned selector of inverseQuerySelector always uniquely identifies the specified element?
You can create one that can work in all cases. In two ways:
Using nth-child ( which is using index)
The solution is the following:
function getMyPathByIndex(element){
if(element == null)
return '';
if(element.parentElement == null)
return 'html'
return getMyPathByIndex(element.parentElement) + '>' + ':nth-child(' + getMyIndex(element) + ')';
}
function getMyIndex(element){
if(element == null)
return -1;
if(element.parentElement == null)
return 0;
let parent = element.parentElement;
for(var index = 0; index < parent.childElementCount; index++)
if(parent.children[index] == element)
return index + 1;
}
For instance, the element:
<a id="y" class="vote-up-off" title="This answer is useful">up vote</a>
You can get this element in this page just by typing in the console:
document.querySelector('a[title="This answer is useful"]');
has it unique querySelector:
html>:nth-child(2)>:nth-child(5)>:nth-child(1)>:nth-child(1)>:nth-child(3)>:nth-child(2)>:nth-child(6)>:nth-child(1)>:nth-child(1)>:nth-child(1)>:nth-child(1)>:nth-child(1)>:nth-child(2)
Using attributes for a "human readable" way:
Get the attributes of the elements ( only the explicit attributes).
Get the entire path to the element.
Use the multiple attribute selector to unify all the features.
Using the same elemen before has it unique querySelector:
html>body>div>div>div>div>div>div>table>tbody>tr>td>div>a[id="y"][class="vote-up-off"][title="This
answer is useful"]
test if the solution is the correct by:
// e is an element and e must exist inside the page
document.querySelector( getMyPathByIndex(e)) === e &&
document.querySelectorAll( getMyPathByIndex(e)).length === 1
The code solutions is the follow:
function convertAttributesToQuerySelector(element){
var tagName = element.tagName.toLowerCase();
var result = tagName;
Array.prototype.slice.call(element.attributes).forEach( function(item) {
if(element.outerHTML.contains(item.name))
result += '[' + item.name +'="' + item.value + '"]';
});
return result;
//["a[id="y"]", "a[class="vote-up-off"]", "a[title="This answer is useful"]"]
}
function getMyPath(element){
if(element.parentElement.tagName == 'HTML')
return 'html';
return getMyPath(element.parentElement) + '>' + element.parentElement.tagName.toLowerCase() ;
//"html>body>div>div>div>div>div>div>table>tbody>tr>td>div"
}
function myUniqueQuerySelector(element){
var elementPath = getMyPath(element);
var simpleSelector = convertAttributesToQuerySelector(element);
return elementPath + '>' + simpleSelector;
}
You can always test if the solution is the correct by:
// e is an element and e must exist inside the page
document.querySelector( myUniqueQuerySelector(e)) === e &&
document.querySelectorAll( myUniqueQuerySelector(e)).length === 1
No, because there are many selectors (probably infinite) that can select the same element.
For a function to be inversable (even in math), it's mapping has to be 1 to 1, this is not the case.
BTW, Because of that, you could create some that may work only in some cases. For example:
function inverseForElementWithUniqueId(element){
return '#' + element.id;
}
var selector = inverseForElementWithUniqueId(element);
Assert.AreEqual(element, document.querySelector(selector)); //True
(code which indeed may look trivial)
But as said, because of the theory, this would work only in a subset of the cases.
But, it would work only sometimes
I mixed the 2 solutions proposed to have a result readable by humans and which gives the right element if there are several similar siblings:
function elemToSelector(elem) {
const {
tagName,
id,
className,
parentNode
} = elem;
if (tagName === 'HTML') return 'HTML';
let str = tagName;
str += (id !== '') ? `#${id}` : '';
if (className) {
const classes = className.split(/\s/);
for (let i = 0; i < classes.length; i++) {
str += `.${classes[i]}`;
}
}
let childIndex = 1;
for (let e = elem; e.previousElementSibling; e = e.previousElementSibling) {
childIndex += 1;
}
str += `:nth-child(${childIndex})`;
return `${elemToSelector(parentNode)} > ${str}`;
}
Test with:
// Select an element in Elements tab of your navigator Devtools, or replace $0
document.querySelector(elemToSelector($0)) === $0 &&
document.querySelectorAll(elemToSelector($0)).length === 1
Which might give you something like, it's a bit longer but it's readable and it always works:
HTML > BODY:nth-child(2) > DIV.container:nth-child(2) > DIV.row:nth-child(2) > DIV.col-md-4:nth-child(2) > DIV.sidebar:nth-child(1) > DIV.sidebar-wrapper:nth-child(2) > DIV.my-4:nth-child(1) > H4:nth-child(3)
Edit: I just found the package unique-selector
you can use this :
const querySelectorInvers = (elem) => {
let query = "";
document.querySelectorAll('*').forEach((el) => {
if (el !== elem) {
query += ":not(" + el.tagName + ")";
}
});
return query;
}

Extracting XPath query for a given element

Is there a way to be extracted the XPath query string with javascript for a given element, like the firebug "Copy XPath" functionality.
Thanks
Did you mean that you need something like this:
var getXPath = function(aNode) {
var xpath = '', prevSibling = aNode, position = 1, nodeType = aNode.nodeType, nodeName = aNode.nodeName;
while (prevSibling = prevSibling.previousSibling) {
if (prevSibling.nodeType == nodeType && prevSibling.nodeName == nodeName) {
position += 1;
}
}
xpath = ((nodeType == 3 /* TEXT_NODE */) ? 'text()' : nodeName) + '[' + position + ']' + (xpath.length ? '/' + xpath : '');
if (aNode.parentNode && aNode.parentNode.nodeName != 'BODY') {
return xpath = (getXPath(aNode.parentNode, xpath) + '/' + xpath).toLowerCase();
}
return xpath;
};

Get unique selector of element in Jquery

I want to create something like a recorder whichs tracks all actions of a user. For that, i need to identify elements the user interacts with, so that i can refer to these elements in a later session.
Spoken in pseudo-code, i want to be able to do something like the following
Sample HTML (could be of any complexity):
<html>
<body>
<div class="example">
<p>foo</p>
<span>bar</span>
</div>
</body>
</html>
User clicks on something, like the link. Now i need to identify the clicked element and save its location in the DOM tree for later usage:
(any element).onclick(function() {
uniqueSelector = $(this).getUniqueSelector();
})
Now, uniqueSelector should be something like (i don't mind if it is xpath or css selector style):
html > body > div.example > span > a
This would provide the possibility to save that selector string and use it at a later time, to replay the actions the user made.
How is that possible?
Update
Got my answer: Getting a jQuery selector for an element
I'll answer this myself, because i found a solution which i had to modify. The following script is working and is based on a script of Blixt:
jQuery.fn.extend({
getPath: function () {
var path, node = this;
while (node.length) {
var realNode = node[0], name = realNode.name;
if (!name) break;
name = name.toLowerCase();
var parent = node.parent();
var sameTagSiblings = parent.children(name);
if (sameTagSiblings.length > 1) {
var allSiblings = parent.children();
var index = allSiblings.index(realNode) + 1;
if (index > 1) {
name += ':nth-child(' + index + ')';
}
}
path = name + (path ? '>' + path : '');
node = parent;
}
return path;
}
});
Same solution like that one from #Alp but compatible with multiple jQuery elements.
jQuery('.some-selector') can result in one or many DOM elements. #Alp's solution works only with the first one. My solution concatenates all the patches with , if necessary.
jQuery.fn.extend({
getPath: function() {
var pathes = [];
this.each(function(index, element) {
var path, $node = jQuery(element);
while ($node.length) {
var realNode = $node.get(0), name = realNode.localName;
if (!name) { break; }
name = name.toLowerCase();
var parent = $node.parent();
var sameTagSiblings = parent.children(name);
if (sameTagSiblings.length > 1)
{
var allSiblings = parent.children();
var index = allSiblings.index(realNode) + 1;
if (index > 0) {
name += ':nth-child(' + index + ')';
}
}
path = name + (path ? ' > ' + path : '');
$node = parent;
}
pathes.push(path);
});
return pathes.join(',');
}
});
If you want just handle the first element do it like this:
jQuery('.some-selector').first().getPath();
// or
jQuery('.some-selector:first').getPath();
I think a better solution would be to generate a random id and then access an element based on that id:
Assigning unique id:
// or some other id-generating algorithm
$(this).attr('id', new Date().getTime());
Selecting based on the unique id:
// getting unique id
var uniqueId = $(this).getUniqueId();
// or you could just get the id:
var uniqueId = $(this).attr('id');
// selecting by id:
var element = $('#' + uniqueId);
// if you decide to use another attribute other than id:
var element = $('[data-unique-id="' + uniqueId + '"]');
(any element).onclick(function() {
uniqueSelector = $(this).getUniqueSelector();
})
this IS the unique selector and path to that clicked element. Why not use that? You can utilise jquery's $.data() method to set the jquery selector. Alternatively just push the elements you need to use in the future:
var elements = [];
(any element).onclick(function() {
elements.push(this);
})
If you really need the xpath, you can calculate it using the following code:
function getXPath(node, path) {
path = path || [];
if(node.parentNode) {
path = getXPath(node.parentNode, path);
}
if(node.previousSibling) {
var count = 1;
var sibling = node.previousSibling
do {
if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {count++;}
sibling = sibling.previousSibling;
} while(sibling);
if(count == 1) {count = null;}
} else if(node.nextSibling) {
var sibling = node.nextSibling;
do {
if(sibling.nodeType == 1 && sibling.nodeName == node.nodeName) {
var count = 1;
sibling = null;
} else {
var count = null;
sibling = sibling.previousSibling;
}
} while(sibling);
}
if(node.nodeType == 1) {
path.push(node.nodeName.toLowerCase() + (node.id ? "[#id='"+node.id+"']" : count > 0 ? "["+count+"]" : ''));
}
return path;
};
Reference: http://snippets.dzone.com/posts/show/4349
Pure JavaScript Solution
Note: This uses Array.from and Array.prototype.filter, both of which need to be polyfilled in IE11.
function getUniqueSelector(node) {
let selector = "";
while (node.parentElement) {
const siblings = Array.from(node.parentElement.children).filter(
e => e.tagName === node.tagName
);
selector =
(siblings.indexOf(node)
? `${node.tagName}:nth-of-type(${siblings.indexOf(node) + 1})`
: `${node.tagName}`) + `${selector ? " > " : ""}${selector}`;
node = node.parentElement;
}
return `html > ${selector.toLowerCase()}`;
}
Usage
getUniqueSelector(document.getElementsByClassName('SectionFour')[0]);
getUniqueSelector(document.getElementById('content'));
While the question was for jQuery, in ES6, it is pretty easy to get something similar to #Alp's for Vanilla JavaScript (I've also added a couple lines, tracking a nameCount, to minimize use of nth-child):
function getSelectorForElement (elem) {
let path;
while (elem) {
let subSelector = elem.localName;
if (!subSelector) {
break;
}
subSelector = subSelector.toLowerCase();
const parent = elem.parentElement;
if (parent) {
const sameTagSiblings = parent.children;
if (sameTagSiblings.length > 1) {
let nameCount = 0;
const index = [...sameTagSiblings].findIndex((child) => {
if (elem.localName === child.localName) {
nameCount++;
}
return child === elem;
}) + 1;
if (index > 1 && nameCount > 1) {
subSelector += ':nth-child(' + index + ')';
}
}
}
path = subSelector + (path ? '>' + path : '');
elem = parent;
}
return path;
}
I found for my self some modified solution. I added to path selector #id, .className and cut the lenght of path to #id:
$.fn.extend({
getSelectorPath: function () {
var path,
node = this,
realNode,
name,
parent,
index,
sameTagSiblings,
allSiblings,
className,
classSelector,
nestingLevel = true;
while (node.length && nestingLevel) {
realNode = node[0];
name = realNode.localName;
if (!name) break;
name = name.toLowerCase();
parent = node.parent();
sameTagSiblings = parent.children(name);
if (realNode.id) {
name += "#" + node[0].id;
nestingLevel = false;
} else if (realNode.className.length) {
className = realNode.className.split(' ');
classSelector = '';
className.forEach(function (item) {
classSelector += '.' + item;
});
name += classSelector;
} else if (sameTagSiblings.length > 1) {
allSiblings = parent.children();
index = allSiblings.index(realNode) + 1;
if (index > 1) {
name += ':nth-child(' + index + ')';
}
}
path = name + (path ? '>' + path : '');
node = parent;
}
return path;
}
});
This answer does not satisfy the original question description, however it does answer the title question. I came to this question looking for a way to get a unique selector for an element but I didn't have a need for the selector to be valid between page-loads. So, my answer will not work between page-loads.
I feel like modifying the DOM is not idel, but it is a good way to build a selector that is unique without a tun of code. I got this idea after reading #Eli's answer:
Assign a custom attribute with a unique value.
$(element).attr('secondary_id', new Date().getTime())
var secondary_id = $(element).attr('secondary_id');
Then use that unique id to build a CSS Selector.
var selector = '[secondary_id='+secondary_id+']';
Then you have a selector that will select your element.
var found_again = $(selector);
And you many want to check to make sure there isn't already a secondary_id attribute on the element.
if ($(element).attr('secondary_id')) {
$(element).attr('secondary_id', (new Date()).getTime());
}
var secondary_id = $(element).attr('secondary_id');
Putting it all together
$.fn.getSelector = function(){
var e = $(this);
// the `id` attribute *should* be unique.
if (e.attr('id')) { return '#'+e.attr('id') }
if (e.attr('secondary_id')) {
return '[secondary_id='+e.attr('secondary_id')+']'
}
$(element).attr('secondary_id', (new Date()).getTime());
return '[secondary_id='+e.attr('secondary_id')+']'
};
var selector = $('*').first().getSelector();
In case you have an identity attribute (for example id="something"), you should get the value of it like,
var selector = "[id='" + $(yourObject).attr("id") + "']";
console.log(selector); //=> [id='something']
console.log($(selector).length); //=> 1
In case you do not have an identity attribute and you want to get the selector of it, you can create an identity attribute. Something like the above,
var uuid = guid();
$(yourObject).attr("id", uuid); // Set the uuid as id of your object.
You can use your own guid method, or use the source code found in this so answer,
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
My Vanilla JavaScript function:
function getUniqueSelector( element ) {
if (element.id) {
return '#' + element.id;
} else if (element.tagName === 'BODY') {
return 'BODY';
} else {
return `${getUniqueSelector(element.parentElement)} > ${element.tagName}:nth-child(${myIndexOf(element)})`;
}
}
function myIndexOf( element ) {
let index = 1;
// noinspection JSAssignmentUsedAsCondition
while (element = element.previousElementSibling) index++;
return index;
}
You may also have a look at findCssSelector. Code is in my other answer.
You could do something like this:
$(".track").click(function() {
recordEvent($(this).attr("id"));
});
It attaches an onclick event handler to every object with the track class. Each time an object is clicked, its id is fed into the recordEvent() function. You could make this function record the time and id of each object or whatever you want.
$(document).ready(function() {
$("*").click(function(e) {
var path = [];
$.each($(this).parents(), function(index, value) {
var id = $(value).attr("id");
var class = $(value).attr("class");
var element = $(value).get(0).tagName
path.push(element + (id.length > 0 ? " #" + id : (class.length > 0 ? " .": "") + class));
});
console.log(path.reverse().join(">"));
return false;
});
});
Working example: http://jsfiddle.net/peeter/YRmr5/
You'll probably run into issues when using the * selector (very slow) and stopping the event from bubbling up, but cannot really help there without more HTML code.
You could do something like that (untested)
function GetPathToElement(jElem)
{
var tmpParent = jElem;
var result = '';
while(tmpParent != null)
{
var tagName = tmpParent.get().tagName;
var className = tmpParent.get().className;
var id = tmpParent.get().id;
if( id != '') result = '#' + id + result;
if( className !='') result = '.' + className + result;
result = '>' + tagName + result;
tmpParent = tmpParent.parent();
}
return result;
}
this function will save the "path" to the element, now to find the element again in the future it's gonna be nearly impossible the way html is because in this function i don't save the sibbling index of each element,i only save the id(s) and classes.
So unless each and every-element of your html document have an ID this approach won't work.
Getting the dom path using jquery and typescript functional programming
function elementDomPath( element: any, selectMany: boolean, depth: number ) {
const elementType = element.get(0).tagName.toLowerCase();
if (elementType === 'body') {
return '';
}
const id = element.attr('id');
const className = element.attr('class');
const name = elementType + ((id && `#${id}`) || (className && `.${className.split(' ').filter((a: any) => a.trim().length)[0]}`) || '');
const parent = elementType === 'html' ? undefined : element.parent();
const index = (id || !parent || selectMany) ? '' : ':nth-child(' + (Array.from(element[0].parentNode.children).indexOf(element[0]) + 1) + ')';
return !parent ? 'html' : (
elementDomPath(parent, selectMany, depth + 1) +
' ' +
name +
index
);
}
Pass the js element (node) to this function.. working little bit..
try and post your comments
function getTargetElement_cleanSelector(element){
let returnCssSelector = '';
if(element != undefined){
returnCssSelector += element.tagName //.toLowerCase()
if(element.className != ''){
returnCssSelector += ('.'+ element.className.split(' ').join('.'))
}
if(element.id != ''){
returnCssSelector += ( '#' + element.id )
}
if(document.querySelectorAll(returnCssSelector).length == 1){
return returnCssSelector;
}
if(element.name != undefined && element.name.length > 0){
returnCssSelector += ( '[name="'+ element.name +'"]' )
}
if(document.querySelectorAll(returnCssSelector).length == 1){
return returnCssSelector;
}
console.log(returnCssSelector)
let current_parent = element.parentNode;
let unique_selector_for_parent = getTargetElement_cleanSelector(current_parent);
returnCssSelector = ( unique_selector_for_parent + ' > ' + returnCssSelector )
console.log(returnCssSelector)
if(document.querySelectorAll(returnCssSelector).length == 1){
return returnCssSelector;
}
}
return returnCssSelector;
}

What is the JavaScript equivalent of var_dump or print_r in PHP? [duplicate]

This question already has answers here:
Is there an equivalent for var_dump (PHP) in Javascript?
(20 answers)
Closed 5 years ago.
I would like to see the structure of object in JavaScript (for debugging). Is there anything similar to var_dump in PHP?
Most modern browsers have a console in their developer tools, useful for this sort of debugging.
console.log(myvar);
Then you will get a nicely mapped out interface of the object/whatever in the console.
Check out the console documentation for more details.
Most common way:
console.log(object);
However I must mention JSON.stringify which is useful to dump variables in non-browser scripts:
console.log( JSON.stringify(object) );
The JSON.stringify function also supports built-in prettification as pointed out by Simon Zyx.
Example:
var obj = {x: 1, y: 2, z: 3};
console.log( JSON.stringify(obj, null, 2) ); // spacing level = 2
The above snippet will print:
{
"x": 1,
"y": 2,
"z": 3
}
On caniuse.com you can view the browsers that support natively the JSON.stringify function: http://caniuse.com/json
You can also use the Douglas Crockford library to add JSON.stringify support on old browsers: https://github.com/douglascrockford/JSON-js
Docs for JSON.stringify: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
I hope this helps :-)
I wrote this JS function dump() to work like PHP's var_dump().
To show the contents of the variable in an alert window: dump(variable)
To show the contents of the variable in the web page: dump(variable, 'body')
To just get a string of the variable: dump(variable, 'none')
/* repeatString() returns a string which has been repeated a set number of times */
function repeatString(str, num) {
out = '';
for (var i = 0; i < num; i++) {
out += str;
}
return out;
}
/*
dump() displays the contents of a variable like var_dump() does in PHP. dump() is
better than typeof, because it can distinguish between array, null and object.
Parameters:
v: The variable
howDisplay: "none", "body", "alert" (default)
recursionLevel: Number of times the function has recursed when entering nested
objects or arrays. Each level of recursion adds extra space to the
output to indicate level. Set to 0 by default.
Return Value:
A string of the variable's contents
Limitations:
Can't pass an undefined variable to dump().
dump() can't distinguish between int and float.
dump() can't tell the original variable type of a member variable of an object.
These limitations can't be fixed because these are *features* of JS. However, dump()
*/
function dump(v, howDisplay, recursionLevel) {
howDisplay = (typeof howDisplay === 'undefined') ? "alert" : howDisplay;
recursionLevel = (typeof recursionLevel !== 'number') ? 0 : recursionLevel;
var vType = typeof v;
var out = vType;
switch (vType) {
case "number":
/* there is absolutely no way in JS to distinguish 2 from 2.0
so 'number' is the best that you can do. The following doesn't work:
var er = /^[0-9]+$/;
if (!isNaN(v) && v % 1 === 0 && er.test(3.0)) {
out = 'int';
}
*/
break;
case "boolean":
out += ": " + v;
break;
case "string":
out += "(" + v.length + '): "' + v + '"';
break;
case "object":
//check if null
if (v === null) {
out = "null";
}
//If using jQuery: if ($.isArray(v))
//If using IE: if (isArray(v))
//this should work for all browsers according to the ECMAScript standard:
else if (Object.prototype.toString.call(v) === '[object Array]') {
out = 'array(' + v.length + '): {\n';
for (var i = 0; i < v.length; i++) {
out += repeatString(' ', recursionLevel) + " [" + i + "]: " +
dump(v[i], "none", recursionLevel + 1) + "\n";
}
out += repeatString(' ', recursionLevel) + "}";
}
else {
//if object
let sContents = "{\n";
let cnt = 0;
for (var member in v) {
//No way to know the original data type of member, since JS
//always converts it to a string and no other way to parse objects.
sContents += repeatString(' ', recursionLevel) + " " + member +
": " + dump(v[member], "none", recursionLevel + 1) + "\n";
cnt++;
}
sContents += repeatString(' ', recursionLevel) + "}";
out += "(" + cnt + "): " + sContents;
}
break;
default:
out = v;
break;
}
if (howDisplay == 'body') {
var pre = document.createElement('pre');
pre.innerHTML = out;
document.body.appendChild(pre);
}
else if (howDisplay == 'alert') {
alert(out);
}
return out;
}
The var_dump equivalent in JavaScript? Simply, there isn't one.
But, that doesn't mean you're left helpless. Like some have suggested, use Firebug (or equivalent in other browsers), but unlike what others suggested, don't use console.log when you have a (slightly) better tool console.dir:
console.dir(object)
Prints an interactive listing of all properties of the object. This
looks identical to the view that you would see in the DOM tab.
As others have already mentioned, the best way to debug your variables is to use a modern browser's developer console (e.g. Chrome Developer Tools, Firefox+Firebug, Opera Dragonfly (which now disappeared in the new Chromium-based (Blink) Opera, but as developers say, "Dragonfly is not dead though we cannot give you more information yet").
But in case you need another approach, there's a really useful site called
php.js:
http://phpjs.org/
which provides "JavaScript alternatives to PHP functions" - so you can use them the similar way as you would in PHP. I will copy-paste the appropriate functions to you here, BUT be aware that these codes can get updated on the original site in case some bugs are detected, so I suggest you visiting the phpjs.org site! (Btw. I'm NOT affiliated with the site, but I find it extremely useful.)
var_dump() in JavaScript
Here is the code of the JS-alternative of var_dump():
http://phpjs.org/functions/var_dump/
it depends on the echo() function: http://phpjs.org/functions/echo/
function var_dump() {
// discuss at: http://phpjs.org/functions/var_dump/
// original by: Brett Zamir (http://brett-zamir.me)
// improved by: Zahlii
// improved by: Brett Zamir (http://brett-zamir.me)
// depends on: echo
// note: For returning a string, use var_export() with the second argument set to true
// test: skip
// example 1: var_dump(1);
// returns 1: 'int(1)'
var output = '',
pad_char = ' ',
pad_val = 4,
lgth = 0,
i = 0;
var _getFuncName = function(fn) {
var name = (/\W*function\s+([\w\$]+)\s*\(/)
.exec(fn);
if (!name) {
return '(Anonymous)';
}
return name[1];
};
var _repeat_char = function(len, pad_char) {
var str = '';
for (var i = 0; i < len; i++) {
str += pad_char;
}
return str;
};
var _getInnerVal = function(val, thick_pad) {
var ret = '';
if (val === null) {
ret = 'NULL';
} else if (typeof val === 'boolean') {
ret = 'bool(' + val + ')';
} else if (typeof val === 'string') {
ret = 'string(' + val.length + ') "' + val + '"';
} else if (typeof val === 'number') {
if (parseFloat(val) == parseInt(val, 10)) {
ret = 'int(' + val + ')';
} else {
ret = 'float(' + val + ')';
}
}
// The remaining are not PHP behavior because these values only exist in this exact form in JavaScript
else if (typeof val === 'undefined') {
ret = 'undefined';
} else if (typeof val === 'function') {
var funcLines = val.toString()
.split('\n');
ret = '';
for (var i = 0, fll = funcLines.length; i < fll; i++) {
ret += (i !== 0 ? '\n' + thick_pad : '') + funcLines[i];
}
} else if (val instanceof Date) {
ret = 'Date(' + val + ')';
} else if (val instanceof RegExp) {
ret = 'RegExp(' + val + ')';
} else if (val.nodeName) {
// Different than PHP's DOMElement
switch (val.nodeType) {
case 1:
if (typeof val.namespaceURI === 'undefined' || val.namespaceURI === 'http://www.w3.org/1999/xhtml') {
// Undefined namespace could be plain XML, but namespaceURI not widely supported
ret = 'HTMLElement("' + val.nodeName + '")';
} else {
ret = 'XML Element("' + val.nodeName + '")';
}
break;
case 2:
ret = 'ATTRIBUTE_NODE(' + val.nodeName + ')';
break;
case 3:
ret = 'TEXT_NODE(' + val.nodeValue + ')';
break;
case 4:
ret = 'CDATA_SECTION_NODE(' + val.nodeValue + ')';
break;
case 5:
ret = 'ENTITY_REFERENCE_NODE';
break;
case 6:
ret = 'ENTITY_NODE';
break;
case 7:
ret = 'PROCESSING_INSTRUCTION_NODE(' + val.nodeName + ':' + val.nodeValue + ')';
break;
case 8:
ret = 'COMMENT_NODE(' + val.nodeValue + ')';
break;
case 9:
ret = 'DOCUMENT_NODE';
break;
case 10:
ret = 'DOCUMENT_TYPE_NODE';
break;
case 11:
ret = 'DOCUMENT_FRAGMENT_NODE';
break;
case 12:
ret = 'NOTATION_NODE';
break;
}
}
return ret;
};
var _formatArray = function(obj, cur_depth, pad_val, pad_char) {
var someProp = '';
if (cur_depth > 0) {
cur_depth++;
}
var base_pad = _repeat_char(pad_val * (cur_depth - 1), pad_char);
var thick_pad = _repeat_char(pad_val * (cur_depth + 1), pad_char);
var str = '';
var val = '';
if (typeof obj === 'object' && obj !== null) {
if (obj.constructor && _getFuncName(obj.constructor) === 'PHPJS_Resource') {
return obj.var_dump();
}
lgth = 0;
for (someProp in obj) {
lgth++;
}
str += 'array(' + lgth + ') {\n';
for (var key in obj) {
var objVal = obj[key];
if (typeof objVal === 'object' && objVal !== null && !(objVal instanceof Date) && !(objVal instanceof RegExp) &&
!
objVal.nodeName) {
str += thick_pad + '[' + key + '] =>\n' + thick_pad + _formatArray(objVal, cur_depth + 1, pad_val,
pad_char);
} else {
val = _getInnerVal(objVal, thick_pad);
str += thick_pad + '[' + key + '] =>\n' + thick_pad + val + '\n';
}
}
str += base_pad + '}\n';
} else {
str = _getInnerVal(obj, thick_pad);
}
return str;
};
output = _formatArray(arguments[0], 0, pad_val, pad_char);
for (i = 1; i < arguments.length; i++) {
output += '\n' + _formatArray(arguments[i], 0, pad_val, pad_char);
}
this.echo(output);
}
print_r() in JavaScript
Here is the print_r() function:
http://phpjs.org/functions/print_r/
It depends on echo() too.
function print_r(array, return_val) {
// discuss at: http://phpjs.org/functions/print_r/
// original by: Michael White (http://getsprink.com)
// improved by: Ben Bryan
// improved by: Brett Zamir (http://brett-zamir.me)
// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
// input by: Brett Zamir (http://brett-zamir.me)
// depends on: echo
// example 1: print_r(1, true);
// returns 1: 1
var output = '',
pad_char = ' ',
pad_val = 4,
d = this.window.document,
getFuncName = function(fn) {
var name = (/\W*function\s+([\w\$]+)\s*\(/)
.exec(fn);
if (!name) {
return '(Anonymous)';
}
return name[1];
};
repeat_char = function(len, pad_char) {
var str = '';
for (var i = 0; i < len; i++) {
str += pad_char;
}
return str;
};
formatArray = function(obj, cur_depth, pad_val, pad_char) {
if (cur_depth > 0) {
cur_depth++;
}
var base_pad = repeat_char(pad_val * cur_depth, pad_char);
var thick_pad = repeat_char(pad_val * (cur_depth + 1), pad_char);
var str = '';
if (typeof obj === 'object' && obj !== null && obj.constructor && getFuncName(obj.constructor) !==
'PHPJS_Resource') {
str += 'Array\n' + base_pad + '(\n';
for (var key in obj) {
if (Object.prototype.toString.call(obj[key]) === '[object Array]') {
str += thick_pad + '[' + key + '] => ' + formatArray(obj[key], cur_depth + 1, pad_val, pad_char);
} else {
str += thick_pad + '[' + key + '] => ' + obj[key] + '\n';
}
}
str += base_pad + ')\n';
} else if (obj === null || obj === undefined) {
str = '';
} else {
// for our "resource" class
str = obj.toString();
}
return str;
};
output = formatArray(array, 0, pad_val, pad_char);
if (return_val !== true) {
if (d.body) {
this.echo(output);
} else {
try {
// We're in XUL, so appending as plain text won't work; trigger an error out of XUL
d = XULDocument;
this.echo('<pre xmlns="http://www.w3.org/1999/xhtml" style="white-space:pre;">' + output + '</pre>');
} catch (e) {
// Outputting as plain text may work in some plain XML
this.echo(output);
}
}
return true;
}
return output;
}
var_export() in JavaScript
You may also find the var_export() alternative useful, which also depends on echo():
http://phpjs.org/functions/var_export/
function var_export(mixed_expression, bool_return) {
// discuss at: http://phpjs.org/functions/var_export/
// original by: Philip Peterson
// improved by: johnrembo
// improved by: Brett Zamir (http://brett-zamir.me)
// input by: Brian Tafoya (http://www.premasolutions.com/)
// input by: Hans Henrik (http://hanshenrik.tk/)
// bugfixed by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: Brett Zamir (http://brett-zamir.me)
// depends on: echo
// example 1: var_export(null);
// returns 1: null
// example 2: var_export({0: 'Kevin', 1: 'van', 2: 'Zonneveld'}, true);
// returns 2: "array (\n 0 => 'Kevin',\n 1 => 'van',\n 2 => 'Zonneveld'\n)"
// example 3: data = 'Kevin';
// example 3: var_export(data, true);
// returns 3: "'Kevin'"
var retstr = '',
iret = '',
value,
cnt = 0,
x = [],
i = 0,
funcParts = [],
// We use the last argument (not part of PHP) to pass in
// our indentation level
idtLevel = arguments[2] || 2,
innerIndent = '',
outerIndent = '',
getFuncName = function(fn) {
var name = (/\W*function\s+([\w\$]+)\s*\(/)
.exec(fn);
if (!name) {
return '(Anonymous)';
}
return name[1];
};
_makeIndent = function(idtLevel) {
return (new Array(idtLevel + 1))
.join(' ');
};
__getType = function(inp) {
var i = 0,
match, types, cons, type = typeof inp;
if (type === 'object' && (inp && inp.constructor) &&
getFuncName(inp.constructor) === 'PHPJS_Resource') {
return 'resource';
}
if (type === 'function') {
return 'function';
}
if (type === 'object' && !inp) {
// Should this be just null?
return 'null';
}
if (type === 'object') {
if (!inp.constructor) {
return 'object';
}
cons = inp.constructor.toString();
match = cons.match(/(\w+)\(/);
if (match) {
cons = match[1].toLowerCase();
}
types = ['boolean', 'number', 'string', 'array'];
for (i = 0; i < types.length; i++) {
if (cons === types[i]) {
type = types[i];
break;
}
}
}
return type;
};
type = __getType(mixed_expression);
if (type === null) {
retstr = 'NULL';
} else if (type === 'array' || type === 'object') {
outerIndent = _makeIndent(idtLevel - 2);
innerIndent = _makeIndent(idtLevel);
for (i in mixed_expression) {
value = this.var_export(mixed_expression[i], 1, idtLevel + 2);
value = typeof value === 'string' ? value.replace(/</g, '<')
.
replace(/>/g, '>'): value;
x[cnt++] = innerIndent + i + ' => ' +
(__getType(mixed_expression[i]) === 'array' ?
'\n' : '') + value;
}
iret = x.join(',\n');
retstr = outerIndent + 'array (\n' + iret + '\n' + outerIndent + ')';
} else if (type === 'function') {
funcParts = mixed_expression.toString()
.
match(/function .*?\((.*?)\) \{([\s\S]*)\}/);
// For lambda functions, var_export() outputs such as the following:
// '\000lambda_1'. Since it will probably not be a common use to
// expect this (unhelpful) form, we'll use another PHP-exportable
// construct, create_function() (though dollar signs must be on the
// variables in JavaScript); if using instead in JavaScript and you
// are using the namespaced version, note that create_function() will
// not be available as a global
retstr = "create_function ('" + funcParts[1] + "', '" +
funcParts[2].replace(new RegExp("'", 'g'), "\\'") + "')";
} else if (type === 'resource') {
// Resources treated as null for var_export
retstr = 'NULL';
} else {
retstr = typeof mixed_expression !== 'string' ? mixed_expression :
"'" + mixed_expression.replace(/(["'])/g, '\\$1')
.
replace(/\0/g, '\\0') + "'";
}
if (!bool_return) {
this.echo(retstr);
return null;
}
return retstr;
}
echo() in JavaScript
http://phpjs.org/functions/echo/
function echo() {
// discuss at: http://phpjs.org/functions/echo/
// original by: Philip Peterson
// improved by: echo is bad
// improved by: Nate
// improved by: Brett Zamir (http://brett-zamir.me)
// improved by: Brett Zamir (http://brett-zamir.me)
// improved by: Brett Zamir (http://brett-zamir.me)
// revised by: Der Simon (http://innerdom.sourceforge.net/)
// bugfixed by: Eugene Bulkin (http://doubleaw.com/)
// bugfixed by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: Brett Zamir (http://brett-zamir.me)
// bugfixed by: EdorFaus
// input by: JB
// note: If browsers start to support DOM Level 3 Load and Save (parsing/serializing),
// note: we wouldn't need any such long code (even most of the code below). See
// note: link below for a cross-browser implementation in JavaScript. HTML5 might
// note: possibly support DOMParser, but that is not presently a standard.
// note: Although innerHTML is widely used and may become standard as of HTML5, it is also not ideal for
// note: use with a temporary holder before appending to the DOM (as is our last resort below),
// note: since it may not work in an XML context
// note: Using innerHTML to directly add to the BODY is very dangerous because it will
// note: break all pre-existing references to HTMLElements.
// example 1: echo('<div><p>abc</p><p>abc</p></div>');
// returns 1: undefined
var isNode = typeof module !== 'undefined' && module.exports && typeof global !== "undefined" && {}.toString.call(
global) == '[object global]';
if (isNode) {
var args = Array.prototype.slice.call(arguments);
return console.log(args.join(' '));
}
var arg = '';
var argc = arguments.length;
var argv = arguments;
var i = 0;
var holder, win = this.window;
var d = win.document;
var ns_xhtml = 'http://www.w3.org/1999/xhtml';
// If we're in a XUL context
var ns_xul = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
var stringToDOM = function(str, parent, ns, container) {
var extraNSs = '';
if (ns === ns_xul) {
extraNSs = ' xmlns:html="' + ns_xhtml + '"';
}
var stringContainer = '<' + container + ' xmlns="' + ns + '"' + extraNSs + '>' + str + '</' + container + '>';
var dils = win.DOMImplementationLS;
var dp = win.DOMParser;
var ax = win.ActiveXObject;
if (dils && dils.createLSInput && dils.createLSParser) {
// Follows the DOM 3 Load and Save standard, but not
// implemented in browsers at present; HTML5 is to standardize on innerHTML, but not for XML (though
// possibly will also standardize with DOMParser); in the meantime, to ensure fullest browser support, could
// attach http://svn2.assembla.com/svn/brettz9/DOMToString/DOM3.js (see http://svn2.assembla.com/svn/brettz9/DOMToString/DOM3.xhtml for a simple test file)
var lsInput = dils.createLSInput();
// If we're in XHTML, we'll try to allow the XHTML namespace to be available by default
lsInput.stringData = stringContainer;
// synchronous, no schema type
var lsParser = dils.createLSParser(1, null);
return lsParser.parse(lsInput)
.firstChild;
} else if (dp) {
// If we're in XHTML, we'll try to allow the XHTML namespace to be available by default
try {
var fc = new dp()
.parseFromString(stringContainer, 'text/xml');
if (fc && fc.documentElement && fc.documentElement.localName !== 'parsererror' && fc.documentElement.namespaceURI !==
'http://www.mozilla.org/newlayout/xml/parsererror.xml') {
return fc.documentElement.firstChild;
}
// If there's a parsing error, we just continue on
} catch (e) {
// If there's a parsing error, we just continue on
}
} else if (ax) {
// We don't bother with a holder in Explorer as it doesn't support namespaces
var axo = new ax('MSXML2.DOMDocument');
axo.loadXML(str);
return axo.documentElement;
}
/*else if (win.XMLHttpRequest) {
// Supposed to work in older Safari
var req = new win.XMLHttpRequest;
req.open('GET', 'data:application/xml;charset=utf-8,'+encodeURIComponent(str), false);
if (req.overrideMimeType) {
req.overrideMimeType('application/xml');
}
req.send(null);
return req.responseXML;
}*/
// Document fragment did not work with innerHTML, so we create a temporary element holder
// If we're in XHTML, we'll try to allow the XHTML namespace to be available by default
//if (d.createElementNS && (d.contentType && d.contentType !== 'text/html')) {
// Don't create namespaced elements if we're being served as HTML (currently only Mozilla supports this detection in true XHTML-supporting browsers, but Safari and Opera should work with the above DOMParser anyways, and IE doesn't support createElementNS anyways)
if (d.createElementNS && // Browser supports the method
(d.documentElement.namespaceURI || // We can use if the document is using a namespace
d.documentElement.nodeName.toLowerCase() !== 'html' || // We know it's not HTML4 or less, if the tag is not HTML (even if the root namespace is null)
(d.contentType && d.contentType !== 'text/html') // We know it's not regular HTML4 or less if this is Mozilla (only browser supporting the attribute) and the content type is something other than text/html; other HTML5 roots (like svg) still have a namespace
)) {
// Don't create namespaced elements if we're being served as HTML (currently only Mozilla supports this detection in true XHTML-supporting browsers, but Safari and Opera should work with the above DOMParser anyways, and IE doesn't support createElementNS anyways); last test is for the sake of being in a pure XML document
holder = d.createElementNS(ns, container);
} else {
// Document fragment did not work with innerHTML
holder = d.createElement(container);
}
holder.innerHTML = str;
while (holder.firstChild) {
parent.appendChild(holder.firstChild);
}
return false;
// throw 'Your browser does not support DOM parsing as required by echo()';
};
var ieFix = function(node) {
if (node.nodeType === 1) {
var newNode = d.createElement(node.nodeName);
var i, len;
if (node.attributes && node.attributes.length > 0) {
for (i = 0, len = node.attributes.length; i < len; i++) {
newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i].nodeName));
}
}
if (node.childNodes && node.childNodes.length > 0) {
for (i = 0, len = node.childNodes.length; i < len; i++) {
newNode.appendChild(ieFix(node.childNodes[i]));
}
}
return newNode;
} else {
return d.createTextNode(node.nodeValue);
}
};
var replacer = function(s, m1, m2) {
// We assume for now that embedded variables do not have dollar sign; to add a dollar sign, you currently must use {$$var} (We might change this, however.)
// Doesn't cover all cases yet: see http://php.net/manual/en/language.types.string.php#language.types.string.syntax.double
if (m1 !== '\\') {
return m1 + eval(m2);
} else {
return s;
}
};
this.php_js = this.php_js || {};
var phpjs = this.php_js;
var ini = phpjs.ini;
var obs = phpjs.obs;
for (i = 0; i < argc; i++) {
arg = argv[i];
if (ini && ini['phpjs.echo_embedded_vars']) {
arg = arg.replace(/(.?)\{?\$(\w*?\}|\w*)/g, replacer);
}
if (!phpjs.flushing && obs && obs.length) {
// If flushing we output, but otherwise presence of a buffer means caching output
obs[obs.length - 1].buffer += arg;
continue;
}
if (d.appendChild) {
if (d.body) {
if (win.navigator.appName === 'Microsoft Internet Explorer') {
// We unfortunately cannot use feature detection, since this is an IE bug with cloneNode nodes being appended
d.body.appendChild(stringToDOM(ieFix(arg)));
} else {
var unappendedLeft = stringToDOM(arg, d.body, ns_xhtml, 'div')
.cloneNode(true); // We will not actually append the div tag (just using for providing XHTML namespace by default)
if (unappendedLeft) {
d.body.appendChild(unappendedLeft);
}
}
} else {
// We will not actually append the description tag (just using for providing XUL namespace by default)
d.documentElement.appendChild(stringToDOM(arg, d.documentElement, ns_xul, 'description'));
}
} else if (d.write) {
d.write(arg);
} else {
console.log(arg);
}
}
}
Firebug.
Then, in your javascript:
var blah = {something: 'hi', another: 'noway'};
console.debug("Here is blah: %o", blah);
Now you can look at the console, click on the statement and see what is inside blah
A nice simple solution for parsing a JSON Response to HTML.
var json_response = jQuery.parseJSON(data);
html_response += 'JSON Response:<br />';
jQuery.each(json_response, function(k, v) {
html_response += outputJSONReponse(k, v);
});
function outputJSONReponse(k, v) {
var html_response = k + ': ';
if(jQuery.isArray(v) || jQuery.isPlainObject(v)) {
jQuery.each(v, function(j, w) {
html_response += outputJSONReponse(j, w);
});
} else {
html_response += v + '<br />';
}
return html_response;
}
You could also try this function. I can't remember the original author, but all credits go to them.
Works like a charm - 100% the same as var_dump in PHP.
Check it out.
function dump(arr,level) {
var dumped_text = "";
if(!level) level = 0;
//The padding given at the beginning of the line.
var level_padding = "";
for(var j=0;j<level+1;j++) level_padding += " ";
if(typeof(arr) == 'object') { //Array/Hashes/Objects
for(var item in arr) {
var value = arr[item];
if(typeof(value) == 'object') { //If it is an array,
dumped_text += level_padding + "'" + item + "' ...\n";
dumped_text += dump(value,level+1);
} else {
dumped_text += level_padding + "'" + item + "' => \"" + value + "\"\n";
}
}
} else { //Stings/Chars/Numbers etc.
dumped_text = "===>"+arr+"<===("+typeof(arr)+")";
}
return dumped_text;
}
// Example:
var employees = [
{ id: '1', sex: 'm', city: 'Paris' },
{ id: '2', sex: 'f', city: 'London' },
{ id: '3', sex: 'f', city: 'New York' },
{ id: '4', sex: 'm', city: 'Moscow' },
{ id: '5', sex: 'm', city: 'Berlin' }
]
// Open dev console (F12) to see results:
console.log(dump(employees));
I put this forward to help anyone needing something readily practical for giving you a nice, prettified (indented) picture of a JS Node. None of the other solutions worked for me for a Node ("cyclical error" or whatever...). This walks you through the tree under the DOM Node (without using recursion) and gives you the depth, tagName (if applicable) and textContent (if applicable).
Any other details from the nodes you encounter as you walk the tree under the head node can be added as per your interest...
function printRNode( node ){
// make sort of human-readable picture of the node... a bit like PHP print_r
if( node === undefined || node === null ){
throwError( 'node was ' + typeof node );
}
let s = '';
// NB walkDOM could be made into a utility function which you could
// call with one or more callback functions as parameters...
function walkDOM( headNode ){
const stack = [ headNode ];
const depthCountDowns = [ 1 ];
while (stack.length > 0) {
const node = stack.pop();
const depth = depthCountDowns.length - 1;
// TODO non-text, non-BR nodes could show more details (attributes, properties, etc.)
const stringRep = node.nodeType === 3? 'TEXT: |' + node.nodeValue + '|' : 'tag: ' + node.tagName;
s += ' '.repeat( depth ) + stringRep + '\n';
const lastIndex = depthCountDowns.length - 1;
depthCountDowns[ lastIndex ] = depthCountDowns[ lastIndex ] - 1;
if( node.childNodes.length ){
depthCountDowns.push( node.childNodes.length );
stack.push( ... Array.from( node.childNodes ).reverse() );
}
while( depthCountDowns[ depthCountDowns.length - 1 ] === 0 ){
depthCountDowns.splice( -1 );
}
}
}
walkDOM( node );
return s;
}

Categories

Resources