Test if a selector matches a given element

后端 未结 7 1545
庸人自扰
庸人自扰 2020-11-27 05:58

Is there any way to test if a selector would match a given DOM Element? Preferably, without the use of an external library like Sizzle. This is for a library and I would li

相关标签:
7条回答
  • 2020-11-27 06:36

    The W3C selectors API (http://www.w3.org/TR/selectors-api/) specifies document.querySelectorAll(). This is not supported on all browsers, so you'd have to google the ones that do support it: http://www.google.com/search?q=browsers+implementing+selector+api

    0 讨论(0)
  • 2020-11-27 06:36

    Just use an id for your element? HTML-IDs have to be unique…

    0 讨论(0)
  • 2020-11-27 06:38

    Modern browsers can do it with the document.querySelectorAll function.

    http://www.w3.org/TR/selectors-api/

    0 讨论(0)
  • 2020-11-27 06:39

    In the absence of xMatchesSelector, I'm thinking to try adding a style with the requested selector to a styleSheet object, along with some arbitrary rule and value that is not likely to be already in use. Then check the computed/currentStyle of the element to see if it has inherited the added CSS rule. Something like this for IE:

    function ieMatchesSelector(selector, element) {
      var styleSheet = document.styleSheets[document.styleSheets.length-1];
    
      //arbitrary value, probably should first check 
      //on the off chance that it is already in use
      var expected = 91929;
    
      styleSheet.addRule(selector, 'z-index: '+expected+' !important;', -1);
    
      var result = element.currentStyle.zIndex == expected;
    
      styleSheet.removeRule(styleSheet.rules.length-1);
    
      return result;
    }
    

    There's probably a handbag full of gotcha's with this method. Probably best to find some obscure proprietary CSS rule that is less likely to have a visual effect than z-index, but since it is removed almost immediately after it is set, a brief flicker should be the only side effect if that. Also a more obscure rule will be less likely to be overridden by a more specific selector, style attribute rules, or other !important rules (if IE even supports that). Anyway, worth a try at least.

    0 讨论(0)
  • 2020-11-27 06:41

    For best performance, use the browser implementations ((moz|webkit|o|ms)matchesSelector) where possible. When you can't do that, here is a manual implementation.

    An important case to consider is testing selectors for elements not attached to the document.

    Here's an approach that handles this situation. If it turns out the the element in question is not attached to the document, crawl up the tree to find the highest ancestor (the last non-null parentNode) and drop that into a DocumentFragment. Then from that DocumentFragment call querySelectorAll and see if the your element is in the resulting NodeList.

    Here is the code.

    The document

    Here's a document structure we'll be working with. We'll grab the .element and test whether it matches the selectors li and .container *.

    <!DOCTYPE html>
    <html>
      <body>
        <article class="container">
          <section>
            <h1>Header 1</h1>
            <ul>
              <li>one</li>
              <li>two</li>
              <li>three</li>
            </ul>
          </section>
          <section>
            <h1>Header 2</h1>
            <ul>
              <li>one</li>
              <li>two</li>
              <li class="element">three</li>
            </ul>
          </section>
          <footer>Footer</footer>
        </article>
      </body>
    </html>
    

    Searching with document.querySelectorAll

    Here is a matchesSelector function that uses document.querySelectorAll.

    // uses document.querySelectorAll
    function matchesSelector(selector, element) {
      var all = document.querySelectorAll(selector);
      for (var i = 0; i < all.length; i++) {
        if (all[i] === element) {
          return true;
        }
      }
      return false;
    }
    

    This works as long as that element is in the document.

    // this works because the element is in the document
    console.log("Part 1");
    var element = document.querySelector(".element");
    console.log(matchesSelector("li", element)); // true
    console.log(matchesSelector(".container *", element)); // true
    

    However, it fails if the element is removed from the document.

    // but they don't work if we remove the article from the document
    console.log("Part 2");
    var article = document.querySelector("article");
    article.parentNode.removeChild(article);
    console.log(matchesSelector("li", element)); // false
    console.log(matchesSelector(".container *", element)); // false
    

    Searching within a DocumentFragment

    The fix requires searching whatever subtree that element happens to be in. Here's an updated function named matchesSelector2.

    // uses a DocumentFragment if element is not attached to the document
    function matchesSelector2(selector, element) {
      if (document.contains(element)) {
        return matchesSelector(selector, element);
      }
      var node = element;
      var root = document.createDocumentFragment();
      while (node.parentNode) {
        node = node.parentNode;
      }
      root.appendChild(node);
      var all = root.querySelectorAll(selector);
      for (var i = 0; i < all.length; i++) {
        if (all[i] === element) {
          root.removeChild(node);
          return true;
        }
      }
      root.removeChild(node);
      return false;
    }
    

    Now we see that matchesSelector2 works even though the element is in a subtree that is detached from the document.

    // but they will work if we use matchesSelector2
    console.log("Part 3");
    console.log(matchesSelector2("li", element)); // true
    console.log(matchesSelector2(".container *", element)); // true
    

    You can see this working at jsfiddle.

    Putting it all together

    Here's the final implementation I came up with:

    function is(element, selector) {
      var node = element;
      var result = false;
      var root, frag;
    
      // crawl up the tree
      while (node.parentNode) {
        node = node.parentNode;
      }
    
      // root must be either a Document or a DocumentFragment
      if (node instanceof Document || node instanceof DocumentFragment) {
        root = node;
      } else {
        root = frag = document.createDocumentFragment();
        frag.appendChild(node);
      }
    
      // see if selector matches
      var matches = root.querySelectorAll(selector);
      for (var i = 0; i < matches.length; i++) {
        if (this === matches.item(i)) {
          result = true;
          break;
        }
      }
    
      // detach from DocumentFragment and return result
      while (frag && frag.firstChild) {
        frag.removeChild(frag.firstChild);
      }
      return result;
    }
    

    An important note is that jQuery's is implementation is much faster. The first optimization I would look into is avoiding crawling up the tree if we don't have to. To do this you could look at the right-most part of the selector and test whether this matches the element. However, beware that if the selector is actually multiple selectors separated by commas, then you'll have to test each one. At this point you're building a CSS selector parser, so you might as well use a library.

    0 讨论(0)
  • 2020-11-27 06:52

    I'm dealing with this issue now. I have to support IE8 with native Javascript, which presents a curious challenge: IE8 supports both querySelector and querySelectorAll, but not matchesSelector. If your situation is similar, here's an option for you to consider:

    When you're handed the DOM node and a selector, make a shallow copy of the node as well as its parent. This will preserve all of their attributes but won't make copies of their respective children.

    Attach the cloned node to the cloned parent. Use querySelector on the cloned parent -- the only thing it needs to search is the only child node it has so this process is constant time. It will either return the child node or it won't.

    That'd look something like this:

    function matchesSelector(node, selector)
    {
       var dummyNode = node.cloneNode(false);
       var dummyParent = node.parent.cloneNode(false);
       dummyParent.appendChild(dummyNode);
       return dummyNode === dummyParent.querySelector(selector);
    }
    

    It may be worth creating a complete chain of shallow-copied parents all the way up to the root node and querying the (mostly empty) dummy root if you'd like to be able to test your node's relationship to its ancestors.

    Off the top of my head I'm not sure what portion of selectors this would work for, but I think it'd do nicely for any that didn't worry about the tested node's children. YMMV.

    -- EDIT --

    I decided to write the function to shallow copy everything from the node being tested to root. Using this, a lot more selectors are employable. (Nothing related to siblings, though.)

    function clonedToRoot(node)
    {
        dummyNode = node.cloneNode(false);
        if(node.parentNode === document)
        {
            return {'root' : dummyNode, 'leaf' : dummyNode};
        }
        parent = clonedToRoot(node.parentNode).root;
        parent.appendChild(dummyNode);
        return {'root' : parent, 'leaf' : dummyNode};
    }
    
    function matchesSelector(node, selector)
    {
        testTree = clonedToRoot(node)
        return testTree.leaf === testTree.root.querySelector(selector)
    }
    

    I'd welcome an expert to explain what kinds of selectors there are that this wouldn't cover!

    0 讨论(0)
提交回复
热议问题