views:

197

answers:

4

This is a challenge question / problem. Hope you find it interesing.

Scenario: You have a very long list (unreasonably long) in a single column. It would be much better displayed in multiple shorter columns. Using jQuery or another tool, what do you do?

The format of the list is as follows:

<div class="toc">
 <dl>
  <dt>item 1</dt>
  <dd>related to 1</dd>
  <dt>item 2</dt>
  <dd>related to 2</dd>
  <dt>item 3</dt>
  <dd>related to 3</dd>
  <dt>item 4</dt>
  <dd>related to 4</dd>
  <dt>item 5</dt>
  <dd>related to 5</dd>
  <dt>item 6</dt>
  <dd>related to 6</dd>
  <dt>item 7</dt>
  <dd>related to 7</dd>
  <dt>item 8</dt>
  <dd>related to 8</dd>
  <dt>item 9</dt>
  <dd>related to 9</dd>
  <dt>item 10</dt>
  <dd>related to 10</dd>
 </dl>
</div>

Caveat: The dd's may contain nested dl's, dt's, & dd's.

Also be sure to keep related items in the same column (ie. if dt 7 is col x, so should dd 7).

This problem inspired by the somewhat ridiculously laid out Zend Framework manual.

Edit: See below for answer.

+1  A: 

i would do a count of the array of $("dt") then if it's over a certain size inject a closing and opening then using styling to float them into columns.

Josh

Josh
closing then opening tags won't work, but that was my initial approach too.
Keith Bentrup
A: 

I would first write the complete data in a json then check the amount of dt's + dd's + related children amount by considering this sum as total row amount.

Then i would check the predefined row amount per column and divide this json into proper parts.

And finally from this parts i'd create columns without breaking related items.

Sinan.

Sinan Y.
i'm curious about your answer. can you flush it out a bit more? what exactly are you proposing? maybe some pseudo code?
Keith Bentrup
+1  A: 
cic
good idea! however, i can see that it's not maintaining the relatedness condition. from the challenge, if a top level dt is in one column, the entire immediate sibling dd needs to be in that same column - the reason being that we're going to hide the the dd's and toggle them on off by click the dt.
Keith Bentrup
hmm ... i was just thinking if we end up hiding the related sibling dd's. the browser probably wouldn't wrap those elements into the next column b/c they wouldn't be displayed. there's a possibility here ...
Keith Bentrup
A: 

Answer So this problem is more difficult than it first appears.

My initial thought was that I would wrap the column in <table><tr><td></td></tr></table> and then after every nth parent dd output a </td><td>. Simple, right? Except you can't really output a closing tag like that. Ultimately when you write $('</td><td>').after('.toc > dl > dd'), you'll be creating nodes - that's nodes with opening and closing tags. Since you must create nodes, the browser will ignore the first closing tag.

Well, let's say that you solve that problem somehow. What are you're iteration conditions? My first attempt was to construct a for loop. It seems reasonable. For every nth parent dd, do whatever you need to do. However, how do you construct those conditions in jQuery? You have less than, greater than, and equal. But you don't have greater than or equal to (>=) or less than or equal to (<=), and this is a critical distinction.

You might try something like this:

for (i = 0; i < len; i+=40) { // where 40 determines the # per col
  get elements i thru i+40;
  wrap this set and float left  
}

So how would you do this in jQuery?

// note that you must use > to prevent nested descendants from being selected
var len = jQuery('.toc > dl > dd').size()
for (i = 0; i < len; i+=40) {
  // this selector says give me the first 40 child elements 
  // whatever type of element they may be
  $('.toc > dl > *:gt('+i+'):lt('+(i+40)').wrapAll('<div style="float:left">');

  // however because we don't have >= and :gt won't accept negatives as an input
  // we must either do a special case for the first iteration 
  // or construct a different selector
  $('.toc > dl > *:eq('+i+')', ' + 
    '.toc > dl > *:gt('+i+'):lt('+(i+40)')
        .wrapAll('<div style="float:left">');
}

You could also do something with jQuery's add() method to add the first element of each iteration to your set, but you must maintain document order in your selection or jQuery will rearrange the set, so you have to do that first.

Ultimately, the for loop made sense initially, but it ran into problems with challenging selectors. Of course, we're not using the $('selector').each(function () { }); construct because that would only be useful if we could output independent closing tags.

So what did I end up with? Final Answer:

$('.toc').after('<div id="new"></div>');
do {
    var curSet = $('.toc > dl > *:lt(40)')
        .appendTo('#new').wrapAll('<div style="float:left"></div>');
} while (curSet.size());

This approach appends a new div after the old one. Then iteratively grabs the first 40 elements from the old and appends them to the new div after wrapping them in a div that will float left, looping as long as there are elements left to grab, and it maintains order.

Not terribly complicated after you figure it out, but there were a few gotcha's throughout the problem that I hope you find interesting. I did.

To finish up the ultimate goal of making the documentation significantly more useful:

I added some style and used the dt's as togglers to show the dd's. I also used a simple php proxy wrapper (5-10 LOC) so I could bring in any given, desired doc page thru an ajax call without remote ajax warnings.

I ended up with a nice little document in a single, navigable page that loads in < 2 secs (and uses ajax to load all subsequent pages in < 1 sec) rather than a monstrous page that takes 15-20 sec to load per page!

Problem solved. Something much more enjoyable and useful in 10-15 lines of javascript (total with the reorganizing, toggling, and ajax code), < 10 lines of PHP, and a few style rules.

No more slow Zend docs and endless scrolling.

Keith Bentrup