﻿MultiColumnResizeTimer = null;
MultiColumnList = null;

function MultiColumnSettings() {
    this.extraHeight = 10; 	//Add extra height to a column. Increase this if the last column 'sticks out' too much.
    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 = 2; 	//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;
            var nextColumn = columns.item(i + 1);
            switch (lastChild.nodeName.toLowerCase()) {
                case 'p':
                    new ParapgraphWrapper(currentColumn, lastChild, nextColumn, minHeight);
                    break;
                case 'ul':
                case 'ol':
                    new ListWrapper(currentColumn, lastChild, nextColumn, minHeight);
                    break;
                default:
                    //don't know what to do with this element. Let it stick out.
            }

            //Move headings at the bottom to next column. (this implies a proper usage of headings!)
            new HeadingWrapper(currentColumn, nextColumn);

            //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);
                dest.insertBefore(newDest, dest.firstChild);
                //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.
        dest.insertBefore(document.createTextNode(newText), dest.firstChild);

        //return true if the target has been reached.
        return this.sourceColumn.offsetHeight <= this.height;
    }

    //Duplicate the current paragraph by shallow copy
    destinationColumnIn.insertBefore(sourceParagraphIn.cloneNode(false), destinationColumnIn.firstChild);

    this.processElement(sourceParagraphIn, destinationColumnIn.firstChild);

    //check if the origal paragraph is emtpy
    if (sourceParagraphIn.offsetHeight == 0) { //Check to see if normalized text is "" would be better..
        //Yes it's empty. Remove the empty paragraph.
        this.sourceColumn.removeChild(sourceParagraphIn);
    }
}

function ListWrapper(sourceColumnIn, sourceListIn, destinationColumnIn, heightIn) {
    //Duplicate the current paragraph by shallow copy
    var newList = sourceListIn.cloneNode(false);

    destinationColumnIn.insertBefore(newList, destinationColumnIn.firstChild);

    //Loop over all elements in this list.
    while (currentElement = sourceListIn.lastChild) {
        if (sourceColumnIn.offsetHeight <= heightIn) {
            break;
        }

        if (currentElement.nodeName.toLowerCase() == 'li') {
            newList.insertBefore(currentElement, newList.firstChild);
        } 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;

    //check if the origal list is emtpy
    if (sourceListIn.offsetHeight == 0) { //Check to see if normalized text is "" would be better..
        //Yes it's empty. Remove the empty list.
        sourceColumnIn.removeChild(sourceListIn);
    }
}

function HeadingWrapper(currentColumn, nextColumn) {
    //Wrap a heading, if there was one. 
    if (/^h[1-6]$/i.test(currentColumn.lastChild.nodeName)) {
        nextColumn.insertBefore(currentColumn.lastChild, nextColumn.firstChild);
    }
}