Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code is poorly commented #32

Closed
chrisgraham opened this issue Jan 20, 2012 · 2 comments
Closed

Code is poorly commented #32

chrisgraham opened this issue Jan 20, 2012 · 2 comments

Comments

@chrisgraham
Copy link

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 = $('<div></div>'); // 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($("<div style='float: " + options.columnFloat + ";'></div>")); //"
            $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<maxLoops;loopCount++){
                if (typeof window.console!='undefined') console.log('STARTING COLUMNISATION ITERATION');

                $inBox.empty();
                var $destroyable; // This is where we'll pull all our data from, as we progressively fill our columns
                try{
                    $destroyable = $cache.clone(true);
                }catch(e){
                    // jquery in ie6 can't clone with true
                    $destroyable = $cache.clone();
                }
                $destroyable.css("visibility", "hidden");
                // create the columns
                for (var i = 0; i < numCols; i++) {
                    /* create column */
                    var className = (i == 0) ? "first column" : "column";
                    var className = (i == numCols - 1) ? ("last " + className) : className;
                    $inBox.append($("<div class='" + className + "' style='float: " + options.columnFloat + ";'></div>")); //"
                }

                // 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($("<div class='" + className + "' style='float: " + options.columnFloat + ";'></div>")); //"
                    }
                    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<kid.attributes.length;i++){
                                if(kid.attributes[i].nodeName.indexOf("jQuery") == 0){
                                    kid.removeAttribute(kid.attributes[i].nodeName);
                                }
                            }
                            div.innerHTML = "";
                            div.appendChild($destroyable[0].childNodes[0]);
                            html += div.innerHTML;
                        }
                        var overflow = $(options.overflow.id)[0];
                        overflow.innerHTML = html;
                    }else{
                        $(options.overflow.id).empty().append($destroyable.contents().clone(true));
                    }
                }else if(!scrollHorizontally){
                    // it's scrolling horizontally, try and workout our average height. We know it initially but if the last column is too high we need to raise 'adjustment'. We try this over a few iterations until we're 'solid'.

                    // the last column in the series
                    $col = $inBox.children().eq($inBox.children().length-1);
                    while($destroyable.contents().length) $col.append($destroyable.contents(":first"));
                    var afterH = $col.height();
                    var diff = afterH - targetHeight;
                    var totalH = 0;
                    var min = 10000000;
                    var max = 0;
                    var lastIsMax = false;
                    $inBox.children().each(function($inBox){ return function($item){
                        var h = $inBox.children().eq($item).height();
                        lastIsMax = false;
                        totalH += h;
                        if(h > 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($("<br style='clear:both;'>"));
            }
            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);
@chrisgraham
Copy link
Author

This code should not be considered "my version" of columnizer. I've posted quite a lot of fixes to bugs I found separately, some of which are newer than this code. This is only here so you can see how I tidied up the code to help other developers understand it. I suggest you run a diff and pull some of my changes over if you want to make things easier for further code collaboration.

@adamwulf
Copy link
Owner

I've taken many of your comments and added them into the master branch. I'm also going through tickets now and fixing many of the issues you've posted. thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants