/*
	MultiColumn text reflower v1.3 by Randy Simons
	
	Usage:
		
		new MultiColumn(<node of column container element>,new MultiColumnSettings());
		
		The container element itself should not contain any text. Instead, use paragraph elements,
		lists, tables etc. for text.
		
		The extra height per column is optional. Default this is 50 (pixels). If the last column
		"sticks out", you can try to increase this number.
		
		USE VALID (X)HTML!
		
	Example:
		<div id="container">
			<div>
				<h1> heading </h1>
				<p> text </p>
				<p> text </p>
				...
				<p> text </p>
			</div>
		</div>
			
		<script type="text/javascript">
		
		multiColumnSettings=new MultiColumnSettings;
		multiColumnSettings.classNameScreen='columns';
		multiColumnSettings.minSplitHeight=400;
		multiColumnSettings.minHeight=300;
		
		new MultiColumn(document.getElementById("container"),multiColumnSettings);
		</script>
	
	Suggested CSS, with requirements:
		/* Initial definitions for base column. 
		   Define the (minimum) width here, and optionally a padding * /
		.columns div {
			float: left;	/* Columns MUST float left * /
			width: 20em;	/* A width must be given * /
			padding: 10px;	/* You may use a padding... but thanks to IE you can only use pixels! * /
			position: relative;
			text-align: justify;
			margin: 0; 		/* Don't use a margin! * /
		}
		
		/* Optional 'read on'-message * /
		.columns div .readOn {
			position: absolute;
			right: 1em;
			bottom: -0.5em;
			color: #999999;
		}

	License:
		This file is entirely BSD-style licensed.
	
	More information and contact:
		http://randysimons.com
		
	History:
		v1.3 (27-5-2006)
			- Added numberOfColumns setting. Now you can specify a fixed number of columns instead
			  of a calculation based on minimum width.
			- Improved printing options. A classNamePrint can be specified. If set, the original
			  column container is duplicated, inserted above the original container and the given
			  classname will be set. Use media-rules to diffentiate between print and screen-styles.
			- The old className is renamed to classNameScreen. It now sets the class name of the
			  *container*, instead of the columns"
			- The "read on" text can be specified.
			- Fixed IE6 compatibility, was broken since v1.2.1. (keyword: hasLayout. *sigh* IE6 is
			  simply obsolete.)
			- Automatically adds a clearing element at the bottom of each container.
			  
		v1.2.1 (18-3-2006)
			- Width of the container is used for calculations, instead of width of parent of
			  container.
			- Fix for resizing with more than one container.
			
		v1.2   (12-3-2006)
			- Settings are passed by creating an instance of the MultiColumnSettings class, so
			  you can easy change some default values at choice.
			- Added minSplitHeight. Only one column will be used if the height of this column is
			 less than minHeight.
			- Added minHeight. This is the minimum height for every column. Avoids 'flat' columns.
			- Added addReadOn. If true, the 'Read On' notice will be added to the columns.
			- BUG fixes in ParapgraphWrapper.
			- Added HeadingWrapper, so headings are kept with next paragraph.
				
		v1.1   (10-3-2006)
			- The height of the content in the columns is now equalized, by splitting elements and
			  wrapping their content to the next column. Currently only paragraphs and lists can
			  be splitted.
			- Introduced the ParapgraphWrapper and ListWrapper for this feature.
			- Added uncertainty. And a parameter to compensatate.
	
		v1.0   (8-3-2006)
			First release
*/

//Some stuff needs to be maintained in a global variable.
MultiColumnResizeTimer=null;
MultiColumnList=null;

function MultiColumnSettings() {
	this.extraHeight=50;		//Add extra height to a column
	this.minSplitHeight=0;		//If the base column is smaller than minSplitHeight, the column does not split.
	this.minHeight=0;			//Minimum height of a column
	this.readOnText=null;		//Add the "read on" notice a the bottom of each column
	this.classNameScreen=null;	//Set the classname of *container* of the rendered columns for screen-media. Use to differentiate between JS/non-JS base column widths.
	this.classNamePrint=null;	//Set the classname of *container* of the rendered columns for print-media. (optional, may be null)
	this.numberOfColumns=null; 	//null: calculate number of columns based on minimum width. Number >0: use specified number of columns, always adapt width.
}

