﻿//Applies multicolumn ability to a specified element
//Author: Brian R Miedlar (c) 2006
// *Modified from Codex, version 0.1

// define some node types
var TEXT_NODE = 3;
var COMMENT_NODE = 8;

var MultiColumn = Class.create();
MultiColumn.IdSeed = 0;
MultiColumn.ActiveBooks = [];
MultiColumn.GetGuid = function() { 
    MultiColumn.IdSeed++;
    return 'MC_' + MultiColumn.IdSeed; 
}
MultiColumn.SplitableTags = ['P','DIV', 'SPAN', 'BLOCKQUOTE','ADDRESS','PRE', 'A', 'EM', 'I', 'STRONG', 'B', 'CITE', 'OL', 'UL', 'LI'];
MultiColumn.prototype = {
    initialize: function(element, observer) {
        this.element = $(element);
        this.book = null;
        this.columns = 1;
        this.optimalHeight = 0;
        this.optimalWidth = 0;
        this.observer = observer; //for callbacks
        this.multiPageSets = true;
        this.showNavigation = true;
        this.navPrev = null;
        this.navNext = null;
        this.navIndicator = null;
        this.navPage1 = null;
        this.navPage2 = null;
        this.pageSets = []; //hashtable (key=pageset1,pageset2, etc)
        this.pages = [];    //hashtable (key=page1,page2, etc)

        // keep track of the number of pages made
        this.pagecount = 0;

        // keep track of the number of pagesets
        this.setcount = 0;

        // keep track of the current pageset being displayed
        this.currentset = 1;
        this.bookIndex = MultiColumn.ActiveBooks.length;
        MultiColumn.ActiveBooks.push(this);
    },
    update: function(columns, multiPageSets, showNavigation) {

        this.columns = columns;
        this.multiPageSets = multiPageSets;
        this.showNavigation = showNavigation;

        //compute optimal height if no multi page sets
        if (!this.multiPageSets) {
            Element.show(this.element);

            var iHeight = Element.getHeight(this.element);
            var iWidth = Element.getWidth(this.element);
            Element.hide(this.element);
            //iHeight = iHeight; //* this.columns;
            this.optimalHeight = iHeight;
            this.optimalWidth = (iWidth / columns) - (10 * this.columns);
        }

        // save its child nodes
        var source_nodes = this.element.childNodes;

        // make the book container
        this.book = document.createElement("div"); // create the book
        this.book.id = MultiColumn.GetGuid(); 						// give it the book id

        // put the book in the document, where the content source currently is
        this.element.parentNode.insertBefore(this.book, this.element);

        // make the first page
        var page = this.makePage();

        // keep track of the hierarchy in an array
        var parents = [];

        // keep track of the number of <li>'s for numbering <ol>'s that break across pages
        var ol_li_count = 0;

        // recursive function to place all childnodes in pages
        // returns the parent of the node that was placed
        function placeNodes(node, parent, caller) {
            if (node.hasChildNodes()) {
                // if this node is an <ol>, reset the number of <li>'s 
                if (node.nodeName == "OL") ol_li_count = 0;

                // if this node is an <li>, keep track for <ol> numbering purposes
                if (node.nodeName == "LI") ol_li_count++;

                // place an empty clone of the node
                var node_clone = null;
                try {
                    node_clone = parent.appendChild(node.cloneNode(false));
                } catch (e) { return; }


                // if the empty clone causes the page to overflow (like the bullet of an empty <li>),
                // delete the node, make a new page, add the clone to the new page
                if (MultiColumn.IsOverflow(page)) {
                    // delete the cloned node
                    parent.removeChild(node_clone);

                    // make a new page
                    if (!caller.multiPageSets && caller.pagecount >= caller.columns) return;
                    page = caller.makePage();

                    // recreate the heirarchy on the new page
                    var clone_parent = caller.makeHierarchy(page, parents, ol_li_count);

                    // make the clone again on the new page
                    node_clone = clone_parent.appendChild(node.cloneNode(false));
                }

                // keep track of the heirarchy
                parents.push(node);
                // recurse through the children
                for (var j = 0; j < node.childNodes.length; j++) {
                    node_clone = placeNodes(node.childNodes[j], node_clone, caller);

                    // nodeClode gets updated above based on whether the placement of a child node
                    // causes a new page to be made (and thus a new cloned node to place nodes in)
                }

                // keep track of the heirarchy
                parents.pop(node);

                if (!node_clone) return;
                return node_clone.parentNode;
            }

            // if there are no children, this is a text node (or image, etc.)
            else {
                // keep track of the parent node of the placed clone node
                var clone_parent = parent;

                // if this is a text node
                if (node.nodeType == TEXT_NODE) {
                    // add the text node to the page
                    var text_node_clone = MultiColumn.AddNode(parent, node);

                    // if the text node made the page overflow
                    if (MultiColumn.IsOverflow(page)) {
                        // clear the text node that was just made
                        text_node_clone.nodeValue = "";

                        // add words until it's too long
                        var text = node.nodeValue; // the text that wouldn't fit
                        var next_word = ""; 		// the next word to be added			

                        // keep adding words and making new pages (if necessary) until there is no more text
                        while (MultiColumn.IsText(text)) {
                            while (!MultiColumn.IsOverflow(page) && MultiColumn.IsText(text)) {
                                // get the next word from the text
                                next_word = text.match(/\s*\S+\s*/);
                                next_word = next_word.toString();

                                // add it to the page
                                text_node_clone.nodeValue = text_node_clone.nodeValue + next_word;

                                // delete it from the text 
                                text = text.substr(text.indexOf(next_word) + next_word.length);
                            }

                            // if the last word caused the page to overflow, 
                            // delete it from the page, and add it back to the text
                            if (MultiColumn.IsOverflow(page)) {
                                // delete it from the page
                                text_node_clone.nodeValue = text_node_clone.nodeValue.substring(0, text_node_clone.nodeValue.lastIndexOf(next_word));

                                // add it back to the text
                                text = next_word + text;

                                // if the page now contains an empty node
                                var empty_node = null;
                                if (text_node_clone.nodeValue == "") {
                                    // delete the last node and save it for later
                                    empty_node = parent.parentNode.removeChild(parent);

                                    // pop the node off the parents array
                                    parents.pop();
                                }
                            }

                            // if there is still text left, make a new page
                            if (MultiColumn.IsText(text)) {
                                // make a new page
                                if (!caller.multiPageSets && caller.pagecount >= caller.columns) return;
                                page = caller.makePage();

                                // recreate the heirarchy on the new page and add the blank text node
                                clone_parent = caller.makeHierarchy(page, parents, ol_li_count);

                                // if the last page ended in an empty node, add it to the heirarchy
                                if (empty_node != null) {
                                    // add it to the end of the heirarchy
                                    clone_parent = MultiColumn.AddNode(clone_parent, empty_node);

                                    // push it onto the parents array
                                    parents.push(empty_node);
                                }

                                text_node_clone = MultiColumn.AddNode(clone_parent, node); // add the text node to the heirarhcy
                                text_node_clone.nodeValue = ""; 			// clear the text node				
                            }
                        } // endwhile
                    } // end if (page overflow)
                } // end if(text node)

                // if this is not a text node 
                else {
                    // if the node is <hr class="newpage">
                    if (node.nodeName == "HR" && node.className == "newpage") {
                        // make a new page
                        if (!caller.multiPageSets && caller.pagecount >= caller.columns) return;
                        page = caller.makePage();

                        // recreate the heirarchy on the new page
                        clone_parent = caller.makeHierarchy(page, parents, ol_li_count);
                    }

                    // add the node to the page
                    var leaf_node_clone = MultiColumn.AddNode(clone_parent, node);

                    // if the node made the page overflow
                    if (MultiColumn.IsOverflow(page)) {
                        // remove the node
                        parent.removeChild(leaf_node_clone);

                        // make a new page
                        if (!caller.multiPageSets && caller.pagecount >= caller.columns) return;
                        page = caller.makePage();

                        // recreate the heirarchy on the new page and add the node
                        clone_parent = caller.makeHierarchy(page, parents, ol_li_count);
                        leaf_node_clone = MultiColumn.AddNode(clone_parent, node); // add the node to the heirarhcy

                        // if the node was too tall for the new page, shrink it
                        if (MultiColumn.IsOverflow(page)) {
                            var aspect_ratio = leaf_node_clone.width / leaf_node_clone.height;

                            leaf_node_clone.height = pageHeight(page)
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "margin-top"))
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "margin-bottom"))
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "padding-top"))
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "padding-bottom"))
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "border-top"))
											        - MultiColumn.ToNumber(Element.getStyle(leaf_node_clone, "border-bottom"));

                            leaf_node_clone.width = leaf_node_clone.height * aspect_ratio;
                        }
                    }
                }

                // if a new page was made, notify the calling function that the parent node has changed
                // otherwise, return the original parent
                return clone_parent;

            } // endelse (no children)
        } // end function

        // put all of the childnodes in pages (recurse through children of children)
        for (var i = 0; i < source_nodes.length; i++) {
            placeNodes(source_nodes[i], page, this);
        }

        // remove the book source from the document
        this.element.parentNode.removeChild(this.element);

        // hide all but the first pages
        for (var k = 2; k <= this.setcount; k++) {
            var eSet = this.pageSets['pageset' + k];
            if (!eSet) return;
            Element.hide(eSet);
        }

        // make the navigation 
        this.makeNav();
        if (this.observer) this.observer.MulticolumnUpdated(this);

    },
    makePage: function() {
        // if this is an even numbered page (0, 2, 4, ...), make a new pageset
        if (this.pagecount % this.columns == 0) this.pageset = this.makePageSet();

        var page = document.createElement("div"); // create the page
        Element.addClassName(page, 'bookpage'); 	// give it the bookpage class
        Element.addClassName(page, 'bookpage-' + (this.pagecount + 1)); // set the class based on the number
        page.style.overflow = "hidden"; 			// hide overflow so IE height stays defined
        if (this.optimalHeight > 0) page.style.height = this.optimalHeight + 'px';
        if (this.optimalWidth > 0) page.style.width = this.optimalWidth + 'px';

        // put the page in the current set
        this.pageset.appendChild(page);

        // increment the page counter
        this.pagecount++;
        this.pages['page' + this.pagecount] = page;

        return page;
    },
    // makes a pageset of the book, which contains two pages
    // returns the pageset element
    makePageSet: function() {
        var set = document.createElement("div"); // create the set
        Element.addClassName(set, 'pageset');   // give it the pageset class
        Element.addClassName(set, 'pageset-' + (this.setcount + 1)); // set the id based on the number

        // put the set in the book
        this.book.appendChild(set);

        // increment the set counter
        this.setcount++;
        this.pageSets['pageset' + this.setcount] = set;

        return set;
    },
    // recreates the hierarchy of the last filled page on a newly created page
    // returns the last node in the hierarchy
    makeHierarchy: function(page, parents, ol_li_count) {
        var clone_parent = page;
        for (var z = 0; z < parents.length; z++) {
            clone_parent = clone_parent.appendChild(parents[z].cloneNode(false));

            // if a node with an ID was just placed, append the pagenumber to the ID 
            // to avoid having multiple nodes with the same ID in the DOM tree
            if (clone_parent.id != "") {
                clone_parent.id += "-" + this.pagecount;
            }

            // if an <ol> was just placed set the start attribute such that numbering will continue where it left off
            if (clone_parent.nodeName == "OL") clone_parent.setAttribute("start", ol_li_count);

            // if an <li> was just placed, give it the class "li-continued" so that it can be styled
            // as a continuation from the previous page
            if (clone_parent.nodeName == "LI") clone_parent.className = "li-continued " + clone_parent.className;
        }

        return clone_parent;
    },

    // make the navigation 
    makeNav: function() {
        if (!this.showNavigation) return;

        // make the navigation container
        var container = document.createElement("div");
        Element.addClassName(container, 'booknav'); // give it the id "booknav"

        // make the previous link
        var prev = document.createElement("a");
        Element.addClassName(prev, 'booknav-prev');
        prev.appendChild(document.createTextNode("Previous Page"));
        prev.onclick = function() {
            MultiColumn.ActiveBooks[this.bookIndex].prevPage();
        } .bind(this);
        this.navPrev = prev;

        // make the next link
        var next = document.createElement("a");
        Element.addClassName(next, 'booknav-next');
        next.appendChild(document.createTextNode("Next Page"));
        next.onclick = function() {
            MultiColumn.ActiveBooks[this.bookIndex].nextPage();
        } .bind(this);
        this.navNext = next;

        // make the current pageset indicator
        var indicator = document.createElement("div");
        Element.addClassName(indicator, 'booknav-current');
        indicator.appendChild(document.createTextNode(this.currentset + " of " + this.setcount));
        this.navIndicator = indicator;

        // make the first page number
        var page1 = document.createElement("span");
        Element.addClassName(page1, 'booknav-pagenumber');
        Element.addClassName(page1, 'booknav-page1');
        page1.appendChild(document.createTextNode("Page"));
        this.navPage1 = page1;

        // make the second page number
        var page2 = document.createElement("span");
        Element.addClassName(page2, 'booknav-pagenumber');
        Element.addClassName(page2, 'booknav-page2');
        page2.appendChild(document.createTextNode("Page"));
        this.navPage2 = page2;

        /*
        // make the page numbers container
        var pagenums = document.createElement("div");
        pagenums.id = "booknav-pagenumbers";

	    // add the page numbers to the page numbers container
        pagenums.appendChild(page1);
        pagenums.appendChild(document.createTextNode(" "));
        pagenums.appendChild(page2);
        */

        // add the components to the container
        container.appendChild(indicator); // add indicator
        container.appendChild(document.createTextNode(" "));
        container.appendChild(page1); 	// add page 1
        container.appendChild(document.createTextNode(" "));
        container.appendChild(page2); 	// add page 2
        container.appendChild(document.createTextNode(" "));
        container.appendChild(prev); 	// add previous link 
        container.appendChild(document.createTextNode(" "));
        container.appendChild(next); 	// add next link 

        // add the container to the end of the book
        this.book.appendChild(container);

        // update the page navigation
        this.updateNav();
    },

    // show the next page
    nextPage: function() {
        if (this.currentset < this.setcount) {
            // get the current and next pagesets
            var current = $(this.pageSets['pageset' + this.currentset]);
            var next = $(this.pageSets['pageset' + (this.currentset + 1)]);

            // show the next, hide the current
            next.style.display = current.style.display;
            current.style.display = "none";

            // update the current set
            this.currentset++;

            // update the page navigation
            this.updateNav();
        }
        if (this.observer) this.observer.MulticolumnUpdated(this);
    },

    // show the previous page
    prevPage: function() {
        if (this.currentset > 1) {
            // get the current and previous pagesets
            var current = $(this.pageSets['pageset' + this.currentset]);
            var prev = $(this.pageSets['pageset' + (this.currentset - 1)]);

            // show the next, hide the current
            prev.style.display = current.style.display;
            current.style.display = "none";

            // update the current set
            this.currentset--;

            // update the page navigation
            this.updateNav();
        }
        if (this.observer) this.observer.MulticolumnUpdated(this);
    },

    // if there are no "next" or "previous" pages, hide the link 
    // update the current page indicator
    updateNav: function() {
        // if there are no more next pages
        if (this.currentset == this.setcount) {
            // hide the next link
            Element.hide(this.navNext);
        }
        else {
            Element.show(this.navNext);
        }

        // if there are no more previous pages
        if (this.currentset == 1) {
            // hide the previous link
            Element.hide(this.navPrev);
        }
        else {
            Element.show(this.navPrev);
        }

        // update the current page indicator
        this.updateIndicator();

        // update the page numbers
        this.updatePageNums();
    },

    // update the current page indicator
    updateIndicator: function() {
        var indicator = this.navIndicator;
        Element.update(indicator, (this.currentset + " of " + this.setcount));
        //indicator.firstChild.nodeValue = (this.currentset + " of " + this.setcount);
    },

    // update the page numbers
    updatePageNums: function() {
        // get page numbers spans
        var page1 = this.navPage1;
        var page2 = this.navPage2;

        // calculate page numbers
        var num1 = "Page " + (((this.currentset - 1) * 2) + 1) + " of " + this.pagecount;
        var num2 = ((this.currentset - 1) * 2) + 2;

        // if there is only content on the first page of the pageset
        if (num2 > this.pagecount) {
            num2 = "";
        }
        else {
            num2 = "Page " + num2 + " of " + this.pagecount;
        }

        // put page numbers in spans	
        page1.firstChild.nodeValue = num1;
        page2.firstChild.nodeValue = num2;
    }
}

