// 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);
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).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){
//
// 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( 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"));
}
}
}
}
}
}
}
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);