﻿jQuery.cookie = function(name, value, options) {
    if (typeof value != 'undefined') { // name and value given, set cookie
        options = options || {};
        if (value === null) { value = ''; options.expires = -1; }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; }
            expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
        }
        var path = options.path ? '; path=' + options.path : '';
        var domain = options.domain ? '; domain=' + options.domain : '';
        var secure = options.secure ? '; secure' : '';
        document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
    } else { // only name given, get cookie
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; }
            }
        }
        return cookieValue;
    }
};

; (function($) {

    $.extend($.fn, {
        swapClass: function(c1, c2) { var c1Elements = this.filter('.' + c1); this.filter('.' + c2).removeClass(c2).addClass(c1); c1Elements.removeClass(c1).addClass(c2); return this; },
        replaceClass: function(c1, c2) { return this.filter('.' + c1).removeClass(c1).addClass(c2).end(); },
        hoverClass: function(className) { className = className || "hover"; return this.hover(function() { $(this).addClass(className); }, function() { $(this).removeClass(className); }); },
        heightToggle: function(animated, callback) { animated ? this.animate({ height: "toggle" }, animated, callback) : this.each(function() { jQuery(this)[jQuery(this).is(":hidden") ? "show" : "hide"](); if (callback) callback.apply(this, arguments); }); },
        heightHide: function(animated, callback) { if (animated) { this.animate({ height: "hide" }, animated, callback); } else { this.hide(); if (callback) this.each(callback); } },
        prepareBranches: function(settings) {
            if (!settings.prerendered) {
                // mark last tree items
                this.filter(":last-child:not(ul)").addClass(CLASSES.last);
                this.filter(":has(>ul:visible)").addClass(CLASSES.open);
                // collapse whole tree, or only those marked as closed, anyway except those marked as open
                this.filter((settings.collapsed ? "" : "." + CLASSES.closed) + ":not(." + CLASSES.open + ")").find(">ul").hide();
            }
            // return all items with sublists
            return this.filter(":has(>ul)");
        },
        applyClasses: function(settings, toggler) {
            this.filter(":has(>ul):not(:has(>a))").find(">span").click(function(event) {
                toggler.apply($(this).next());
            }).add($("a", this)).hoverClass();

            if (!settings.prerendered) {
                // handle closed ones first
                this.filter(":has(>ul:hidden)")
						.addClass(CLASSES.expandable)
						.replaceClass(CLASSES.last, CLASSES.lastExpandable);

                // handle open ones
                this.filter(":not(:has(>ul:hidden))")
						.addClass(CLASSES.collapsable)
						.replaceClass(CLASSES.last, CLASSES.lastCollapsable);

                // create hitarea
                this.prepend("<div class=\"" + CLASSES.hitarea + "\"/>").find("div." + CLASSES.hitarea).each(function() {
                    var classes = "";
                    $.each($(this).parent().attr("class").split(" "), function() { classes += this + "-hitarea "; });
                    $(this).addClass(classes);
                });
            }

            // apply event to hitarea
            this.find("div." + CLASSES.hitarea).click(toggler);
        },
        treeview: function(settings) {

            settings = $.extend({ cookieId: "treeview" }, settings);

            if (settings.add) { return this.trigger("add", [settings.add]); }

            if (settings.toggle) { var callback = settings.toggle; settings.toggle = function() { return callback.apply($(this).parent()[0], arguments); }; }

            // factory for treecontroller
            function treeController(tree, control) {
                // factory for click handlers
                function handler(filter) {
                    return function() {
                        // reuse toggle event handler, applying the elements to toggle
                        // start searching for all hitareas
                        toggler.apply($("div." + CLASSES.hitarea, tree).filter(function() {
                            // for plain toggle, no filter is provided, otherwise we need to check the parent element
                            return filter ? $(this).parent("." + filter).length : true;
                        }));
                        return false;
                    };
                }
                // click on first element to collapse tree
                $("a:eq(0)", control).click(handler(CLASSES.collapsable));
                // click on second to expand tree
                $("a:eq(1)", control).click(handler(CLASSES.expandable));
                // click on third to toggle tree
                $("a:eq(2)", control).click(handler());
            }

            // handle toggle event
            function toggler() {
                $(this)
					.parent()
                // swap classes for hitarea
					.find(">.hitarea")
						.swapClass(CLASSES.collapsableHitarea, CLASSES.expandableHitarea)
						.swapClass(CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea)
					.end()
                // swap classes for parent li
					.swapClass(CLASSES.collapsable, CLASSES.expandable)
					.swapClass(CLASSES.lastCollapsable, CLASSES.lastExpandable)
                // find child lists
					.find(">ul")
                // toggle them
					.heightToggle(settings.animated, settings.toggle);
                if (settings.unique) {
                    $(this).parent()
						.siblings()
                    // swap classes for hitarea
						.find(">.hitarea")
							.replaceClass(CLASSES.collapsableHitarea, CLASSES.expandableHitarea)
							.replaceClass(CLASSES.lastCollapsableHitarea, CLASSES.lastExpandableHitarea)
						.end()
						.replaceClass(CLASSES.collapsable, CLASSES.expandable)
						.replaceClass(CLASSES.lastCollapsable, CLASSES.lastExpandable)
						.find(">ul")
						.heightHide(settings.animated, settings.toggle);
                }
            }

            function serialize() {
                function binary(arg) { return arg ? 1 : 0; }
                var data = [];
                branches.each(function(i, e) { data[i] = $(e).is(":has(>ul:visible)") ? 1 : 0; });
                $.cookie(settings.cookieId, data.join(""));
            }

            function deserialize() {
                var stored = $.cookie(settings.cookieId);
                if (stored) {
                    var data = stored.split("");
                    branches.each(function(i, e) { $(e).find(">ul")[parseInt(data[i]) ? "show" : "hide"](); });
                }
            }

            // add treeview class to activate styles
            this.addClass("treeview");

            // prepare branches and find all tree items with child lists
            var branches = this.find("li").prepareBranches(settings);

            switch (settings.persist) {
                case "cookie":
                    var toggleCallback = settings.toggle;
                    settings.toggle = function() { serialize(); if (toggleCallback) { toggleCallback.apply(this, arguments); } };
                    deserialize();
                    break;
                case "location":
                    var current = this.find("a").filter(function() { return this.href.toLowerCase() == location.href.toLowerCase(); });
                    if (current.length) { this.filter(":not(:has(>ul:hidden))").addClass("open").show(); }
                    break;
            }

            branches.applyClasses(settings, toggler);

            // if control option is set, create the treecontroller and show it
            if (settings.control) { treeController(this, settings.control); $(settings.control).show(); }

            return this.bind("add", function(event, branches) {
                $(branches).prev()
					.removeClass(CLASSES.last)
					.removeClass(CLASSES.lastCollapsable)
					.removeClass(CLASSES.lastExpandable)
				.find(">.hitarea")
					.removeClass(CLASSES.lastCollapsableHitarea)
					.removeClass(CLASSES.lastExpandableHitarea);
                $(branches).find("li").andSelf().prepareBranches(settings).applyClasses(settings, toggler);
            });
        }
    });

    // classes used by the plugin
    // need to be styled via external stylesheet, see first example
    var CLASSES = $.fn.treeview.classes = {
        open: "open", closed: "closed", expandable: "expandable", expandableHitarea: "expandable-hitarea", lastExpandableHitarea: "lastExpandable-hitarea", collapsable: "collapsable",
        collapsableHitarea: "collapsable-hitarea", lastCollapsableHitarea: "lastCollapsable-hitarea", lastCollapsable: "lastCollapsable", lastExpandable: "lastExpandable", last: "last", hitarea: "hitarea"
    };

    // provide backwards compability
    $.fn.Treeview = $.fn.treeview;

})(jQuery);