// Add a node to a parent
// Does not add empty text nodes	
MultiColumn.AddNode = function (parent, node) {
    // if it's not a text node, add it
    if(node.nodeType != TEXT_NODE) {
	    return parent.appendChild(node.cloneNode(true));
    }
    // if it is a text node that has some content, add it
    else if(node.nodeValue.search(/\S/i) != -1) {
	    return parent.appendChild(node.cloneNode(true));
    }
},

// return true if the page has overflown, false otherwise
MultiColumn.IsOverflow = function(page) {
	return MultiColumn.GetChildBottom(page) > MultiColumn.GetPageBottom(page);
}

// returns the distance from the top of the body to the bottom of the last child
MultiColumn.GetChildBottom = function(page) {
	if(!page.lastChild)
		return 0;
	else
		var child = page.lastChild;
		
	if(child.offsetParent == document.body) {
		return child.offsetHeight+child.offsetTop;
	}
	else {
		return child.offsetHeight+child.offsetTop+MultiColumn.GetTotalOffset(child.offsetParent);
	}
}

// returns the distance from the top of the body to the bottom of the content of the page
MultiColumn.GetPageBottom = function(page) {
	var paddingB = 0;	// bottom padding of the page
	var borderB = 0;	// bottom border of the page
	
	paddingB = Element.getStyle(page, "padding-bottom");
	borderB = Element.getStyle(page, "border-bottom");
	
	// strip the "px" from the padding and border values
	paddingB = MultiColumn.ToNumber(paddingB);	
	borderB = MultiColumn.ToNumber(borderB);
		
	if(page.offsetParent == document.body)
		return page.offsetHeight+page.offsetTop-paddingB-borderB;
	else
		return page.offsetHeight+page.offsetTop+MultiColumn.GetTotalOffset(page.offsetParent)-paddingB-borderB;
}

