Tom
Tom

Reputation: 6729

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?

Upvotes: 7

Views: 4795

Answers (4)

Sweet HD
Sweet HD

Reputation: 11

you can use this :

const querySelectorInvers = (elem) => {
      let query = "";
      document.querySelectorAll('*').forEach((el) => {
        if (el !== elem) {
          query += ":not(" + el.tagName + ")";
        }
      });
      return query;
    }

Upvotes: 0

Kévin Berthommier
Kévin Berthommier

Reputation: 1572

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

Upvotes: 0

titusfx
titusfx

Reputation: 2016

You can create one that can work in all cases. In two ways:

  1. 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)

  1. Using attributes for a "human readable" way:
    1. Get the attributes of the elements ( only the explicit attributes).
    2. Get the entire path to the element.
    3. 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

Upvotes: 4

Edgar Villegas Alvarado
Edgar Villegas Alvarado

Reputation: 18354

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

Upvotes: 1

Related Questions