CSS selector to exclude all children where any par

2019-07-30 18:27发布

问题:

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

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



回答2:

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>