22

Suppose I have the following HTML:

<div class="foo">
  <ul>
    <li>One</li>
    <li>Two</li>
  </ul>
</div> <!-- not originally here -->
<div class="bar">
  <ul>
    <li>Three</li>
    <li>Four</li>
  </ul>
</div>

I want to select all li elements that are not descendants of an element with class foo. I know I can do it with a fancy filter function, but I'm wondering whether I can do it with just a selector. First I tried:

$(":not(.foo) li")

Unfortunately this doesn't work since the li has other ancestors without the style (the ul in this case). The following seems to work;

$(":not(.foo) :not(.foo) li")

In other words, select all li elements that have no ancestor that either has class foo or has an ancestor of its own with class foo. Perhaps this is the best/only way to do it with a selector, but I'm not thrilled about the repetition of the :not selector. Any better ideas out there?

fiddle

Community
  • 1
  • 1
Matthew Gertner
  • 4,487
  • 2
  • 32
  • 54
  • 2
    The problem here is that `:not(.foo) li` does not mean "`li` that has no ancestor that has class `foo`". It means "`li` that has an ancestor that does not have class `foo`". The difference seems subtle, but it's actually a very big difference. – BoltClock Dec 14 '12 at 15:25
  • Yeah exactly. And the second one means `li` that has an ancestor that does not have class `foo` AND doesn't have an ancestor of its own with class `foo`. Which I think is the right result, but is a pretty ugly and unsatisfying way to get there. – Matthew Gertner Dec 14 '12 at 16:52

5 Answers5

32

You can do it like this

$("li").not('.foo li')

http://jsfiddle.net/y7s54/

or

$("li:not(.foo li)")

http://jsfiddle.net/QpCYY/

Select all li's that don't have an ancestor with class foo

wirey00
  • 33,517
  • 7
  • 54
  • 65
4

Try li:not(.foo > ul > li); that selects all lis minus those with a parent .foo two levels up.

DNS
  • 37,249
  • 18
  • 95
  • 132
  • I'm not sure why OP is *not thrilled about the repetition of the `:not` selector*. What you've written is probably fine in JQuery/Sizzle, but according to the CSS3 spec, the `:not` selector can only contain [simple selectors](http://www.w3.org/TR/css3-selectors/#simple-selectors-dfn). – kojiro Dec 14 '12 at 15:15
  • @kojiro: Probably because repeating the `:not` selector in a way that is CSS3-compliant [doesn't actually work](http://stackoverflow.com/questions/7084112/css-negation-pseudo-class-not-for-parent-ancestor-elements/7084147#7084147). The only way to do this using `:not` that doesn't rely on the nature of the markup is by using it in a non-standard way that is provided by jQuery/Sizzle, as given in this answer. – BoltClock Dec 14 '12 at 15:17
  • @BoltClock It works just fine. It's just not what you might expect. – kojiro Dec 14 '12 at 15:22
  • This isn't flexible enough for me since in the real use case the `foo` class can occur many levels up in the hierarchy. It's a direct parent of `ul` in my example but it doesn't have to be, which is why I want to handle the case where an arbitrary ancestor has the class. – Matthew Gertner Dec 14 '12 at 16:55
2

You can do it using context along with selector as under,

$('li', $('div:not(.foo)'))

LIve Demo

$('li', $('div:not(.foo)')).each(function(){
    alert($(this).text());
});​
Adil
  • 146,340
  • 25
  • 209
  • 204
  • +1 – this avoids the weirdness of using `:not` with a non-simple selector and is likely more efficient than doing the lookup with pure CSS. – kojiro Dec 14 '12 at 15:16
  • 1
    Actually, it's not any more efficient unless you remove the inner quotes from `:not()` - having them there is not necessary, and is invalid CSS so it won't be any faster. See http://stackoverflow.com/questions/12475595/why-do-the-not-and-has-selectors-allow-quoted-arguments – BoltClock Dec 14 '12 at 15:19
  • This also fails if any of `div.foo`'s ancestors is `div:not(.foo)`. – BoltClock Dec 14 '12 at 15:19
0

How about this:

$("li:not(.foo > li)")

The comments in the docs are quite useful if this doesn't work:

http://api.jquery.com/not-selector/

matpol
  • 3,042
  • 1
  • 15
  • 18
  • That's the right basic idea, but doesn't work, since the lis aren't direct descendants of .foo. – DNS Dec 14 '12 at 15:13
0

I think this solution looks a little nicer, but there's a bit of controversy on the API comments about speed differences.

$("li").not(".foo li")

fiddle

Brigand
  • 84,529
  • 20
  • 165
  • 173