javascriptjquerynextuntil

jQuery nextUntil() works weird with comment element


I need help. I have weird problem with jQuery's nextUntil() method. It is not working as I am expecting...

I have two comment tags inside my HTML which delimit the elements I am interested in and which I want to get into a jQuery collection. For that I first find both comment nodes, and apply nextUntil on the first, providing as argument the second. But nextUntil returns all of the children that follow the first comment, seemingly ignoring the argument I have passed to it.

How can I prevent this from happening, and get only the children between the two comments?

Here is my code:

HTML:

<ul>
  <li>1</li>
  <li>2</li>

  <!-- from -->

  <li>3</li>
  <li>4</li>

  <!--  to  -->

  <li>5</li>
  <li>6</li>
</ul>

JavaScript:

  const fromSign = 'from'
  const toSign = 'to'

  // filter callback - return only comment
  // element with specific text
  function commentWith (elm, txt) {
    return elm.nodeType === 8 &&
           elm.nodeValue.trim() === txt
  }

  // Step - 1 find comment with "from"
  // text and save returned element
  const $start = $('ul').contents().filter(
    (idx, elm) => commentWith(elm, fromSign)
  )

  // Find comment with "to"
  // text and save returned element
  const $stop = $('ul').contents().filter(
    (idx, elm) => commentWith(elm, toSign)
  )

  console.info($start, $stop) // So far, so good

  // Step 2 - find all elements
  // between "from" and "to" comments
  let $inner = $start.nextUntil($stop)

  // Not works - inner contains elements
  // beyond "to" comment
  console.log($inner)

  // Step 2 again - find all elements
  // between "from" and "to" comments
  // with "redundant" prev/next
  $inner = $start.nextUntil($stop.prev().next())

  // OK, just li 3 and 4,
  // but why? What am I missing?
  console.log($inner)

PS: If you use <div class="from"> and <div class="end"> or other elements to select start and end, nextAll() works as expected. But, I want use comments for marking the selection....

I tried with prev/next "hacks" as alternative (see also code above), but they don't behave well when there are no elements preceding and/or following the comment markers.


Solution

  • The reason the first variant of your code fails is that most jQuery methods only look at elements, not nodes of other node types. As stated in the jQuery documentation on contents:

    Please note that most jQuery operations don't support text nodes and comment nodes. The few that do will have an explicit note on their API documentation page.

    In your case the call to nextUntil will step through elements only, not even looking at other nodes, and so it will "miss" the $stop node.

    A workaround is to use index(). This will also work well when there are no elements before the start mark, and/or no elements after the stop mark. The index returned for a non-element node will be the index it would have if it were an element node.

    const fromSign = 'from'
    const toSign = 'to'
    
    // filter callback - return only comment
    // element with specific text
    function commentWith (elm, txt) {
        return elm.nodeType === 8 &&
               elm.nodeValue.trim() === txt
    }
    
    // Step - 1 find comment with "from"
    // text and save returned index
    const startIndex = $('ul').contents().filter(
        (idx, elm) => commentWith(elm, fromSign)
    ).index();
    
    // Find comment with "to"
    // text and save returned index
    const stopIndex = $('ul').contents().filter(
        (idx, elm) => commentWith(elm, toSign)
    ).index();
    
    console.log('indexes:', startIndex, stopIndex);
    
    // Step 2 - get all child elements
    // between "from" and "to" comments
    let $inner = $('ul').children().slice(startIndex, stopIndex);
    
    // Now it works - inner contains the elements between the markings
    console.log('selected first and last text:', $inner.first().text(), $inner.last().text());
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <ul>
        <li>1</li>
        <li>2</li>
    
        <!-- from -->
    
        <li>3</li>
        <li>4</li>
    
        <!--  to  -->
    
        <li>5</li>
        <li>6</li>
    </ul>