function MultiColumn(columnContainerIn,settingsIn) {
	//IE6 doesn't support HTMLElement prototyping. IE7 probably won't too. Let's aim for IE8! *sigh*	
	//But thank you, www.quirksmode.org!
	this.getStyle= function (element,stylePropW3,stylePropIE) {
		var y = null;
		if (element.currentStyle)
			y = element.currentStyle[stylePropIE];
		else if (window.getComputedStyle && document.defaultView.getComputedStyle(element,null)) {
			y = document.defaultView.getComputedStyle(element,null).getPropertyValue(stylePropW3);
		}
		return y;
	}
	
	this.generateColumns= function () {
		var i=0;
		var numColumns;
		
		//Obtain the base column. This column contains the original text.
		var baseColumn=this.columnContainer.getElementsByTagName('div').item(0);
		
		//Add a node with style: "clear: both;" to stretch the container-node.
		var clearingNode=document.createElement('span');
		clearingNode.style.display="block";
		clearingNode.style.clear="both";
		clearingNode.style.zoom="1"; //yet another work-around for a certain obsolete browser.
		this.columnContainer.appendChild(clearingNode);
		
		//Use specified number of columns, or calculate number based on minimum width?
		if (this.settings.numberOfColumns!=null) {
			//Use specified number of columns
			numColumns=this.settings.numberOfColumns;
		}	else {
			//Calculate the number of columns that can be added, based on width.
			numColumns=Math.floor(this.columnContainer.offsetWidth/(baseColumn.offsetWidth)); //baseColumn.getStyle('width') gives wrong value in Opera
		}
		
		//Calculate the available width for one column.
		var availableWidth=Math.floor((this.columnContainer.offsetWidth-10)/numColumns)-parseInt(this.getStyle(baseColumn,'padding-right','paddingRight'))-parseInt(this.getStyle(baseColumn,'padding-left','paddingLeft'));
		
		//Add new columns
		for (i=1;i<numColumns;i++) {
			this.columnContainer.insertBefore(baseColumn.cloneNode(false),this.columnContainer.firstChild);
		}
				
		//First, set the new width for the existing column..
		baseColumn.style.width=availableWidth+'px';
		
		//Get all columns in the container
		var columns=this.columnContainer.getElementsByTagName('div');
		
		//..then calculate the average needed height for other .
		var minHeight;
		
		if (baseColumn.offsetHeight<=this.settings.minSplitHeight) {
			var minHeight=baseColumn.offsetHeight;
		} else {
			var minHeight=Math.max(parseInt((baseColumn.offsetHeight+numColumns*this.settings.extraHeight)/columns.length),this.settings.minHeight);
		}

		//Cut/paste blocks from the baseColumn to the new columns, until the reached the minHeight.
		for (i=0;i<columns.length-1;i++) {
			var currentColumn=columns.item(i);
			currentColumn.style.width=availableWidth+'px';
			
			//Cut/paste blocks from the baseColumn to the current column, while the
			//current column has not reach the minHeight
			while (currentColumn.offsetHeight<minHeight && baseColumn.hasChildNodes()) {
				if (baseColumn.firstChild.nodeType==1) { //Node.ELEMENT_NODE Doesn't work in ^%@$#@$!!! IE6
					currentColumn.appendChild(baseColumn.firstChild);
				} else {
					baseColumn.removeChild(baseColumn.firstChild);
				}
			}
			
			//Some elements can be split and wrapped to the next column
			
			var lastChild=currentColumn.lastChild;
			switch (lastChild.nodeName.toLowerCase()) {
				case 'p': 
					new ParapgraphWrapper(currentColumn,lastChild,columns.item(i+1),minHeight);
					break;
				case 'ul':
				case 'ol':
					new ListWrapper(currentColumn,lastChild,columns.item(i+1),minHeight);
					break;
				case 'h1': //Keep headings with next paragraph
				case 'h2':
				case 'h3':
				case 'h4':
				case 'h5':
				case 'h6':
					new HeadingWrapper(lastChild,columns.item(i+1));
				default:
					//don't know what to do with this element. Let it stick out.
			}

			//add the 'read on' text.
			if (this.settings.readOnText!=null) {
				currentColumn.appendChild(this.readOnNode.cloneNode(true));
			}
		}
			
		//Stretch all columns to equal height
		var maxHeight=0;
		
		for (i=0;i<columns.length;i++) {
			maxHeight=Math.max(maxHeight,columns.item(i).offsetHeight);
		}			
		for (i=0;i<columns.length;i++) {
			columns.item(i).style.height=maxHeight+"px";
		}
	}
	
	//Initialisation starts here
	this.settings=settingsIn;
	
	if (this.settings.readOnText!=null) {					
		this.readOnNode=document.createElement('p');
		this.readOnNode.className="readOn";
		this.readOnNode.appendChild(document.createTextNode(this.settings.readOnText));
	}
	
	this.columnContainer=columnContainerIn;
	
	//If a screen class name is set, 
	if (this.settings.classNameScreen!=null) {
		//assign the classname.
		this.columnContainer.className=this.settings.classNameScreen;
	}
	
	//Store a copy of the original column	
	this.originalContent=columnContainerIn.cloneNode(true);
		
	//If a print class name is set, 
	if (this.settings.classNamePrint!=null) {
		//make a copy of the original node,
		var printNode=this.originalContent.cloneNode(true);
		//assign the classname
		printNode.className=this.settings.classNamePrint;
		//and insert it into the dom.
		this.columnContainer.parentNode.insertBefore(printNode,this.columnContainer);
	}	
	
	//Add this MultiColumn to the listener.
	if (MultiColumnList === null) {		
		MultiColumnList = new Array;
		if (window.addEventListener) {
			window.addEventListener('resize',multiColumnSetResizeTimer,false);		
		} else {
			window.attachEvent('onresize',multiColumnSetResizeTimer);
		}
	}
	
	MultiColumnList.push(this);
	
	//And do the magic!
	this.generateColumns();
}

