Alp
Alp

Reputation: 29739

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

Upvotes: 62

Views: 83095

Answers (16)

algorhythm
algorhythm

Reputation: 8728

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

Upvotes: 20

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

Upvotes: -1

Daniel De Le&#243;n
Daniel De Le&#243;n

Reputation: 13649

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

Upvotes: 2

Alp
Alp

Reputation: 29739

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

Upvotes: 39

Nevo David
Nevo David

Reputation: 137

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

Upvotes: -1

Rikki Nik
Rikki Nik

Reputation: 41

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

Upvotes: 4

Raphael Rafatpanah
Raphael Rafatpanah

Reputation: 19977

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

Upvotes: 6

Brett Zamir
Brett Zamir

Reputation: 14345

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

Upvotes: 4

Ashraf Sabry
Ashraf Sabry

Reputation: 3182

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

Upvotes: 1

Georgios Syngouroglou
Georgios Syngouroglou

Reputation: 19944

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

Upvotes: 2

Nate
Nate

Reputation: 13242

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

Upvotes: 3

Peeter
Peeter

Reputation: 9382

$(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.

Upvotes: 0

Jean-Philippe Gire
Jean-Philippe Gire

Reputation: 901

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.

Upvotes: -1

Eli
Eli

Reputation: 17825

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

Upvotes: 10

Gary Green
Gary Green

Reputation: 22395

(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

Upvotes: 6

Nick Brunt
Nick Brunt

Reputation: 10057

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.

Upvotes: 0

Related Questions