4

I have an unordered list exported by a CMS and need to identify <li> elements that have the class .sub and wrap them in a <ul>.

I have tried the wrapAll() method but that finds all <li class="sub"> elements and wraps them in one <ul>. I need it to maintain seperate groupings.

The exported code is as follows:

<ul>
  <li></li>
  <li></li>
  <li></li>
  <li class="sub"></li>
  <li class="sub"></li>
  <li class="sub"></li>
  <li></li>
  <li></li>
  <li class="sub"></li>
  <li class="sub"></li>
  <li class="sub"></li>
  <li></li>
 </ul>

I need it to be:

<ul>
  <li></li>
  <li></li>
  <li></li>
  <ul>
     <li class="sub"></li>
     <li class="sub"></li>
     <li class="sub"></li>
  </ul>
  <li></li>
  <li></li>
  <li></li>
  <ul>
     <li class="sub"></li>
     <li class="sub"></li>
     <li class="sub"></li>
   </ul>
   <li></li>
   <li></li>
</ul>

Any help would be greatly appreciated.

Rob W
  • 341,306
  • 83
  • 791
  • 678
Lucas JD
  • 121
  • 1
  • 1
  • 6

3 Answers3

7
  1. Use .each to walk through all .sub elements.
  2. Ignore elements whose parent has class wrapped, using hasClass()
  3. Use nextUntil(:not(.sub)) to select all consecutive sub elements (include itself using .andSelf()).
    The given first parameter means: Stop looking forward when the element does not match .sub.
  4. wrapAll

Demo: http://jsfiddle.net/8MVKu/

For completeness, I have wrapped the set of <li> elements in <li><ul>...</ul></li> instead of a plain <ul>.

Code:

$('.sub').each(function() {
   if ($(this.parentNode).hasClass('wrapped')) return;
   $(this).nextUntil(':not(.sub)').andSelf().wrapAll('<li><ul class="wrapped">');
});
$('ul.wrapped').removeClass('wrapped'); // Remove temporary dummy
Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
5

I would like to expand on Rob W's already awesome solution, providing a way to eliminate the temporary wrap class:

$(document).ready(function() {
    $('li.sub').filter(':not(li.sub + li.sub)').each(function() {
        $(this).nextUntil(':not(li.sub)').andSelf().wrapAll('<li><ul>');
    });
});

http://jsfiddle.net/m8yW3/

Edit: The filter isn't even needed:

$('li.sub:not(li.sub + li.sub)').each(function() {
    $(this).nextUntil(':not(li.sub)').andSelf().wrapAll('<li><ul>');
});
Dan A.
  • 2,914
  • 18
  • 23
  • Looks good. I don't think that the double loop using filter (filter loops through all elements, and tests them against the selector) is more efficient than a temporary class, though. – Rob W Jan 26 '12 at 18:32
  • Good point, Rob. I realized the filter wasn't even necessary, and changed my code in an edit down to a single selector. – Dan A. Jan 26 '12 at 19:48
  • @DanA. Could you please explain the :not(li.sub + li.sub) part? Your solution is clean and works great but I'm trying to understand what that part actually means. I can see that it prevents continued embedding of UL elements, but can't figure out how. Thank you. – user1447679 Mar 16 '15 at 01:50
  • @user1447679 `:not(li.sub + li.sub)` means it's not an `li.sub element` that immediately follows another `li.sub` element. So basically it starts out by only selecting the first `li.sub` in each grouping. – Dan A. Mar 18 '15 at 19:54
0

I believe what you should have is this jQuery:

$('li.sub').wrap('<li><ul>');

This will properly wrap your <li> elements in a new <ul> tag while wrapped them within <li> tags. The output of your example would then be:

<ul>
  <li></li>
  <li></li>
  <li></li>
  <li><ul><li class="sub"></li></ul></li>
  <li><ul><li class="sub"></li></ul></li>
  <li><ul><li class="sub"></li></ul></li>
  <li></li>
  <li></li>
  <li><ul><li class="sub"></li></ul></li>
  <li><ul><li class="sub"></li></ul></li>
  <li><ul><li class="sub"></li></ul></li>
  <li></li>
 </ul>
j08691
  • 204,283
  • 31
  • 260
  • 272