Home > Back-end >  jquery `.find()` use with `:not` selector is missing some elements (the selector is affected by the
jquery `.find()` use with `:not` selector is missing some elements (the selector is affected by the

Time:01-08

problem

I have an element $(elt_Main) (<span id="a">)

I call $(elt_Main).find(selector); on it

With a selector selector = ':not(span span)';

But jquery is not giving out all the child elements I want.

ex [test1.html]

  • say the content in html is::

    <body>
    <span id="a">The first <span >signature <span >for the .find() <span >method accepts</span> a selector</span> <strong>expression</strong> of the <span >same type that we can pass to the</span> $() function</span>. The <span >elements</span> will be filtered by testing whether they match this selector; all parts of the selector <strong>must lie inside of an element</strong> on which .find() is called.</span>
    </body>
    
  • the javascript is::

      let elt_Main = document.getElementById('a');
      // ...
      html_ResultAppend  = hr_3;
      selector = ':not(span span)';         // <<< watch
      jqElt = $(elt_Main).find(selector);   // <<< watch
    
  • the js that provides output for debug is::

      for (let o of jqElt) {
        html_ResultAppend  = hr_4;
        html_ResultAppend  = o.outerHTML;
      }  
    
  • the result of jqElt will be::

    <strong>expression</strong>
    <strong>must lie inside of an element</strong>
    
  • but the expecting result should be::

    <span >signature <span >for the .find() <span >method accepts</span> a selector</span> <strong>expression</strong> of the <span >same type that we can pass to the</span> $() function</span>
    <span >elements</span>
    <strong>expression</strong>
    <strong>must lie inside of an element</strong>
    
  • the pb & the cause is::

    these 2 elements are missing

    <span >signature <span >for the .find() <span >method accepts</span> a selector</span> <strong>expression</strong> of the <span >same type that we can pass to the</span> $() function</span>
    <span >elements</span>
    
  • (I think) the logic of jquery is that (which is wrong)::

    <span id="a"> <span >signature ...

    -> <span >signature ... is inside a span <span id="a">

    -> we said :not(span span)

    -> so this should not be selected

    (^ same as above for <span id="a"> <span >elements</span>)

  • my logic is that::

    <span id="a"> ($(elt_Main)) is the element that invoke .find()

    <span id="a"> ($(elt_Main)) itself should never be taken into account / play any effect when we use css selectors inside .find()

    • otherwise, it wont make any sense in following examples::

      ex, say we do this: $(elt_Main).find('span');

      this should only find child element of elt_Main that is a span -- this is correct.

      if we take <span id="a"> ($(elt_Main)) into account when use use selector, this will select the <span id="a"> ($(elt_Main)) itself -- this is wrong.

    • in another words --

      when .find() is invoked by <span id="a"> ($(elt_Main)),

      the selector should treat this (the element <span id="a"> ($(elt_Main)) to search on) as if the tag <span id="a"> doesnt exist (-- it should not play any effect), and only search on the innerHtml.

to prove my logic -- example [test1.html test2.html]

  • plunker code image compare

  • test1 uses .find() :not()

    test2 uses only :not()

    test2 removed the <span id="a"> tag in $(elt_Main)

    • (-- just as I said above -- we should treat it as if the tag <span id="a"> doesnt exist)

    • (if you think Im wrong on this, and you say: do not remove the <span id="a"> makes test1 & test2 produce same result -> check out test1 vs test3 -- its even worse)

  • we clearly see that

    test1 output is missing elements

    test2 output is correct

(back to the question)

"jquery .find() use with :not selector is missing some elements (the selector is affected by the .find() invoking element, but it shouldnt)"

So, Is jQuery wrong? or am I wrong? Why?

CodePudding user response:

Your selector asks for any element that is not a <span> descendant of any other <span>. The descendant test does not apply only to the subtree beneath the starting point in the DOM (the element where the .find() is rooted); it involves the entire DOM. It has to work that way in general. Your claim that "it shouldn't" is a misunderstanding of how selectors work.

Because your starting element is itself a <span>, all the <span> elements in the subtree are descendants of a <span>, so none of them match the selector.

You can try your test with the native .querySelectorAll():

let elt_Main = document.getElementById("a");
let targets = eltMain.querySelectorAll(":not(span span)");

and see what you get.

Here is a clearer example, using a :not() selector that involves a parent element of the starting point:

const start = document.getElementById("start");
let found = start.querySelectorAll(":not(.foo span)");
console.log("Found "   found.length   " elements");
<div >
  <span id="start">
    <span>Hello world</span> <b>a bold tag</b> <span>goodbye world</span>
  </span>
</div>

Starting from the outer span, that selector (:not(.foo span)) asks for any element that is not a <span> that descends from an element with class "foo". All the <span> tags beneath the starting point do match that selector in the :not because they all descend from the parent <div>. Thus the query finds only one element, the <b>.

As a general rule, finding a profound bug in a platform that's been in use for almost 20 years is fairly unlikely. Not impossible, but unlikely.

edit — It's pointed out in a comment that the simpler case of the selector "span span", that is, a positive search for any <span> that is a descendant of a <span> results in different behavior between jQuery's selector engine and the browser's native querySelectorAll():

$(function() {
  console.log("jQuery finds "   $("#start").find(".foo span").length   " spans that descend from .foo");
  console.log("browser finds "   document.getElementById("start").querySelectorAll(".foo span").length   " spans that descend from .foo");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class=foo>
  <span id=start>
    <span>Hello world</span> <b>bold</b> <span>goodbye world</span>
  </span>
</div>

I find this very surprising. It should be noted that jQuery predates the introduction of the native DOM search methods by many years, so while this could be considered wrong (I certainly do) it must be understood that there may be millions of applications that depend on the old behavior, so changing it is probably not an option for the jQuery maintainers.

CodePudding user response:

Okay I have a speculation here so I'm just going to post it as an answer.

As I've written in my comments, essentially $(selector).find(selector) works like a two pass filter. It is not supposed to pose any sandbox-ish mechanism that blocks the the second pass of selection from "detecting" any fact / property of the candidates, including but not limited to their parent.

HOWEVER, what I suspect about $(A).find(A B) is this does NOT (but IMO should) work behind the scene like this:

  1. select all A's descendants
  2. select those from the selection returned by step 1 that are B and have an A ancestor (something like foreach c in S: if c is B and c.hasAncestor(A): nS.add(c))

INSTEAD, it probably work behind the scene like this:

  1. select all A's descendants
  2. select those from the selection returned by step 1 that are A
  3. select the descendants of the selection returned by step 2 that are B

which is probably equivalent to:

  1. select the descendants of A (pass 1) that are themselves A (pass 2)
  2. select the descendants of the selection returned by step 1 that are B (pass 3)

As you can see, the logic is twisted in the (speculated) approach behind the scene. What you get became a "three pass filter".

Here's an example which might give you a clearer idea on what I mean:

const a = $("span").find("span span");
console.log(a.length);
console.log(a.get(0).textContent);

const b = $("span").find("span").find("span");
console.log(b.length);
console.log(b.get(0).textContent);
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<span>A<span>B<span>C</span></span></span>

In other words, what $("span").find("span span") should (IMO) do is:

Among the descendants of "span" selection, select spans that are inside a span

But what it (seem to) really does:

Among the descendants of "span" selection, select spans inside those that are spans

As noted in the other answer, when :not() is involved, such problem does not occur. I suspect (again) that it is because the negation makes it harder to look right when you twist the logic as I stated.

I don't consider the current way that the descendant selector is implemented correct. I do wonder how the jQuery devs would argue about it though. (But this is really just my speculation. I haven't check any source code or so to make sure it is 100% the case.)

  • Related