62

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><a href="bar">bar</a></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

7
  • An infinite number of selectors are valid and unique for any given element in the DOM. See stackoverflow.com/questions/5135287/… Commented Apr 18, 2011 at 17:56
  • Curious to know the need for this... Commented Apr 18, 2011 at 17:58
  • @BoltClock: i do not want to have all possible selectors, just one of them which is valid and unique. I don't mind which one. Commented Apr 18, 2011 at 18:02
  • @Cybernate: I am using Selenium 2 to interact with the browser in a Java application. It loads jQuery to each page to have further interaction possibilities. Commented Apr 18, 2011 at 18:04
  • 2
    possible duplicate of Getting a jQuery selector for an element Commented Apr 18, 2011 at 20:50

16 Answers 16

39

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;
    }
});
Sign up to request clarification or add additional context in comments.

7 Comments

+1 Nice solution, I improved that one for multiple jQuery returns. See my answer...
@Alp -- I'm new to jQuery...hoping you can tell me how this function is actually called when any item is clicked. Sounds like I have a similar need that you had, where I want to be tracking what is clicked on screen. Do you have some kind of listener function that is waiting for a click to call this 'getpath' function? Any help you can provide would be greatly appreciated. Thanks!
@Mark: just create a click listener and use this method as a callback
Just a note - the problem is more complex than it seems if you want selectors that are robust to changes in page structure, and there are 10+ libraries (comparison) that implement this functionality.
You're just linking to your non-answer.
|
20

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();

1 Comment

How about voting to close this question as a duplicate of the one you posted the same answer to?
10

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 + '"]');

5 Comments

Interesting idea, could be useful :)
this would not work after i reload the page. i want to be able to identify an element which a user refered to in a previous session
The only way you can do that is to either store the data using something like HTML5 storage, or keeping it on the server in a database. We are doing something similar to this.
I do not want to be dependent to document changes i made. I need a way to have a unique selector without changing the source document.
If the element has NOT an id already, this is a nice hack. But a if that checks for existing id in the element must be safer.
6
(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

1 Comment

this cannot be used after a page reload, to use the element in an external application. +1 for the xpath function
6

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'));

2 Comments

I'd suggest to check if the element has an id first, because then we would have a really simple selector without the need to traverse the DOM.
@DanielVeihelmann Sure, but that wouldn't work in a codebase that has multiple ids of the same name. That's happened to me way too many times
4

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;
}

Comments

4

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;
            }
        });

Comments

3

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();

2 Comments

There are 10+ libraries to generate CSS selectors for a given DOM node.
Perhaps there are mature, reliable and robust libraries now, but not when this answer was written. But, thanks for the link. css-selector-generator looks pretty interesting.
2

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();
}

Comments

2

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;
}

Comments

1

You may also have a look at findCssSelector. Code is in my other answer.

Comments

0

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.

5 Comments

I gotta say, binding a click event to every element on the page is a terrible idea. This will cause a huge performance load.
I've changed it to a class selector. The idea is the same though.
is there no effective way to have a click handler for each element on the page?
There is, just use $("*").click(), but as stated by Eli, there will be a performance trade-off for this. You'd be surprised how many elements there are on a standard web page. Just try adding this css rule to find out: * { border: 1px solid red; }
Weighing in late here, but the most efficient way to put a click handler on everything is to use a delegate() on the html element. You can then filter out elements if you like, but it basically means you only have one click handler which is delegated to handle everything. NOTE: delegate is deprecated as of jQuery v1.7, in favour of on(), but the signature is very similar
0
$(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.

1 Comment

This is what I get: Uncaught SyntaxError: Unexpected token =
-1

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.

1 Comment

-1

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
        );
    }

Comments

-1

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;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.