Tom
Tom

Reputation: 21

How to use a CSS selector that gets all matching elements except those inside a specific child?

Assume an HTML structure as shown:

<div id="container">
     <div class="A">
         <div id="excludedElement">
             <p>
                 <span class="MyClass">1</span>
                 <span class="MyClass">2</span>
                 <span class="MyClass">3</span>
             </p>
         </div>
     </div>
     <div class="B">
         <p>
             <span class="MyClass">4</span>
             <span class="MyClass">5</span>
             <span class="MyClass">6</span>
         </p>
     </div>
</div>

I want all elements inside of the "container" div that have the class "MyClass" except for those inside of the "excludedElement" div. In this case, the result contains only the spans 4, 5, and 6.

My current solution is to first get all elements with "MyClass", then get all elements inside of excludedElement with "MyClass". For each element in the first list, we check if it's in the second, and skip over it if so. This is O(n^2) running time, so I'd like to avoid this. Psuedocode for reference:

const allElements = container.querySelectorAll('.MyClass');
const excludedElements = container.querySelectorAll('#excludedElement .MyClass');
var result = [];

for (const element in allElements)
{
    if (!excludedElements.Contains(element))
    {
        result.Add(element);
    }
}

Is there a way to craft a CSS selector in querySelectorAll() that can retrieve this particular set of elements?

One way is to temporarily remove excludedElement from the tree, query for "MyClass", then replace the excludedElement, but I want to avoid modifying the DOM.

Upvotes: 1

Views: 1200

Answers (4)

Warren Halderman
Warren Halderman

Reputation: 349

If the structure is predictable and already known:

container.querySelectorAll('div:not(#excludedElement) > p .MyClass');

If the structure is not known and you're okay with adding classes in order to avoid O(n^2):

container.querySelectorAll('#excludedElement .MyClass')
    .forEach(element => element.classList.add('excluded'));

const notExcludedMyClassElements = container.querySelectorAll('.MyClass:not(.excluded)');

Upvotes: 2

Rounin
Rounin

Reputation: 29463

You can use this precise selector in .querySelectorAll():

:not(#excludedElement) > p > .MyClass

Working Example:

const includedSpans = [... document.querySelectorAll(':not(#excludedElement) > p > .MyClass')];

includedSpans.forEach((includedSpan) => console.log(includedSpan.textContent));
<div id="container">
     <div class="A">
         <div id="excludedElement">
             <p>
                 <span class="MyClass">1</span>
                 <span class="MyClass">2</span>
                 <span class="MyClass">3</span>
             </p>
         </div>
     </div>
     <div class="B">
         <p>
             <span class="MyClass">4</span>
             <span class="MyClass">5</span>
             <span class="MyClass">6</span>
         </p>
     </div>
</div>

Upvotes: 0

Mister Jojo
Mister Jojo

Reputation: 22320

I have this....

const  Excludes   = [...container.querySelectorAll('#excludedElement .MyClass')]
  ,    noExcludes = [...container.querySelectorAll('.MyClass')].filter(el=>(!Excludes.includes(el)))
  ;
noExcludes.forEach(element => element.style.backgroundColor = 'lightgreen');
<div id="container">
  <div class="A">
    <div id="excludedElement">
      <p>
        <span class="MyClass">1</span>
        <span class="MyClass">2</span>
        <span class="MyClass">3</span>
      </p>
    </div>
  </div>
  <div class="B">
    <p>
      <span class="MyClass">4</span>
      <span class="MyClass">5</span>
      <span class="MyClass">6</span>
    </p>
  </div>
</div>

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 370759

You can select all .MyClass descendants, then .filter the collection by whether the current item being iterated over has a #excludedElement ancestor with .closest:

const classes = [...container.querySelectorAll('.MyClass')]
  .filter(span => !span.closest('#excludedElement'));
for (const span of classes) {
  span.style.backgroundColor = 'yellow';
}
<div id="container">
     <div class="A">
         <div id="excludedElement">
             <p>
                 <span class="MyClass">1</span>
                 <span class="MyClass">2</span>
                 <span class="MyClass">3</span>
             </p>
         </div>
     </div>
     <div class="B">
         <p>
             <span class="MyClass">4</span>
             <span class="MyClass">5</span>
             <span class="MyClass">6</span>
         </p>
     </div>
</div>

Unless you know in advance the exact sort of structure of the descendants of #container, I don't think there's an elegant way to do this with a single query string; :not accepts simple selectors only.

Just for informational purposes, a silly and repetitive method that you shouldn't use would be to use the query string:

:scope > .MyClass,
:scope > *:not(#excludedElement) > .MyClass,
:scope > *:not(#excludedElement) > *:not(#excludedElement) > .MyClass
...

const selector = `
:scope > .MyClass,
:scope > *:not(#excludedElement) > .MyClass,
:scope > *:not(#excludedElement) > *:not(#excludedElement) > .MyClass
`;

const classes = container.querySelectorAll(selector);
for (const span of classes) {
  span.style.backgroundColor = 'yellow';
}
<div id="container">
     <div class="A">
         <div id="excludedElement">
             <p>
                 <span class="MyClass">1</span>
                 <span class="MyClass">2</span>
                 <span class="MyClass">3</span>
             </p>
         </div>
     </div>
     <div class="B">
         <p>
             <span class="MyClass">4</span>
             <span class="MyClass">5</span>
             <span class="MyClass">6</span>
         </p>
     </div>
</div>

Upvotes: 1

Related Questions