// returns the offset from the top of the screen to the top of element
// no matter how many offsetParents are in between
MultiColumn.GetTotalOffset = function(element) {
	if(element == undefined) {
		return 0;
	}
	if(element.offsetParent == null) {
		return 0;
	}
	
	if(element.offsetParent == document.body) {
		return element.offsetTop;
	}
	else {
		return element.offsetTop+MultiColumn.GetTotalOffset(element.offsetParent);
	}
}

// returns the height of the page
MultiColumn.GetPageHeight = function(page) {
	var paddingB = 0;	// bottom padding of the page
	var borderB = 0;	// bottom border of the page
	var paddingT = 0;	// top padding of the page
	var borderT = 0;	// top border of the page
	
	paddingB = Element.getStyle(page, "padding-bottom");
	borderB = Element.getStyle(page, "border-bottom");
	paddingT = Element.getStyle(page, "padding-top");
	borderT = Element.getStyle(page, "border-top");
	
	// strip the "px" from the padding and border values
	paddingB = MultiColumn.ToNumber(paddingB);
	borderB = MultiColumn.ToNumber(borderB);
	paddingT = MultiColumn.ToNumber(paddingT);
	borderT = MultiColumn.ToNumber(borderT);
	
	return page.offsetHeight-paddingB-borderB-paddingT-borderT;
}

// converts a numeric style value (e.g. "15px") to a number (e.g. 15)
MultiColumn.ToNumber = function(value) {
	if(value)
		return (+value.substring(0,value.search(/\D/)));
	else
		return 0;
}
MultiColumn.IsText = function(text) {
    return text.length && (text.search(/\S/) != -1);
}