123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- // version 1.6.0
- // http://welcome.totheinter.net/columnizer-jquery-plugin/
- // created by: Adam Wulf @adamwulf, adam.wulf@gmail.com
- (function($){
- $.fn.columnize = function(options) {
- var defaults = {
- // default width of columns
- width: 400,
- // 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,
- // don't automatically layout columns, only use manual columnbreak
- manualBreaks : false,
- // previx for all the CSS classes used by this plugin
- // default to empty string for backwards compatibility
- cssClassPrefix : ""
- };
- var options = $.extend(defaults, options);
-
- if(typeof(options.width) == "string"){
- options.width = parseInt(options.width);
- if(isNaN(options.width)){
- options.width = defaults.width;
- }
- }
- 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 manualBreaks = options.manualBreaks;
- var cssClassPrefix = defaults.cssClassPrefix;
- if(typeof(options.cssClassPrefix) == "string"){
- cssClassPrefix = options.cssClassPrefix;
- }
-
- 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
- }
- });
- }
-
- function prefixTheClassName(className, withDot){
- var dot = withDot ? "." : "";
- if(cssClassPrefix.length){
- return dot + cssClassPrefix + "-" + className;
- }
- return dot + className;
- }
-
-
- /**
- * this fuction builds as much of a column as it can without
- * splitting nodes in half. If the last node in the new column
- * is a text node, then it will try to split that text node. otherwise
- * it will leave the node in $pullOutHere and return with a height
- * smaller than targetHeight.
- *
- * 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). return false if the caller should
- * split a node if possible to end this column.
- *
- * @param putInHere, the jquery node to put elements into for the current column
- * @param $pullOutHere, the jquery node to pull elements out of (uncolumnized html)
- * @param $parentColumn, the jquery node for the currently column that's being added to
- * @param targetHeight, the ideal height for the column, get as close as we can to this height
- */
- function columnize($putInHere, $pullOutHere, $parentColumn, targetHeight){
- //
- // add as many nodes to the column as we can,
- // but stop once our height is too tall
- while((manualBreaks || $parentColumn.height() < targetHeight) &&
- $pullOutHere[0].childNodes.length){
- var node = $pullOutHere[0].childNodes[0]
- //
- // Because we're not cloning, jquery will actually move the element"
- // http://welcome.totheinter.net/2009/03/19/the-undocumented-life-of-jquerys-append/
- if($(node).find(prefixTheClassName("columnbreak", true)).length){
- //
- // our column is on a column break, so just end here
- return;
- }
- if($(node).hasClass(prefixTheClassName("columnbreak"))){
- //
- // our column is on a column break, so just end here
- return;
- }
- $putInHere.append(node);
- }
- if($putInHere[0].childNodes.length == 0) return;
-
- // now we're too tall, so undo the last one
- var kids = $putInHere[0].childNodes;
- var lastKid = kids[kids.length-1];
- $putInHere[0].removeChild(lastKid);
- var $item = $(lastKid);
-
- //
- // now lets try to split that last node
- // to fit as much of it as we can into this column
- if($item[0].nodeType == 3){
- // it's a text node, split it up
- var oText = $item[0].nodeValue;
- var counter2 = options.width / 18;
- if(options.accuracy)
- counter2 = options.accuracy;
- var columnText;
- var latestTextNode = null;
- while($parentColumn.height() < targetHeight && oText.length){
- var indexOfSpace = oText.indexOf(' ', counter2);
- if (indexOfSpace != -1) {
- columnText = oText.substring(0, oText.indexOf(' ', counter2));
- } else {
- columnText = oText;
- }
- latestTextNode = document.createTextNode(columnText);
- $putInHere.append(latestTextNode);
-
- if(oText.length > counter2 && indexOfSpace != -1){
- oText = oText.substring(indexOfSpace);
- }else{
- oText = "";
- }
- }
- if($parentColumn.height() >= targetHeight && 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
- }
- }
-
- if($pullOutHere.contents().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, targetHeight){
- if($putInHere.contents(":last").find(prefixTheClassName("columnbreak", true)).length){
- //
- // our column is on a column break, so just end here
- return;
- }
- if($putInHere.contents(":last").hasClass(prefixTheClassName("columnbreak"))){
- //
- // our column is on a column break, so just end here
- return;
- }
- if($pullOutHere.contents().length){
- var $cloneMe = $pullOutHere.contents(":first");
- //
- // make sure we're splitting an element
- if($cloneMe.get(0).nodeType != 1) return;
-
- //
- // clone the node with all data and events
- var $clone = $cloneMe.clone(true);
- //
- // need to support both .prop and .attr if .prop doesn't exist.
- // this is for backwards compatibility with older versions of jquery.
- if($cloneMe.hasClass(prefixTheClassName("columnbreak"))){
- //
- // ok, we have a columnbreak, so add it into
- // the column and exit
- $putInHere.append($clone);
- $cloneMe.remove();
- }else if (manualBreaks){
- // keep adding until we hit a manual break
- $putInHere.append($clone);
- $cloneMe.remove();
- }else if($clone.get(0).nodeType == 1 && !$clone.hasClass(prefixTheClassName("dontend"))){
- $putInHere.append($clone);
- if($clone.is("img") && $parentColumn.height() < targetHeight + 20){
- //
- // we can't split an img in half, so just add it
- // to the column and remove it from the pullOutHere section
- $cloneMe.remove();
- }else if(!$cloneMe.hasClass(prefixTheClassName("dontsplit")) && $parentColumn.height() < targetHeight + 20){
- //
- // pretty close fit, and we're not allowed to split it, so just
- // add it to the column, remove from pullOutHere, and be done
- $cloneMe.remove();
- }else if($clone.is("img") || $cloneMe.hasClass(prefixTheClassName("dontsplit"))){
- //
- // it's either an image that's too tall, or an unsplittable node
- // that's too tall. leave it in the pullOutHere and we'll add it to the
- // next column
- $clone.remove();
- }else{
- //
- // ok, we're allowed to split the node in half, so empty out
- // the node in the column we're building, and start splitting
- // it in half, leaving some of it in pullOutHere
- $clone.empty();
- if(!columnize($clone, $cloneMe, $parentColumn, targetHeight)){
- // this node still has non-text nodes to split
- // add the split class and then recur
- $cloneMe.addClass(prefixTheClassName("split"));
- if($cloneMe.children().length){
- split($clone, $cloneMe, $parentColumn, targetHeight);
- }
- }else{
- // this node only has text node children left, add the
- // split class and move on.
- $cloneMe.addClass(prefixTheClassName("split"));
- }
- if($clone.get(0).childNodes.length == 0){
- // it was split, but nothing is in it :(
- $clone.remove();
- }
- }
- }
- }
- }
-
-
- function singleColumnizeIt() {
- if ($inBox.data("columnized") && $inBox.children().length == 1) {
- return;
- }
- $inBox.data("columnized", true);
- $inBox.data("columnizing", true);
-
- $inBox.empty();
- $inBox.append($("<div class='"
- + prefixTheClassName("first") + " "
- + prefixTheClassName("last") + " "
- + prefixTheClassName("column") + " "
- + "' style='width:100%; float: " + options.columnFloat + ";'></div>")); //"
- $col = $inBox.children().eq($inBox.children().length-1);
- $destroyable = $cache.clone(true);
- if(options.overflow){
- targetHeight = options.overflow.height;
- columnize($col, $destroyable, $col, targetHeight);
- // make sure that the last item in the column isn't a "dontend"
- if(!$destroyable.contents().find(":first-child").hasClass(prefixTheClassName("dontend"))){
- split($col, $destroyable, $col, targetHeight);
- }
-
- while($col.contents(":last").length && checkDontEndColumn($col.contents(":last").get(0))){
- var $lastKid = $col.contents(":last");
- $lastKid.remove();
- $destroyable.prepend($lastKid);
- }
- var html = "";
- var div = document.createElement('DIV');
- while($destroyable[0].childNodes.length > 0){
- var kid = $destroyable[0].childNodes[0];
- if(kid.attributes){
- 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{
- $col.append($destroyable);
- }
- $inBox.data("columnizing", false);
-
- if(options.overflow && options.overflow.doneFunc){
- options.overflow.doneFunc();
- }
-
- }
-
- /**
- * returns true if the input dom node
- * should not end a column.
- * returns false otherwise
- */
- function checkDontEndColumn(dom){
- if(dom.nodeType == 3){
- // text node. ensure that the text
- // is not 100% whitespace
- if(/^\s+$/.test(dom.nodeValue)){
- //
- // ok, it's 100% whitespace,
- // so we should return checkDontEndColumn
- // of the inputs previousSibling
- if(!dom.previousSibling) return false;
- return checkDontEndColumn(dom.previousSibling);
- }
- return false;
- }
- if(dom.nodeType != 1) return false;
- if($(dom).hasClass(prefixTheClassName("dontend"))) return true;
- if(dom.childNodes.length == 0) return false;
- return checkDontEndColumn(dom.childNodes[dom.childNodes.length-1]);
- }
-
-
-
- function columnizeIt() {
- //reset adjustment var
- adjustment = 0;
- if(lastWidth == $inBox.width()) return;
- lastWidth = $inBox.width();
-
- var numCols = Math.round($inBox.width() / options.width);
- var optionWidth = options.width;
- var optionHeight = options.height;
- if(options.columns) numCols = options.columns;
- if(manualBreaks){
- numCols = $cache.find(prefixTheClassName("columnbreak", true)).length + 1;
- optionWidth = false;
- }
-
- // if ($inBox.data("columnized") && numCols == $inBox.children().length) {
- // return;
- // }
- if(numCols <= 1){
- return singleColumnizeIt();
- }
- if($inBox.data("columnizing")) return;
- $inBox.data("columnized", true);
- $inBox.data("columnizing", true);
-
- $inBox.empty();
- $inBox.append($("<div style='width:" + (Math.floor(100 / numCols))+ "%; 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(optionHeight && optionWidth){
- maxLoops = 1;
- targetHeight = optionHeight;
- scrollHorizontally = true;
- }
-
- //
- // 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'.
- //
- // also, lets hard code the max loops to 20. that's /a lot/ of loops for columnizer,
- // and should keep run aways in check. if somehow someone has content combined with
- // options that would cause an infinite loop, then this'll definitely stop it.
- for(var loopCount=0;loopCount<maxLoops && maxLoops < 20;loopCount++){
- $inBox.empty();
- var $destroyable;
- 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) ? prefixTheClassName("first") : "";
- className += " " + prefixTheClassName("column");
- var className = (i == numCols - 1) ? (prefixTheClassName("last") + " " + className) : className;
- $inBox.append($("<div class='" + className + "' style='width:" + (Math.floor(100 / numCols))+ "%; 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='width:" + (Math.floor(100 / numCols))+ "%; float: " + options.columnFloat + ";'></div>")); //"
- }
- var $col = $inBox.children().eq(i);
- columnize($col, $destroyable, $col, targetHeight);
- // make sure that the last item in the column isn't a "dontend"
- split($col, $destroyable, $col, targetHeight);
-
- while($col.contents(":last").length && checkDontEndColumn($col.contents(":last").get(0))){
- var $lastKid = $col.contents(":last");
- $lastKid.remove();
- $destroyable.prepend($lastKid);
- }
- i++;
-
- //
- // https://github.com/adamwulf/Columnizer-jQuery-Plugin/issues/47
- //
- // check for infinite loop.
- //
- // this could happen when a dontsplit or dontend item is taller than the column
- // we're trying to build, and its never actually added to a column.
- //
- // this results in empty columns being added with the dontsplit item
- // perpetually waiting to get put into a column. lets force the issue here
- if($col.contents().length == 0 && $destroyable.contents().length){
- //
- // ok, we're building zero content columns. this'll happen forever
- // since nothing can ever get taken out of destroyable.
- //
- // to fix, lets put 1 item from destroyable into the empty column
- // before we iterate
- $col.append($destroyable.contents(":first"));
- }else if(i == numCols - (options.overflow ? 0 : 1) && !options.overflow){
- //
- // ok, we're about to exit the while loop because we're done with all
- // columns except the last column.
- //
- // if $destroyable still has columnbreak nodes in it, then we need to keep
- // looping and creating more columns.
- if($destroyable.find(prefixTheClassName("columnbreak", true)).length){
- numCols ++;
- }
- }
-
- }
- 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){
- // 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;
- var numberOfColumnsThatDontEndInAColumnBreak = 0;
- $inBox.children().each(function($inBox){ return function($item){
- var $col = $inBox.children().eq($item);
- var endsInBreak = $col.children(":last").find(prefixTheClassName("columnbreak", true)).length;
- if(!endsInBreak){
- var h = $col.height();
- lastIsMax = false;
- totalH += h;
- if(h > max) {
- max = h;
- lastIsMax = true;
- }
- if(h < min) min = h;
- numberOfColumnsThatDontEndInAColumnBreak++;
- }
- }}($inBox));
- var avgH = totalH / numberOfColumnsThatDontEndInAColumnBreak;
- if(totalH == 0){
- //
- // all columns end in a column break,
- // so we're done here
- loopCount = maxLoops;
- }else if(options.lastNeverTallest && lastIsMax){
- // the last column is the tallest
- // so allow columns to be taller
- // and retry
- //
- // hopefully this'll mean more content fits into
- // earlier columns, so that the last column
- // can be shorter than the rest
- adjustment += 30;
- targetHeight = targetHeight + 30;
- if(loopCount == maxLoops-1) 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 width/classes of the columns
- $inBox.children().each(function(i){
- $col = $inBox.children().eq(i);
- $col.width(optionWidth + "px");
- if(i==0){
- $col.addClass(prefixTheClassName("first"));
- }else if(i==$inBox.children().length-1){
- $col.addClass(prefixTheClassName("last"));
- }else{
- $col.removeClass(prefixTheClassName("first"));
- $col.removeClass(prefixTheClassName("last"));
- }
- });
- $inBox.width($inBox.children().length * optionWidth + "px");
- }
- $inBox.append($("<br style='clear:both;'>"));
- }
- $inBox.find(prefixTheClassName("column", true)).find(":first" + prefixTheClassName("removeiffirst", true)).remove();
- $inBox.find(prefixTheClassName("column", true)).find(':last' + prefixTheClassName("removeiflast", true)).remove();
- $inBox.data("columnizing", false);
- if(options.overflow){
- options.overflow.doneFunc();
- }
- options.doneFunc();
- }
- });
- };
- })(jQuery);
|