//Regenerates the columns after a short delay after the user stopped resizing.
function multiColumnSetResizeTimer() {
	if (MultiColumnResizeTimer) {
		clearTimeout(MultiColumnResizeTimer);
	}
	MultiColumnResizeTimer=setTimeout(multiColumnResize,100);	
}
	
//Called when window is resized.
function multiColumnResize() {
	if (!window.addEventListener && window.attachEvent) { //Damned IE keeps fireing events when reflowing the text!
		window.detachEvent('onresize',multiColumnSetResizeTimer);
	}
	for (var i=0; i<MultiColumnList.length;i++) {
		var object = MultiColumnList[i];
		
		//Restore original situation
		var newCopy=object.originalContent.cloneNode(true);
		object.columnContainer.parentNode.replaceChild(newCopy,object.columnContainer);
		object.columnContainer=newCopy;
		
		//Regenerate columns
		object.generateColumns();
	}
	
	if (!window.addEventListener && window.attachEvent) { 
		setTimeout("window.attachEvent('onresize',multiColumnSetResizeTimer)",0);
	}
}

function ParapgraphWrapper(sourceColumnIn, sourceParagraphIn, destinationColumnIn, heightIn) {
	this.sourceColumn=sourceColumnIn;
	this.height=heightIn;
	
	/**
	* Recursively loops over given <source>, moving text from <source> to <dest>
	* until the column height is less or equal to the target height.
	*
	* Preconditions: <source> and <dest> are element nodes.
	*/
	this.processElement=function (source,dest) {
		var lastSourceChild;
		
		while (lastSourceChild=source.lastChild) {
			if (lastSourceChild.nodeType==1) {
				//Make a shalow clone copy of this element to the destination
				//node, to preserve styles and attributes.
				var newDest=lastSourceChild.cloneNode(false);
				if (dest.hasChildNodes()) {					
					dest.insertBefore(newDest,dest.firstChild);
				} else {
					dest.appendChild(newDest);
				}
				//recursively process this node.
				if (this.processElement(lastSourceChild,newDest)) {
					return true;
				}							
			} else if (lastSourceChild.nodeType==3) {
				//Wrap this text node..
				if (this.wrapTextNode(lastSourceChild,dest)) {
					//..and return when the target has been reached.
					return true;
				}
			}
			
			//This node has been cleaned out. Remove it.
			source.removeChild(lastSourceChild);
		} 
				
		return false;
	}
	
	/**
	* Cuts words at the end of <source>, until the column height
	* is less or equal to target-height.
	* Cut words are then placed into <dest>
	*
	* Preconditions: <source> is a text node. <dest> is an element node.
	*/
	this.wrapTextNode=function (source,dest) {	
		var sourceText=source.nodeValue;
		
		//Split the text at spaces.
		var sourceTextAray=sourceText.split(/\s/);
		var destTextArray=new Array();
		
		//Keep removing words form the source until the column fits.
		while (this.sourceColumn.offsetHeight>this.height && sourceTextAray.length>0) {
			destTextArray.push(sourceTextAray.pop());
			source.nodeValue=sourceTextAray.join(' ');
		}
		
		//Add spaces at the front and end, if there are spaces in the original.
		var newText=(/^\s/.test(sourceText)?' ':'') + (destTextArray.reverse().join(' ')) + (/\s$/.test(sourceText)?' ':'');
		
		//Put the text into the destination node in the next column.
		var newTextNode=document.createTextNode(newText);
		if (dest.hasChildNodes()) {					
			dest.insertBefore(newTextNode,dest.firstChild);
		} else {
			dest.appendChild(newTextNode);
		}
		
		//return true if the target has been reached.
		return this.sourceColumn.offsetHeight<=this.height;
	}

	//Duplicate the current paragraph by shallow copy
	if (destinationColumnIn.hasChildNodes()) {					
		destinationColumnIn.insertBefore(sourceParagraphIn.cloneNode(false),destinationColumnIn.firstChild);
	} else {
		destinationColumnIn.appendChild(sourceParagraphIn.cloneNode(false));
	}
	
	this.processElement(sourceParagraphIn,destinationColumnIn.firstChild);
}

