2

What

I am trying to create a CSS selector which selects all children within a given parent; but excludes them as long as any element on the path has a certain class.

Context

I am creating some materialisation class in Javascript which replaces some elements into their material versions. This runs on a top-level app. Each user can create their own apps, and I want to be able to say that a certain group of elements should not go through this process.

Example

This should be selected:

<div>
  <input />
</div>

This should not be selected:

<div class="no-material">
  <input />
</div>

The main challenge is that this label can be at any place. Example:

  <main>
    <section class="no-material">
      <form>
        <fieldset>
          <input />
        </fieldset>
      </form>
    </section>
  </main>

Or it could be:

  <main>
    <section>
      <form class="no-material">
        <fieldset>
          <input />
        </fieldset>
      </form>
    </section>
  </main>

Already tested

I tried a few attempts. The best scenario was:

div:not(.no-material) > input:not(.no-material), div:not(.no-material) *:not(.no-material) input:not(.no-material)

However, it stills gives some false positives. I could get more accurate by adding a lot of levels like:

div:not(.no-material) > input:not(.no-material),
div:not(.no-material) > *:not(.no-material) > input:not(.no-material),
div:not(.no-material) > *:not(.no-material) > *:not(.no-material) > input:not(.no-material)

And like that for 20-50 levels (or more?), but that's not very smart.

Live version

You can test your selectors by editing cssSelector in Javascript.

let cssSelector = [
  // Independent selectors
  'div:not(.no-material) > input:not(.no-material)',
  'div:not(.no-material) *:not(.no-material) input:not(.no-material)'
].join(',');

// This will get elements and run their names. We should get yes1-5, but not no1-5.
let inputs = document.querySelectorAll(cssSelector);
for (let input of inputs) console.log(input.getAttribute('name'));
<!-- Do not edit HTML, just the CSS selector -->

<main style="display: none;">

  <!-- Not selectable -->
  <div class="no-material">
    <input name="no-1">
  </div>

  <div>
    <input name="no-2" class="no-material">
  </div>

  <div>
    <label class="no-material">
      <input name="no-3">
    </label>
  </div>

  <div>
    <label class="no-material">
      <span>
        <input name="no-4">
      </span>
    </label>
  </div>

  <div>
    <label>
      <span class="no-material">
        <input name="no-5">
      </span>
    </label>
  </div>

  <!-- Selectable -->
  <div>
    <input name="yes-1">
  </div>

  <div>
    <input name="yes-2">
  </div>

  <div>
    <label>
      <input name="yes-3">
    </label>
  </div>

  <div>
    <label>
      <span>
        <input name="yes-4">
      </span>
    </label>
  </div>

  <div>
    <label>
      <span>
        <input name="yes-5">
      </span>
    </label>
  </div>

</main>
<!-- Do not edit HTML, just the CSS selector -->

Note: I already have thought of other ways of solving this like iterating all the children of an element called '.no-material' and add the class 'no-material' to all, but that is resource consuming and I want to solve this from a CSS selector standpoint if possible.

Thank you

Asons
  • 84,923
  • 12
  • 110
  • 165
nitobuendia
  • 1,228
  • 7
  • 18
  • regardless, if the CSS selection process doesn't have any indexing (not sure of the internals for it), you'll end up iterating all the elements anyhow in one form or another. You can iterate all items with classes via a `$("[class]")` selector (that's a literal), and then if they have a parent with a class you'd filter. – Rogue Jun 12 '17 at 08:23
  • For starters don't use `>` (direct child) when you want to match children at any level. – pawel Jun 12 '17 at 09:45
  • This is a duplicate of a solved issue: Global selector combined with :not(.class) doesn't seem to work https://stackoverflow.com/a/44497536/7973110 – Jennifer Goncalves Jun 12 '17 at 11:08
  • They asked 1h ago and I did 3h ago :) but, yeah, it seems the same question, to the point where I see the same span.no-material with an input inside (which is a very poor structure). @JenniferGoncalves – nitobuendia Jun 12 '17 at 11:43
  • @pawel If you see my best selector, it does not contain > on the second piece: `div:not(.no-material) *:not(.no-material) input:not(.no-material)` -- still, it does not work. Do you want to make any suggestions on how to solve the issue? – nitobuendia Jun 12 '17 at 11:45
  • @fvbuendia I wanted to help here, though it obviously came out wrong. The second part in my answer clearly referenced that question, and as it only was a part of the answer I gave, I found that okay, still, I now deleted my answer here – Asons Jun 12 '17 at 12:03
  • 1
    @fvbuendia Upvoted this and also gave credit in the other question to yours. – Asons Jun 12 '17 at 12:49

3 Answers3

1

Find all the elements (all), then the elements with no-material on the element or its parent (no), then remove those in the second from those in the first to find those that remain (yes).

const difference = (a, b) => a.filter(elt => b.indexOf(elt) === -1);
  
const all = document.querySelectorAll("input");
const no = document.querySelectorAll(".no-material input, input.no-material");

const yes = difference([...all], [...no]);

console.log(yes.map(elt => elt.name));
<main style="display: none;">

  <!-- Not selectable -->
  <div class="no-material">
    <input name="no-1">
  </div>

  <div>
    <input name="no-2" class="no-material">
  </div>

  <div>
    <label class="no-material">
      <input name="no-3">
    </label>
  </div>

  <div>
    <label class="no-material">
      <span>
        <input name="no-4">
      </span>
    </label>
  </div>

  <div>
    <label>
      <span class="no-material">
        <input name="no-5">
      </span>
    </label>
  </div>

  <!-- Selectable -->
  <div>
    <input name="yes-1">
  </div>

  <div>
    <input name="yes-2">
  </div>

  <div>
    <label>
      <input name="yes-3">
    </label>
  </div>

  <div>
    <label>
      <span>
        <input name="yes-4">
      </span>
    </label>
  </div>

  <div>
    <label>
      <span>
        <input name="yes-5">
      </span>
    </label>
  </div>

</main>
  • This is actually for Javascript; but I wanted to solve it a querySelectorAll level, yes. I do not see how your case solves the issue. I am trying to add all input, except the ones whose parents (at any level) has .no-material, this is selecting the inputs whose parents has .no-material, it seems trivial, but it's quite the opposite which is hard :) – nitobuendia Jun 12 '17 at 11:40
  • Lacking a better answer, I marked this one as answered. I took a similar approach using Set() and an iterator, but with the same concepts behind. – nitobuendia Jun 17 '17 at 06:41
0

In modern browsers, you can use css variables.

Define it at root level, redefine it in your class:

:root {
    --mycolor: lightblue;
}

.container {
    --mycolor: lightgreen;
}

.test {
    background-color: var(--mycolor);
}
<div class="test">BASE</div>

<div class="container">
    <div class="test">BASE</div>
</div>
vals
  • 61,425
  • 11
  • 89
  • 138
  • Thank you. This is nice, but it would not work for the purpose here. The CSS selector is used in Javascript to apply certain classes and behaviours to the elements. – nitobuendia Jun 12 '17 at 11:38
0
:not(.no-material, .no-material *)

This will select all elements except those that have the class "no-material" or are a children of an element that has the class "no-material"

The key is using a space instead of the > selector, see https://stackoverflow.com/a/2636396/10002734 for the differences between them.

If you want to only select the elements you have marked as "yes" in the example you could also combine this with the :empty pseudo-selector, like so:

:not(.no-material, .no-material *):empty
David Neil
  • 61
  • 3