// version 1.6.2 // http://welcome.totheinter.net/columnizer-jquery-plugin/ // created by: Adam Wulf @adamwulf, adam.wulf@gmail.com (function($){ var DATA_ORIGINAL_DOM_KEY = 'columnizer-original-dom'; $.fn.columnize = function(options) { // save original DOM clone as data this.each(function() { var $el = $(this); $el.data(DATA_ORIGINAL_DOM_KEY, $el.clone(true, true)); }); this.cols =[]; this.offset= 0; this.before=[]; this.lastOther=0; this.prevMax =0; this.debug=0; this.setColumnStart =null; this.elipsisText=''; 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, // false to round down column widths (for compatibility) // true to conserve all decimals in the column widths precise : false, // don't automatically layout columns, only use manual columnbreak manualBreaks : false, // disable single column layout when container width < columnWidth // (useful for horizontally scrollable columns in mobile view) disableSingle : false, // previx for all the CSS classes used by this plugin // default to empty string for backwards compatibility cssClassPrefix : "", elipsisText:'...', debug:0 }; options = $.extend(defaults, options); // Variable array for holding and from each table // As we split a table we need to keep copies of the and to place on each column var tables = new Array(); // Find all the table elements in the page $('table').each(function () { // Check if we have not already saved the and if (!$(this).hasClass('tableSaved')) { // Mark the found table by adding the .tableSaved class $(this).addClass('tableSaved') // Give the table a unique ID so we can re-add its elements later $(this).addClass('tableID-' + tables.length) // Save the tables unique ID, , and as an object in our tables array tables.push({ tableID: 'tableID-' + tables.length, thead: $(this).find('thead:first').clone(), tfoot: $(this).find('tfoot:first').clone() }); } }); // Function to add and to all tables in $pullOutHere // This function should be called anywhere split() returns as // that is the point where a column is complete and the remaining content // is not going to change until the next columnize() is called function fixTables($pullOutHere) { // Iterate through all of our saved tables for (i = 0; i < tables.length; i++) { // Check if the root element is a table if ($pullOutHere.is("table")) { // Check if the root element has any elements and // is the current table id for this loop if ($pullOutHere.children('tfoot').length == 0 && $pullOutHere.hasClass(tables[i].tableID)) { // Add the to the table $(tables[i].tfoot).clone().prependTo($pullOutHere); } // Check if the root element has any elements and // is the current table id for this loop if ($pullOutHere.children('thead').length == 0 && $pullOutHere.hasClass(tables[i].tableID)) { // Add the to the table $(tables[i].thead).clone().prependTo($pullOutHere); } } // Check if there are any child tables to the root element with the current table ID $pullOutHere.find('table .' + tables[i].tableID).each(function () { // Check if the child table has no if ($(this).children('tfoot').length == 0) { // Add the to the table $(tables[i].tfoot).clone().prependTo(this); } // Check if the child table has no if ($(this).children('thead').length == 0) { // Add the to the table $(tables[i].thead).clone().prependTo(this); } }); } } if(typeof(options.width) == "string"){ options.width = parseInt(options.width,10); if(isNaN(options.width)){ options.width = defaults.width; } } if(typeof options.setColumnStart== 'function') { this.setColumnStart=options.setColumnStart; } if(typeof options.elipsisText== 'string') { this.elipsisText=options.elipsisText; } if(options.debug) { // assert is off by default this.debug=options.debug; } if(!options.setWidth) { if (options.precise) { options.setWidth = function (numCols) { return 100 / numCols; }; } else { options.setWidth = function (numCols) { return Math.floor(100 / numCols); }; } } /** * appending a text node to a will * cause a jquery crash. * so wrap all append() calls and revert to * a simple appendChild() in case it fails */ function appendSafe($target, $elem){ try{ $target.append($elem); }catch(e){ $target[0].appendChild($elem[0]); } } 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 manualBreaks = options.manualBreaks; var cssClassPrefix = defaults.cssClassPrefix; if(typeof(options.cssClassPrefix) == "string"){ cssClassPrefix = options.cssClassPrefix; } var adjustment = 0; appendSafe($cache, $(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"); appendSafe($inBox.empty(), $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).on('resize', function() { if(!options.buildOnce){ if($inBox.data("timeout")){ clearTimeout($inBox.data("timeout")); } $inBox.data("timeout", setTimeout(columnizeIt, 200)); } }); } 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; } appendSafe($putInHere, $(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){ // // it's been brought up that this won't work for chinese // or other languages that don't have the same use of whitespace // as english. This will need to be updated in the future // to better handle non-english languages. // // https://github.com/adamwulf/Columnizer-jQuery-Plugin/issues/124 var indexOfSpace = oText.indexOf(' ', counter2); if (indexOfSpace != -1) { columnText = oText.substring(0, indexOfSpace); } else { columnText = oText; } latestTextNode = document.createTextNode(columnText); appendSafe($putInHere, $(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{ appendSafe($pullOutHere, $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){ // Fix any tables that have had their and moved fixTables($pullOutHere); // // our column is on a column break, so just end here return; } if($putInHere.contents(":last").hasClass(prefixTheClassName("columnbreak"))){ // Fix any tables that have had their and moved fixTables($pullOutHere); // // 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( typeof $cloneMe.get(0) == 'undefined' || $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 appendSafe($putInHere, $clone); $cloneMe.remove(); }else if (manualBreaks){ // keep adding until we hit a manual break appendSafe($putInHere, $clone); $cloneMe.remove(); }else if($clone.get(0).nodeType == 1 && !$clone.hasClass(prefixTheClassName("dontend"))){ appendSafe($putInHere, $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 may still have non-text nodes to split // add the split class and then recur $cloneMe.addClass(prefixTheClassName("split")); //if this node was ol element, the child should continue the number ordering if($cloneMe.get(0).tagName == 'OL'){ var startWith = $clone.get(0).childElementCount + $clone.get(0).start; $cloneMe.attr('start',startWith+1); } 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(); $cloneMe.removeClass(prefixTheClassName("split")); }else if($clone.get(0).childNodes.length == 1){ // was the only child node a text node w/ whitespace? var onlyNode = $clone.get(0).childNodes[0]; if(onlyNode.nodeType == 3){ // text node var nonwhitespace = /\S/; var str = onlyNode.nodeValue; if(!nonwhitespace.test(str)){ // yep, only a whitespace textnode $clone.remove(); $cloneMe.removeClass(prefixTheClassName("split")); } } } } } } // Fix any tables that have had their and moved fixTables($pullOutHere); } function singleColumnizeIt() { if ($inBox.data("columnized") && $inBox.children().length == 1) { return; } $inBox.data("columnized", true); $inBox.data("columnizing", true); $inBox.empty(); $inBox.append($("
")); //" $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")); //" $col = $inBox.children(":last"); appendSafe($col, $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")); //" } // fill all but the last column (unless overflowing) 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($("
")); //" } $col = $inBox.children().eq(i); if(scrollHorizontally){ $col.width(optionWidth + "px"); } 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))){ $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 @if (@_jscript_version < 5.7) IE6 = true; @end @*/ 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(i=0;i 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 += 5; 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($("
")); } $inBox.find(prefixTheClassName("column", true)).find(":first" + prefixTheClassName("removeiffirst", true)).remove(); $inBox.find(prefixTheClassName("column", true)).find(':last' + prefixTheClassName("removeiflast", true)).remove(); $inBox.find(prefixTheClassName("split", true)).find(":first" + prefixTheClassName("removeiffirst", true)).remove(); $inBox.find(prefixTheClassName("split", true)).find(':last' + prefixTheClassName("removeiflast", true)).remove(); $inBox.data("columnizing", false); if(options.overflow){ options.overflow.doneFunc(); } options.doneFunc(); } }); }; $.fn.uncolumnize = function() { // revert to initial DOM this.each(function() { var $el = $(this), $clone; if($clone = $el.data(DATA_ORIGINAL_DOM_KEY)) { $el.replaceWith($clone); } }); }; $.fn.renumberByJS=function($searchTag, $colno, $targetId, $targetClass ) { this.setList = function($cols, $list, $tag1) { var $parents = this.before.parents(); var $rest; $rest = $($cols[this.offset-1]).find('>*'); if( ($rest.last())[0].tagName!=$tag1.toUpperCase()) { if(this.debug) { console.log("Last item in previous column, isn't a list..."); } return 0; } $rest = $rest.length; var $tint = 1; if(this.lastOther<=0) { $tint = this.before.children().length+1; } else { $tint = $($parents[this.lastOther]).children().length+1; } // if the first LI in the current column is split, decrement, as we want the same number/key if( $($cols[this.offset]).find($tag1+':first li.split').length ) { var $whereElipsis=$($cols[this.offset-1]).find($tag1+':last li:last'); if( this.elipsisText==='' || $($cols[this.offset-1]).find($tag1+':last ~ div').length || $($cols[this.offset-1]).find($tag1+':last ~ p').length ) { ; } else { if($($whereElipsis).find('ul, ol, dl').length ==0 ) { var $txt=$whereElipsis.last().text(); // char counting, 'cus MSIE 8 is appearently stupid var $len=$txt.length; if($txt.substring($len-1)==';') { if($txt.substring($len-4)!=this.elipsisText+';') { $txt=$txt.substring(0, $len-1)+this.elipsisText+';'; } } else { if($txt.substring($len-3)!=this.elipsisText) { $txt+=this.elipsisText; } } $whereElipsis.last().text($txt); } } // an item in split between two columns. it only holds one key... if($($cols[this.offset]).find($tag1+':first >li.split >'+$tag1).length==0) { $tint--; } } if($rest==1) { // the last column only held one thing, so assume its wrapped to the column before that as well. $tint += this.prevMax ; } if(this.nest>1) { if(this.debug) { console.log("Supposed to be a nested list...decr"); } $tint--; // some how, id previous list starts split, need secins decrement, // if "split" is now correct, reference this var $tt = $($cols[this.offset -1]).find($tag1+':first li.split:first'); if($tt.length>0) { if(this.debug) { console.log("Previous column started with a split item, so that count is one less than expected"); } $tint--; } $tt = $($cols[this.offset]).find($tag1+':first li:first').clone(); $tt.children().remove(); if( $.trim($tt.text()).length>0 ){ if(this.debug) { console.log("If that was a complete list in the previous column, don't decr."); } $tint++; if($($cols[this.offset-1]).find(">"+$tag1+':last ').children().length==0 ) { if(this.debug) { console.log("unless that was empty, in which case revert"); } $tint--; } } } else { var $tt = $($cols[this.offset]).find($tag1+':first li:first '+$tag1+".split li.split"); if($tt.length>0) { if(this.debug) { console.log("[Nested] Column started with a split item, so that count is one less than expected"); } $tint--; } } if(this.debug) { console.log("Setting the start value to "+$tint+" ("+this.prevMax +")"); } if($tint >0) { // if the above computation leads to 0, or an empty list (more likely), don't set, leave as 1 if(typeof this.setColumnStart == 'function') { this.setColumnStart($list, $tint); } else { $list.attr('start', $tint); } } return 0; } if(typeof $targetId === 'undefined') { $targetId=false; } if(typeof $targetClass === 'undefined') { $targetClass=false; } if(! $targetId && !$targetClass ) { throw "renumberByJS(): Bad param, must pass an id or a class"; } var $target =''; this.prevMax =1; if($targetClass) { $target ="."+$targetClass; } else { $target ="#"+$targetId; } var $tag1 = $searchTag.toLowerCase(); var $tag2 = $searchTag.toUpperCase(); this.cols = $($target); if(this.debug) { console.log("There are "+this.cols.length+" items, looking for "+$tag1); } this.before = $(this.cols[0]).find($tag1+':last'); this.prevMax = this.before.children().length; // start at 1, as must compare to previous... for(this.offset=1; this.offset"+$tag1+':first li '+$tag1+":first").length) { this.nest = 2; } this.setList(this.cols, $list, $tag1); this.lastOther--; $list = $(this.cols[this.offset]).find($tag1+':first li '+$tag1+":first"); if($list.length) { // I hope the two columns have same nesting, or its busted this.before= $(this.cols[this.offset-1]).find(">"+$tag1+':last li '+$tag1+":last"); this.prevMax= 0; this.nest =1; this.setList(this.cols, $list, $tag1); } var $reset = $(this.cols[this.offset-1]).find(">"+$tag1+':last'); this.prevMax = $reset.children().length; } } return 0; }; })(jQuery);