function ListWrapper(sourceColumnIn, sourceListIn, destinationColumnIn, heightIn) {
	//Duplicate the current paragraph by shallow copy
	var newList=sourceListIn.cloneNode(false);
	if (destinationColumnIn.hasChildNodes()) {					
		destinationColumnIn.insertBefore(newList,destinationColumnIn.firstChild);
	} else {
		destinationColumnIn.appendChild(newList);
	}
	
	//Loop over all elements in this list.
	while (currentElement=sourceListIn.lastChild) {
		if (sourceColumnIn.offsetHeight<=heightIn) {
			break;
		}
		
		if (currentElement.nodeName.toLowerCase()=='li') {
			if (newList.hasChildNodes()) {				
				newList.insertBefore(currentElement,newList.firstChild);
			} else {
				newList.appendChild(currentElement);
			}
		} else {		
			//remove the last element.
			sourceListIn.removeChild(currentElement);
		}
	}
		
	//Count current number of items.
	var numItems=1;
	var elementList=sourceListIn.childNodes;
	//count remaining items.
	for (var i=0;i<elementList.length;i++) {
		if (elementList[i].nodeName.toLowerCase()=='li') {
			numItems++;
		}
	}
	
	newList.start=numItems;
	
	return;
}

function HeadingWrapper(sourceHeadingIn, destinationColumnIn) {
	if (destinationColumnIn.hasChildNodes()) {				
		destinationColumnIn.insertBefore(sourceHeadingIn,destinationColumnIn.firstChild);
	} else {
		destinationColumnIn.appendChild(sourceHeadingIn);
	}
}
