adamwulf/Columnizer-jQuery-Plugin — Issue #32: Code is poorly commented
Author: chrisgraham
Created: 2012-01-20 02:32:50+00:00
State: open

Algorithmically this code is very complex, but nothing really explains the algorithm. It took me a few hours to work it all out to fix the nasty bugs I had.

The API header comment for 'columnize' is completely wrong. It refers to jQuery and DOM nodes, but it's the other way around. It does not return as it says. Other parameters are not mentioned there. The description isn't helpful either.

The first while loop in 'columnize' really confused me. It relies on a non-documented jQuery quirk that an 'append' will move a node. Probably some jQuery programmers know this automatically, but it is not in the jQuery docs for the append method, and it's quite a magical thing to do given it is happening via a DOM reference not a jQuery reference.
So I'd add a comment such as:
"Because we're not cloning, jquery will actually move the element"

Then later on the maxLoops stuff is very confusing. I added this comment:
"We loop as we try and workout a good height to use. We know it initially as an average but if the last column is higher than the first ones (which can happen, depending on split points) we need to raise 'adjustment'. We try this over a few iterations until we're 'solid'."

I actually made various cleanups. My full changes (including bug fixes, and simplification for my purposes are as follows)...

```
// Based on http://welcome.totheinter.net/columnizer-jquery-plugin/
// But with fixes and better flexibility, and pure CSS-based activation

addEventListenerAbstract(window,'load',function () {
$('.column_wrapper').columnize({ columns: 3 });
$('.column_wrapper_2').columnize({ columns: 2 });
} );

// version 1.5.0
// http://welcome.totheinter.net/columnizer-jquery-plugin/
// created by: Adam Wulf @adamwulf, adam.wulf@gmail.com

(function($){

$.fn.columnize = function(options) {

var defaults = {
// optional # of columns instead of width
columns : false,
// true to build columns once regardless of window resize
// false to rebuild when content box changes bounds
buildOnce : false,
// an object with options if the text should overflow
// it's container if it can't fit within a specified height
overflow : false,
// this function is called after content is columnized
doneFunc : function(){},
// if the content should be columnized into a
// container node other than it's own node
target : false,
// re-columnizing when images reload might make things
// run slow. so flip this to true if it's causing delays
ignoreImageLoading : true,
// should columns float left or right
columnFloat : "left",
// ensure the last column is never the tallest column
lastNeverTallest : false,
// (int) the minimum number of characters to jump when splitting
// text nodes. smaller numbers will result in higher accuracy
// column widths, but will take slightly longer
accuracy : false
};
var options = $.extend(defaults, options);

$(this).find('h1, h2, h3, h4, h5, h6').addClass('dontend');

return this.each(function() {
var $inBox = options.target ? $(options.target) : $(this);
var maxHeight = $(this).height();
var $cache = $('

'); // this is where we'll put the real content
var lastWidth = 0;
var columnizing = false;

var adjustment = 0;

$cache.append($(this).contents().clone(true));

// images loading after dom load
// can screw up the column heights,
// so recolumnize after images load
if(!options.ignoreImageLoading && !options.target){
if(!$inBox.data("imageLoaded")){
$inBox.data("imageLoaded", true);
if($(this).find("img").length > 0){
// only bother if there are
// actually images...
var func = function($inBox,$cache){ return function(){
if(!$inBox.data("firstImageLoaded")){
$inBox.data("firstImageLoaded", "true");
$inBox.empty().append($cache.children().clone(true));
$inBox.columnize(options);
}
}}($(this), $cache);
$(this).find("img").one("load", func);
$(this).find("img").one("abort", func);
return;
}
}
}

$inBox.empty();

columnizeIt();

if(!options.buildOnce){
$(window).resize(function() {
if(!options.buildOnce && $.browser.msie){
if($inBox.data("timeout")){
clearTimeout($inBox.data("timeout"));
}
$inBox.data("timeout", setTimeout(columnizeIt, 200));
}else if(!options.buildOnce){
columnizeIt();
}else{
// don't rebuild
}
});
}

/**
* Create a node that has a height
* less than or equal to height.
* Returns a boolean on whether we did some splitting successfully at a text point (so we know we don't need to split a real element).
*
* @param putInHere, a jQuery element
* @$pullOutHere, a dom element
*/
function columnize($putInHere, $pullOutHere, $parentColumn, height){
while($parentColumn.height() < height && $pullOutHere[0].childNodes.length){
$putInHere.append($pullOutHere[0].childNodes[0]); // Because we're not cloning, jquery will actually move the element
}
if($putInHere[0].childNodes.length == 0) return false;

// now we're too tall, undo the last one
var kids = $putInHere[0].childNodes;
var lastKid = kids[kids.length-1];
$putInHere[0].removeChild(lastKid);
var $item = $(lastKid);

// and now try and put a split version of it
if($item[0].nodeType == 3){
// it's a text node, split it up
var oText = $item[0].nodeValue;

var counter2 = $putInHere.width() / 18;
if(options.accuracy)
counter2 = options.accuracy;
var columnText;
var latestTextNode = null;
while($parentColumn.height() < height && oText.length){
var pos = oText.indexOf(' ', counter2);
if (pos == 0) pos = oText.substring(1).indexOf(' ', counter2)+1;
if (pos != -1) {
columnText = oText.substring(0, pos);
} else {
columnText = oText;
}
latestTextNode = document.createTextNode(columnText);
$putInHere.append(latestTextNode);

if((oText.length > counter2) && (pos)) {
oText = oText.substring(pos);
}else{
oText = "";
}
}
if($parentColumn.height() >= height && latestTextNode != null){
// too tall :(
$putInHere[0].removeChild(latestTextNode);
oText = latestTextNode.nodeValue + oText;
}
if(oText.length){
$item[0].nodeValue = oText;
}else{
return false; // we ate the whole text node, move on to the next node
}
}

// Put what is left back
if($pullOutHere.children().length){
$pullOutHere.prepend($item);
}else{
$pullOutHere.append($item);
}

return $item[0].nodeType == 3;
}

// Split up an element, which is more complex than splitting text. We need to create two copies of the element with it's contents divided between each
function split($putInHere, $pullOutHere, $parentColumn, height){
if($pullOutHere.children().length){
var $cloneMe = $pullOutHere.children(":first"); // From
var $clone = $cloneMe.clone(true); // To
if($clone.prop("nodeType") == 1 && !$clone.hasClass("dontend")){
$putInHere.append($clone);
var dontsplit=$cloneMe.hasClass("dontsplit") || $clone.is("thead") || $clone.is("table") || $clone.is("tbody") || $clone.is("tr") || $clone.is("th") || $clone.is("td") || $clone.is("li");
if($clone.is("img") && $parentColumn.height() < height + 20){ // Images are easy to handle, just shift them
$cloneMe.remove();
}else if(!dontsplit && $parentColumn.height() < height + 20){ // If this is a viable split point, do it
$cloneMe.remove(); // Remove from from
}else if($clone.is("img") || dontsplit){ // Can't split, we'll just have to let it all stay where it is
$clone.remove(); // Remove from to (i.e. undo copy). Stays at from.
}else{ // Look deeper for split point
$clone.empty();
if(!columnize($clone, $cloneMe, $parentColumn, height)){
if($cloneMe.children().length){
split($clone, $cloneMe, $parentColumn, height);
}
}
if($clone.get(0).childNodes.length == 0){
// it was split, but nothing is in it :(. No deeper to go.
$clone.remove();
}
}
}
}
}

function checkDontEndColumnOnConstraints(dom){
if(dom.nodeType != 1) return false;
if($(dom).hasClass("dontend")) return true;
if(dom.childNodes.length == 0) return false;
return checkDontEndColumnOnConstraints(dom.childNodes[dom.childNodes.length-1]);
}

function columnizeIt() {
if(lastWidth == $inBox.width()) return;
lastWidth = $inBox.width();

var numCols = options.columns;

if($inBox.data("columnizing")) return;
$inBox.data("columnized", true);
$inBox.data("columnizing", true);

$inBox.empty();
$inBox.append($("

")); //"
$col = $inBox.children(":last");
$col.append($cache.clone());
maxHeight = $col.height();
$inBox.empty();

var targetHeight = maxHeight / numCols;
var firstTime = true;
var maxLoops = 3;
var scrollHorizontally = false;
if(options.overflow){
maxLoops = 1;
targetHeight = options.overflow.height;
}else if(options.height){
maxLoops = 1;
targetHeight = options.height;
scrollHorizontally = true;
}

for(var loopCount=0;loopCount

")); //"
}

// fill all but the last column (unless overflowing)
var i = 0;
while(i < numCols - (options.overflow ? 0 : 1) || scrollHorizontally && $destroyable.contents().length){
if($inBox.children().length <= i){
// we ran out of columns, make another
$inBox.append($("

")); //"
}
var $col = $inBox.children().eq(i);
if (!columnize($col, $destroyable, $col, targetHeight))
{
// do a split, but only if the last item in the column isn't a "dontend"
if(!$destroyable.contents().find(":first-child").hasClass("dontend")){
split($col/*put in here*/, $destroyable/*pull out here*/, $col, targetHeight);
}else{
//	alert("not splitting a dontend");
}
}

while(checkDontEndColumnOnConstraints($col.children(":last").length && $col.children(":last").get(0))){
var $lastKid = $col.children(":last");
$lastKid.remove();
$destroyable.prepend($lastKid);
}
i++;
}
if(options.overflow && !scrollHorizontally){
var IE6 = false /*@cc_on || @_jscript_version < 5.7 @*/;
var IE7 = (document.all) && (navigator.appVersion.indexOf("MSIE 7.") != -1);
if(IE6 || IE7){
var html = "";
var div = document.createElement('DIV');
while($destroyable[0].childNodes.length > 0){
var kid = $destroyable[0].childNodes[0];
for(var i=0;i max) {
max = h;
lastIsMax = true;
}
if(h < min) min = h;
}}($inBox));

var avgH = totalH / numCols;
if(options.lastNeverTallest && lastIsMax){
// the last column is the tallest
// so allow columns to be taller
// and retry
adjustment += 30;
if(adjustment < 100){
targetHeight = targetHeight + 30;
if(loopCount == maxLoops-1) maxLoops++;
}else{
debugger;
loopCount = maxLoops;
}
}else if(max - min > 30){
// too much variation, try again
targetHeight = avgH + 30;
}else if(Math.abs(avgH-targetHeight) > 20){
// too much variation, try again
targetHeight = avgH;
}else {
// solid, we're done
loopCount = maxLoops;
}
}else{
// it's scrolling horizontally, fix the classes of the columns
$inBox.children().each(function(i){
$col = $inBox.children().eq(i);
if(i==0){
$col.addClass("first");
}else if(i==$inBox.children().length-1){
$col.addClass("last");
}else{
$col.removeClass("first");
$col.removeClass("last");
}
});
}
$inBox.append($("
"));
}
for (var i=0;i<10;i++)
$inBox.find('.column>br:first-child').remove();
$inBox.find('.column>:first-child.removeiffirst').remove();
$inBox.find('.column>:last-child.removeiflast').remove();
$inBox.data("columnizing", false);

if(options.overflow){
options.overflow.doneFunc();
}
options.doneFunc();
}
});
};
})(jQuery);
```