(function($){
    // Sets both the keyup and change event handler function for the matched elements
    $.fn.apTextChange = function(keyUpFn, changeFn) {
        changeFn = changeFn || keyUpFn;
        if (keyUpFn instanceof Function && changeFn instanceof Function) {
            $(this).keyup(keyUpFn).change(changeFn);
        }
        return this;
    };
    // Empty a select box. If length is passed that many options will be left
    $.fn.emptySelect = function(length) {
        length = length || 0;
        return this.each(function(){
            if (this.tagName == 'SELECT') this.options.length = length;
        });
    };
    // Empty and then fill a select box. If length is passed that many options will be left before the new ones are added.
    $.fn.loadSelect = function(options, length) {
        return this.emptySelect(length).each(function(){
            if (this.tagName == 'SELECT') {
                var select = this;
                $.each(options, function (value, caption) {
                    var option = new Option(caption, value);
                    if ($.browser.msie) {
                        select.add(option);
                    } else {
                        select.add(option, null);
                    }
                });
            }
        });
    };
    $.fn.addSelectOption = function(value, caption) {
        return this.each(function(){
            var option = new Option(caption, value);
            if ($.browser.msie) {
                this.add(option);
            } else {
                this.add(option, null);
            }
        });
    };
    $(document).ready(function(){
        ap.init();
    });

    var ap;
    var apGlobal = this;
    ap = window.ap = {
        /**
        * Holds the height and width of the window
        */
        window: {height: 0, width: 0},
        document: {height: 0, width: 0},
        messageContainer: null,

        init: function()
        {
            this.ui.init();
            this.getWindowSize();
            this.app.init();
            if ($.validator) {
                $.validator.messages.required = 'Required';
            }
            this.myAccount.init();
            this.myApps.init();
            if ($.browser.msie === true && $.browser.version < 7) {
                $('body').prepend('<div id="ap-ie">This website is designed for modern browsers. You are using Internet Explorer 6, a browser that is over 8 years old.  Please consider <a href="http://www.microsoft.com/windows/internet-explorer" target="_blank">upgrading your version of Internet Explorer</a> or using another browser such as <a href="http://www.getfirefox.com" target="_blank">Firefox</a>.</div>');
                return;
            }
            $(window).resize(function() {
                ap.windowResize();
            });
            this.externalLinks();
        },
        focus: function(el)
        {
            $(el).focus();
        },
        loadFCKEditor: function(instanceName, width, height, toolbarSet, value)
        {
            if (typeof FCKeditor == 'undefined') {
                $.ajax({
                    async: false,
                    type: 'GET',
                    url: apJsBasePath + 'fckeditor/fckeditor.js',
                    dataType: 'script'
                });
            }
            if (height == undefined) {
                height = '500';
            }
            if (toolbarSet == undefined) {
                if (fckEditorToolBar != undefined) {
                    toolbarSet = fckEditorToolBar;
                } else {
                    toolbarSet = 'ncDefault';
                }
            }
            var editor = new FCKeditor(instanceName, width, height, toolbarSet, value);
            editor.BasePath = apJsBasePath + 'fckeditor/' ;
            editor.DisplayErrors = false;
            editor.Config['CustomConfigurationsPath'] = apJsBasePath + 'fckeditor/nc-fckconfig.js';
            return editor;
        },
        loadJsFile: function(file)
        {
            $('<script></script>').attr('src', file).attr('type', 'text/javascript').appendTo('head');
        },
        saveUserConfig: function(group, configName, configValue)
        {
            var data = 'group=' + group + '&name=' + configName + '&value=' + configValue;
            $.post(apAdminBaseUrl + 'user/account/saveConfig', data);
        },
        /**
        * Sets links with a rel attribute of "external" to open in a new window.
        * Adapted from Sitepoint.com (http://www.sitepoint.com/article/standards-compliant-world)
        */
        externalLinks: function() {
            $('a').filter(function() {
                return (!$(this).next().hasClass('ap-external') && ((this.href && this.href.substr(0, 7) != 'mailto:' && this.href.substr(0, 1) != '#' && this.href.substr(0, 10) != 'javascript') && (this.hostname != undefined && this.hostname.split(':')[0] != location.hostname) || (this.rel == 'external') || (this.rel == 'externalSkip')));
            })
            .attr('target', '_blank')
            .filter(function() {
                return this.rev != 'externalSkip' && this.rel != 'externalSkip';
            })
            .after(' <span class="ap-external"></span>');
        },
        /**
        * Helper function to get the correct window height and width
        */
        getWindowSize: function() {
            if ($.browser.msie && $.browser.version < 7) {
                // Height
                var scrollHeight = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
                var offsetHeight = Math.max(document.documentElement.offsetHeight, document.body.offsetHeight);

                if (scrollHeight < offsetHeight) {
                    this.document.height = $(window).height();
                } else {
                    this.document.height = scrollHeight;
                }

                // Width
                var scrollWidth = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
                var offsetWidth = Math.max(document.documentElement.offsetWidth, document.body.offsetWidth);

                if (scrollWidth < offsetWidth) {
                    this.document.width = $(window).width();
                } else {
                    this.document.width = scrollWidth;
                }
                // handle "good" browsers
            } else {
                this.document.height = $(document).height();
                this.document.width = $(document).width();
            }

            this.window.height = $(window).height();
            this.window.width= $(window).width();
        },
        /**
        * Debuging helper function.
        * Will send a message to the console (Firebug) if it exists. If not,
        * Then it'll do a simple alert box.
        */
        debug: function() {
            if (window.loadFirebugConsole) {
                window.loadFirebugConsole();
            }
            if (window.console && window.console.log && $.browser.safari != true) {
            	window.console.log('[debug] ' + Array.prototype.join.call(arguments,''));
            } else {
                alert('[debug] ' + Array.prototype.join.call(arguments,''));
            }
        },
        debugArray: function(array, levels, indent, level) {
        	if (level == undefined) {
        		level = 1;
        	} else {
        		level ++;
        	}
        	if (levels != undefined && level != undefined && levels < level) {
        		return;
        	}
        	if (typeof array == 'object' || $.isArray(array)) {
	        	$.each(array, function(i, v){
	        		ap.debug(indent, i, ': ', v);
	        		if (typeof v == 'object' || $.isArray(v)) {
	        			if (indent != undefined) {
	        				indent += '----';
	        			} else {
	        				indent = '----';
	        			}
	        			ap.debugArray(v, levels, indent, level);
	        		}
	        	});
        	}
        },
        windowResize: function()
        {
            ap.getWindowSize();
            ap.myApps.setHeight();
        },
        getMaxZindex: function() {
            var max = 0;
            $('*').each(function() {
                var z = parseInt($(this).css('zIndex'));
                if (z > max) {
                    max = z;
                }
            });
            return max;
        },
        /**
        * Based on http://stackoverflow.com/questions/359788/javascript-function-name-as-a-string
        */
        execute: function(functionName, thisArg) {
            thisArg = thisArg || this;
            var args = Array.prototype.slice.call(arguments).splice(2);

            if (typeof functionName == 'function') {
                functionName.apply(thisArg, args);
            } else if (typeof functionName == 'string' && functionName.length > 0) {
                var namespaces = functionName.split('.');
                var func = namespaces.pop();
                context = window;
                for(var i = 0; i < namespaces.length; i++) {
                  context = context[namespaces[i]];
                }
                return context[func].apply(thisArg, args);
            }

        }
    };

    /**
    * App specific code. This would be defined in the javascript for the
    * specific app.
    */
    ap.app = {
        init: function(){}
    };

    /**
    * String filters.  Not the same as javascript dom filters
    */
    ap.filter = {
        // Filters the string
        key: function(text, allowBrackets) {
    		allowBrackets = allowBrackets || false;
    		if (allowBrackets == true) {
    			text = text.replace('[', '___lb___');
    			text = text.replace(']', '___rb___');
    		}
            text = $.trim(text.toLowerCase());
            text = text.replace(/\W+/g, ' ');
            if (allowBrackets == true) {
            	text = text.replace('___lb___', '[');
    			text = text.replace('___rb___', ']');
            }
            text = $.trim(text);
            text = text.replace(/\s+|-{2,}/g, '-');
            return text;
        },
        tag: function(text) {
            text = ap.filter.key(text);
            var words = text.split('-');
            if (words.length > 1) {
                // The first word should be all lowercase so it needs
                // to be taken out of the words array so that it's not
                // filtered by ucfirst().
                var first = words.slice(0, 1);
                words = words.slice(1);
                text = first + ap.filter.ucfirst(words.join(' '));
                text = text.replace(/\s+/g, '');
            }
            return text;
        },
        ucfirst: function(text) {
            if (typeof text == 'string' && text.length > 0) {
                // Split the text into separate words if necessary
                var words = text.split(/\s+/g);
                $.each(words, function(i, word) {
                    // Slit the word into two parts. The first part
                    // being the first letter. The second pard being
                    // the rest of the word.
                    var parts = word.match(/(\w)(.*)/);
                    if ($.isArray(parts)) {
                        // Put the word back together but uppercase the first letter
                        words[i] = parts[1].toUpperCase() + parts[2];
                    }
                });
                text = words.join(' ');
            }
            return text;
        }
    };

    /**
    * Provides methods for working urls and getting hash values
    */
    ap.location = {
        get: function() {
            var o = {
                hash: this.hash(),
                url: window.location
            }
            if (o.hash.length > 0) {
                o.url = o.url.replace(o.hash, '');
            }
            return o;
        },
        hash: function() {
            return window.location.hash.substring(1); // remove #
        },
        href: function(href) {
            var o = {
                url: href,
                hash: ''
            }
            var i = href.indexOf('#');
            if (i != -1) {
                o.hash = href.substring(i + 1);
                o.url = href.substring(0, i);
            }
            return o;
        },
        getHostUrl: function() {
            return window.location.protocol + '//' + window.location.hostname;
        }
    };

    // User Interface methods
    ap.ui = {
        activityIndicator: null,
        messageDiv: null,
        errorDiv: null,
        messageContainer: null,
        blocking: false,
        init: function()
        {
            this.setupSubNav();
            //Set the default container for messages and error messages
            this.activityIndicator = $('#ap-activity-indicator');
            this.errorDiv = $('#ap-error');
            this.messageDiv = $('#ap-message');
            $('#ap-message a.ap-close, .ap-message a.ap-close').live('click', function() {
                ap.ui.hideMessage();
                return false;
            });
            $('#ap-error a.ap-close, .ap-error a.ap-close').live('click', function() {
                ap.ui.hideError();
                return false;
            });
            this.setupFloatingButtons();
            this.handleHash();
            this.setupFormFocus();
        },
        handleHash: function() {
            /**
            * If there is a hash in the url try to show the
            * page content for that hash
            */
            var hash = ap.location.hash();
            if (hash.length > 0) {
                ap.ui.page.show(hash);
            }
            /**
            * When a main nav link is click check to see if it has a hash.
            * If so then if the link url is for the current page
            * show the page content for that hash
            */
            $('#ap-main-nav-container a').click(function() {
                var href = ap.location.href(this.href);
                if (href.hash.length > 0) {
                    var loc = ap.location.get();
                    if (href.url == loc.url) {
                        // The url is for the current page
                        ap.ui.page.show(href.hash);
                        $(this).blur();
                        return false;
                    }
                }
            });
        },

        /**
        * Sets the display for form fields when they receive focus
        */
        setupFormFocus: function() {
            $('div.ap-form-field-wrapper').hover(
            function() {
                $(this).addClass('hovering');
            },
            function() {
                $(this).removeClass('hovering');
            }
            );
            $('textarea, select, multi-select, :text, :image, :password, :radio, :checkbox, :file').each(function() {
                var el = $(this);
                el.unbind('focus').unbind('blur');
                var parent = el.parents('div.ap-form-field-wrapper');
                el.bind('focus', function() {
                    $('.ap-form-highlight').not(parent).removeClass('ap-form-highlight');
                    parent.addClass('ap-form-highlight');
                });
                el.bind('blur', function() {
                    if (!parent.hasClass('hovering')) {
                        parent.removeClass('ap-form-highlight');
                    }
                });
            });
        },
        setupSubNav: function() {
            $('ul.ap-main-nav').children().hover(
            function(){
                var height = $(this).height();
                var width = $(this).width();
                var link = $('a:first', this);
                var ul = $('ul', this);
                if (ul.width() < width) {
                    ul.width(width);
                }
                ul.addClass('hover').css('top', height + 'px').hover(
                function(){
                    link.addClass('hover');
                },
                function(){
                    link.removeClass('hover');
                }
                );
            },
            function(){
                $('ul', this).removeClass('hover');
            }
            );
        },
        setupFloatingButtons: function()
        {
            $('.submit-floating').remove();
            var buttons = $('div.submit:first').filter(function() {return !$(this).hasClass('no-float')});
            if (buttons.length > 0) {
                buttonPos = buttons.offset();
                var buttonClone = buttons.clone(true).removeClass('submit').addClass('submit-floating').css({display: 'none', opacity: 0.95}).insertAfter('div.submit:first');
                $(window).scroll(function(){
                    if ($(this).scrollTop() > buttonPos.top) {
                        buttonClone.show();
                    } else {
                        buttonClone.hide();
                    }
                });
                $(document).scroll(function() {
                    if ($(this).scrollTop() > buttonPos.top) {
                        buttonClone.show();
                    } else {
                        buttonClone.hide();
                    }
                });
            }
        },
        // Blocks the UI with the specific text
        block: function(text, selector) {
            if (!this.blocking) {
                var opts = {
                    css: {
                        border: 'none',
                        padding: '15px',
                        backgroundColor: '#000',
                        '-webkit-border-radius': '10px',
                        '-moz-border-radius': '10px',
                        opacity: '.8',
                        color: '#fff',
                        fontSize: '2em'
                    },
                    message: 'Please wait...'
                }
                if (text) {
                    opts.message = text;
                }
                // Test to see if there is an element with a z-index higher than 1000, which is the default z-index
                var zIndex = ap.getMaxZindex();
                if (zIndex < 1000) {
                    zIndex = 1000;
                }
                opts.baseZ = zIndex;
                if (selector == undefined) {
                	$.blockUI(opts);
                } else {
                	$(selector).block(opts);
                }
                this.blocking = true;
            }
        },
        unblock: function(selector) {
        	if (selector == undefined) {
        		$.unblockUI();
        	} else {
        		$(selector).unblock();
        	}
            this.blocking = false;
        },
        // Convenience function to block the UI and show the same text
        uploadBlock: function(options) {
            if (typeof options == 'string') {
                options = {msg: options};
            }
            var uploadOptions = {
                setSaveButton: false,
                saveButtonName: 'save',
                form: null,
                msg: 'Please wait while the information is submitted...'
            };
            $.extend(uploadOptions, options);
            if (true === uploadOptions.setSaveButton && null !== uploadOptions.form) {
                $('<input type="hidden" name="' + uploadOptions.saveButtonName + '" value="yes" />').appendTo(uploadOptions.form);
            }
            this.block(uploadOptions.msg);
        },
        showActivityIndicator: function()
        {
            this.activityIndicator.slideDown();
        },
        hideActivityIndicator: function()
        {
            this.activityIndicator.slideUp();
        },
        /**
        * Gets the message dom object
        * @param string message The message text
        */
        getMessage: function(message) {
            var c = this.messageDiv.clone(true);
            var html = '<div class="ui-state-highlight ui-corner-all"><span class="ui-icon ui-icon-info"></span><a href="#" class="ap-close"><span class="ui-icon ui-icon-closethick"></span></a><span class="ap-message-text">' + message + '</span></div>';
            c.html(html);
            return c;
        },
        /**
        * Gets the error message dom object
        * @param string message The error message text
        */
        getErrorMessage: function(message) {
            var c = this.errorDiv.clone(true);
            var html = '<div class="ui-state-error ui-corner-all"><span class="ui-icon ui-icon-alert"></span><a href="#" class="ap-close"><span class="ui-icon ui-icon-closethick"></span></a><span class="ap-message-text">' + message + '</span></div>';
            c.html(html);
            return c;
        },
        showMessage: function(message)
        {
            this.hideActivityIndicator();
            if (message != undefined) {
                this.hideError();
                var html = '<div class="ui-state-highlight ui-corner-all"><span class="ui-icon ui-icon-info"></span><a href="#" class="ap-close"><span class="ui-icon ui-icon-closethick"></span></a><span class="ap-message-text">' + message + '</span></div>';
                var msgDiv;
                if (this.messageContainer == undefined) {
                	msgDiv = this.messageDiv;
                } else {
                	msgDiv = this.messageContainer;
                	html = '<div class="ap-message">' + html + '</div>';
                }
                msgDiv.html(html);
                if (msgDiv.not(':visible')) {
                	msgDiv.slideDown('fast');
                }
                ap.windowResize(); //Because the message could push down the content and create a scroll bar which would affect the window width
            }
        },
        showError: function(message)
        {
            this.hideActivityIndicator();
            if (message != undefined) {
                this.hideMessage();
                var html = '<div class="ui-state-error ui-corner-all"><span class="ui-icon ui-icon-alert"></span><a href="#" class="ap-close"><span class="ui-icon ui-icon-closethick"></span></a><span class="ap-message-text">' + message + '</span></div>';
                var msgDiv;
                if (this.messageContainer == undefined) {
                	msgDiv = this.errorDiv;
                } else {
                	msgDiv = this.messageContainer;
                	html = '<div class="ap-error">' + html + '</div>';
                }
                msgDiv.html(html);
                if (msgDiv.not(':visible')) {
                	msgDiv.slideDown('fast');
                }
                ap.windowResize(); //Because the message could push down the content and create a scroll bar which would affect the window width
            }
        },
        hideMessage: function()
        {
            if (this.messageDiv != undefined) {
                this.messageDiv.slideUp('fast');
            }
            if (this.messageContainer != undefined) {
            	this.messageContainer.slideUp('fast');
            }
        },
        hideError: function()
        {
            if (this.errorDiv != undefined) {
                this.errorDiv.slideUp('fast');
            }
            if (this.messageContainer != undefined) {
            	this.messageContainer.slideUp('fast');
            }
        },
        hideMessageAndError: function() {
            this.hideMessage();
            this.hideError();
        },
        hideAll: function(exception)
        {
            this.hideActivityIndicator();
            if (exception == undefined || typeof exception == 'string') {
                if (exception != 'message') this.hideMessage();
                if (exception != 'error') this.hideError();
            } else if (exception instanceof Array) {
                if ($.inArray('message', exception) == -1) this.hideMessage();
                if ($.inArray('error', exception) == -1) this.hideError();
            }
            ap.ui.sections.hideAll(exception);
            return false;
        },
        disableButtons: function(buttons)
        {
            $(buttons).attr('disabled', 'disabled');
        },
        enableButtons: function(buttons)
        {
            $(buttons).removeAttr('disabled');
        },
        switchStylesheets: function(title) {
            $('link[@rel*=style][title]').each(function(i) {
                this.disabled = true;
                if (this.getAttribute('title') == title) this.disabled = false;
            });
        },
        /**
        * Remove the scrollbar from the page body
        */
        disableBodyScroll: function() {
            if (ap.document.height > ap.window.height) {
                $('html').css('overflow-y', 'hidden')
                $('body').css('width', ap.document.width);
                $('#ap_toolbar').css('width', ap.document.width);
            }

        },
        /**
        * Enable the scroll bar on the page body
        */
        enableBodyScroll: function() {
            if (ap.document.height > ap.window.height) {
                $('html').css('overflow-y', 'scroll');
                $('body').get(0).style.width = '';
                $('#ap_toolbar').get(0).style.width = '';
            }

        },
        /**
        * Shows or hides a file / image after clicking a link
        */
        viewFile: function(linkId, divId, hideText, showText) {
            var link = $('#' + linkId);
            var div = $('#' + divId);
            if (div.is(':hidden')) {
                link.html(hideText);
                div.slideDown();
            } else {
                link.html(showText);
                div.slideUp();
            }
            link.blur();
            return false;
        }
    };

    /**
    * Works with a collection of HTML elements that need the functionality
    * of "Add Another"
    */
    ap.ui.collection = function(options) {
        this.container = null;
        this.itemsContainer = null;
        this.clone = null;
        this.prepend = null;
        this.count = 1;
        this.link = null;
        this.fieldPrepend = null;
        this.regex = null;
        this.options = {};
        
        this.init(options);
    };
    ap.ui.collection.prototype = {
        init: function(options) {
            if (typeof options == 'object' && options.selector != undefined) {
            	if (options.fieldPrependOriginal == undefined) {
            		options.fieldPrependOriginal = options.fieldPrepend;
            	}
            	if (options.format == undefined) {
            		options.format = 'divs';
            	}
            	this.options = options;
                this.container = $(options.selector);
                this.setContainerMetadata();
                this.itemsContainer = this.container.children('.ap-collection-items');
                var children = this.itemsContainer.children('.ap-collection-item');
                if (children.length > 0) {
                    var self = this;
                    this.fieldPrepend = options.fieldPrepend;
                    this.fieldPrepend = this.fieldPrepend.replace('{num}', 0);
                    this.setNameRegex();
                    this.count = children.length;
                    if (options.link != undefined) {
                        var link = $(options.link);
                        
                        link.click(function() {
                            $(this).blur();
                            self.add(this);
                            return false;
                        });
                    }
                    var clone = $('.ap-collection-clone', this.container);
                    if (options.sortable) {
                    	if (options.format == undefined || options.format == 'grid') {
                    		$('tbody.ap-collection-items:first', this.container).sortable({
                            	axis: 'y',
                            	cursor: 'move',
                            	forceHelperSize: true,
                            	forcePlaceholderSize: true,
                            	handle: 'td.ap-reorder',
                            	helper:  function(e, ui) { // To fix column collapsing. See http://www.briangrinstead.com/blog/make-table-rows-sortable-using-jquery-ui-sortable
                            		ui.children().each(function() {
                            			$(this).width($(this).width());
                            		});
                            		return ui;
                            	},
                            	placeholder: 'ap-sort-placeholder',
                            	start: function(e, ui) {
                            		// Force the placeholder to have a td otherwise some browsers won't show the placeholder because it's just an empty tr
                            		var numTd = ui.item.children().length;
                            		ui.placeholder.append('<td colspan="' + numTd + '">&nbsp;</td>');
                            	},
                            	update: function() {
	                                self.handleSort();
	                            }
                            }).disableSelection();
                    	} else {
	                    	// Remove a sort handle that might have been created already if this is a collection inside another collection
	                    	children.children('.ap-sort-handle').remove();
	                    	children.append('<div class="ap-sort-handle">drag to sort</div>');
	                        clone.append('<div class="ap-sort-handle">drag to sort</div>');
	                        $('.ap-sort-handle', this.container).css('visibility', 'hidden');
	                    	
	                        this.itemsContainer.sortable({
	                            axis: 'y',
	                            cursor: 'move',
	                            forceHelperSize: true,
	                            forcePlaceholderSize: true,
	                            handle: '.ap-sort-handle',
	                            items: 'div.ap-collection-item',
	                            placeholder: 'ui-state-highlight',
	                            update: function() {
	                                self.handleSort();
	                            }
	                        });
                    	}
                    }

                    if (clone.length > 0) {
                        this.clone = clone.clone().removeAttr('style').removeClass('ap-collection-clone').addClass('ap-collection-item');
                        clone.remove();
                    } else {
                        this.clone = $(children.get(0)).clone(true);
                    }
                    this.setContainerMetadata(this.clone);
                    
                    // Clear the clone form field values
                    $(':text,select,:radio,:checkbox,textarea', this.clone).clearFields();
                    $(':input[type=hidden]', this.clone).val('');
                    children.each(function(i) {
                        self.setupItem(this, i);
                    });
                }
            }
        },
        setOptions: function() {
        	var opt = this.container.data();
        	this.options = opt.options;
//        	this.options = this.container.metadata({type: 'attr', name: 'rel'});
        	if (this.options.fieldPrependOriginal != undefined && this.options.num != undefined) {
        		this.fieldPrepend = this.options.fieldPrependOriginal.replace('{num}', this.options.num);
        	}
        	this.setNameRegex();
        },
        setNameRegex: function(fieldPrepend) {
        	fieldPrepend = fieldPrepend || this.fieldPrepend;
        	var prepend = fieldPrepend.replace(new RegExp('\\[', 'g'), '\\[');
            var prepend = prepend.replace(new RegExp('\\]', 'g'), '\\]');
            this.regex = new RegExp('^' + prepend + '\\[.*?\\]');
        },
        setContainerMetadata: function(container, options) {
        	container = container || this.container;
        	container = $(container);
        	
        	options = options || this.options;
        	
        	container.data('options', options);
        	// Add meta data to the container to be used by child collections
//            var optAttr = '{';
//            $.each(options, function (k, v) {
//            	optAttr += k += ':\'' + v + '\',';
//            });
//            // Remove the trailing ","
//            optAttr = optAttr.substring(0, optAttr.length - 1);
//            optAttr += '}';
//            container.attr('rel', optAttr);
        },
        handleSort: function() {
        	var self = this;
        	this.itemsContainer.children('.ap-collection-item').each(function(i) {
        		$('.ap-collection-container', this).each(function() {
        			var container = $(this);
            		var data = container.metadata({type: 'attr', name: 'rel'});
            		data.num = i;
            		self.setContainerMetadata(container, data);
            	});
                self.renameItem(this, i);
                $('.ap-collection-order', this).val(i + 1);
            });
        	this.renameItems();
        },
        /**
         * Setups up a child container when a new parent container has been added
         */
        setupChildCollection: function(container) {
        	var container = $(container);
        	var data = container.metadata({type: 'attr', name: 'rel'});
        	var time = new Date();
        	time = time.getTime();
        	var id = 'ap-collection-' + time;
        	var linkId = 'apc' + time;
        	var prepend = data.fieldPrepend.replace('{num}', this.count);
        	var options = {
    			selector: '#' + id,
				link: '#' + linkId,
				sortable: data.sortable,
				fieldPrepend: prepend,
				fieldPrependOriginal: data.fieldPrepend,
				num: this.count
        	};
        	// Give the container and link a new unique id
        	container.attr('id', id);
        	$(data.link, container).attr('id', linkId).unbind('click');
        	var x = new ap.ui.collection(options);
        },
        renameItems: function() {
            var self = this;
            this.itemsContainer.children('.ap-collection-item').each(function(i) {
                self.renameItem(this, i);
                $('.ap-collection-order', this).val(i + 1);
            });
        },
        add: function(link) {
        	var self = this;
        	var meta = this.clone.metadata({type: 'attr', name: 'rel'});
        	
        	var copyRegex = this.regex;
        	// set the 
        	this.setNameRegex(meta.fieldPrepend);
        	        	
        	var item = this.setupItem(this.clone.clone(true), this.count);
        	
            this.itemsContainer.append(item);
            // See if the item contains another collection
            $('.ap-collection-container', item).each(function() {
            	self.setupChildCollection(this);
            });
            ap.ui.setupFormFocus();

            if ($.isArray(this.options.callbacks)) {
            	$.each(this.options.callbacks, function() {
            		ap.execute(this, self, item);
            	});
            } else if (typeof this.options.callbacks == 'string' && this.options.callbacks.length > 0) {
            	ap.execute(this.options.callbacks, self, item);
            }
            this.count ++;
        },
        setupItem: function(item, count) {
            item = $(item);
            var self = this;

            this.renameItem(item, count);

            item.mouseover(function(event) {
            	//event.stopPropagation();
                self.itemMouseover(this);
            });
            item.mouseout(function(event) {
            	//event.stopPropagation();
                self.itemMouseout(this);
            });
            var del;
            if (this.options.format == 'divs') {
            	del = item.children('a' + this.options.deleteSelector);
            } else {
            	del = $(this.options.deleteSelector, item);
            }
            del.unbind('click').click(function() {
            	// Not sure this is being used correctly
                var hiddenRemove = $('.ap-collection-delete', item);
                if (hiddenRemove.length > 0) {
                    hiddenRemove.val(1).appendTo(self.container);
                }
                // End not sure
                
                $(this).remove();
                self.count --;
                item.hide('explode', {}, 'slow', function() {
                    item.remove();
                    self.renameItems();
                });
                return false;
            });
            
            return item;
        },
        renameItem: function(item, count, refreshOptions) {
            item = $(item);
            if (item.hasClass('ap-no-rename')) {
                // Don't rename
                return;
            }
            refreshOptions = refreshOptions || true;
            if (refreshOptions == true) {
            	this.setOptions();
            }

            var self = this;
            $(':checkbox', item).each(function() {
                this.name = self.getName(this.name, count);
            });
            $(':file', item).each(function() {
                this.name = self.getName(this.name, count);
            });
            $(':radio', item).each(function() {
                this.name = self.getName(this.name, count);
            });
            $('select', item).each(function() {
                this.name = self.getName(this.name, count);
            });
            $(':text,:input[type=hidden]', item).each(function() {
                this.name = self.getName(this.name, count);
            });
            $('textarea', item).each(function() {
                this.name = self.getName(this.name, count);
            });
        },
        getName: function (name, i) {
            prepend = '';
            if (this.fieldPrepend.length > 0) {
                name = name.replace(this.regex, '');
                // Add brackets if they don't already exist
                if (name.indexOf('[') == -1) {
                	name = '[' + name + ']';
                }
                prepend = this.fieldPrepend + '[' + i + ']';
            }
            name = prepend + name;
            return name;
        },
        itemMouseover: function(item) {
            item = $(item);
            item.addClass('hover');
            if (this.options.format == 'divs') {
	            item.children(this.options.deleteSelector).css('visibility', 'visible');
	            if (this.count > 1) {
	            	item.children('.ap-sort-handle').css('visibility', 'visible');
	            }
            }
        },
        itemMouseout: function(item) {
            item = $(item);
            item.removeClass('hover');
            if (this.options.format == 'divs') {
	            $('.ap-sort-handle', item).css('visibility', 'hidden');
	            $(this.options.deleteSelector, item).css('visibility', 'hidden');
            }
        }
    };

    /**
    * Sets up a dialog box.
    * Intended to be used as the onlick event for navigation
    * @param object dialogOptions The options for the jQuery UI dialog box
    * @param object options The general options for this box
    */
    ap.ui.setupDialog = function(url, dialogOptions, options) {
        var dialog = new ap.ui.dialog(dialogOptions, options);
        dialog.open(url);
        return false;
    };

    /**
    * Create a dialog box on the fly that can handle forms and
    * display form messages.

    * The options for the jQuery UI dialog and this object are kept separate
    * so that with future versions of jQuery UI there isn't a possibility of
    * using a option name internally that is used in the jQuery UI dialog options.
    *
    * @param object dialogOptions The options for the jQuery UI dialog box
    * @param object options The general options for this box
    */
    ap.ui.dialog = function(dialogOptions, options) {
        // Holds the dialog dom object
        this.box = null;

        // Holds the URL to be loaded
        this.url = null;

        var self = this;

        // Holds the options for the dialog box
        this.dialogOptions = {
            height: '90%',
            width: '75%',
            minHeight: 200,
            minWidth: 300,
            modal: true,
            position: 'center',
            open: function() {self.load(); },
            close: function() {self.close(); }
        	
        };
        $.extend(this.dialogOptions, dialogOptions);

        // Holds the general options
        this.options = {
            ajaxForm: true, // Whether or not the form will be submitted via ajax.
            load: null, // Callback function for after the content is loaded,
        	close: null // Callback function for when the dialog box closes
        };
        $.extend(this.options, options);

        // If the height and width are percents then get the pixel height
        if (typeof this.dialogOptions.height == 'string' && this.dialogOptions.height.indexOf('%') > -1) {
            this.dialogOptions.height = (parseInt(this.dialogOptions.height) / 100) * ap.window.height;
        }
        if (typeof this.dialogOptions.width == 'string' && this.dialogOptions.width.indexOf('%') > -1) {
            this.dialogOptions.width = (parseInt(this.dialogOptions.width) / 100) * ap.window.width;
        }

    };

    ap.ui.dialog.prototype = {
        open: function(url) {
            this.url = url;
            var html = '<div class="apuiDialog"><div class="apuiDialogMsg"></div><div class="apuiDialogContent"><div class="apuiDialogLoading"></div></div></div>';
            this.box = $(html).appendTo('body').dialog(this.dialogOptions);
            $('.apuiDialogLoading', this.box).height(this.box.height()).css('minHeight', this.box.css('minHeight'));
        },
        load: function() {
            var self = this;
            var saving = $('<div class="apuiDialogSave">Saving</div>');
            $('.apuiDialogContent', this.box).load(this.url, null, function(responseText, textStatus, XMLHttpRequest) {
                if ($.isFunction(self.options.load)) {
                    self.options.load.call(self, self.box, self.options, responseText, textStatus, XMLHttpRequest);
                }
                var submit = $('.submit', self.box);
                if (self.options.ajaxForm) {
                	ap.ui.messageContainer = $('.apuiDialogMsg', self.box);
                	var form = $('form', self.box);
                	form.unbind('ajaxForm');
                    form.ajaxForm({
                        dataType: 'json',
                        beforeSubmit: function() {
                    		ap.ui.uploadBlock();
//                            saving.appendTo(self.box).insertAfter(submit).show();
//                            submit.hide();
                        },
                        success: function(response) {
                            if (response.error == undefined) {
                                if (response.msg != undefined) {
                                	ap.ui.showMessage(response.msg);
//                                    $('.apuiDialogMsg', self.box).html(ap.ui.getMessage(response.msg));
                                }
                            } else {
                            	ap.ui.showErrorMessage(response.error);
//                                $('.apuiDialogMsg', self.box).html(ap.ui.getErrorMessage(response.error));
                            }
                            ap.ui.unblock();
//                            $('.apuiDialogSave', self.box).remove();
//                            submit.show();
                        },
                        error: function(request, msg) {
                            if (msg == 'parsererror') {
                                msg = 'The information was submitted and most likely saved, however there was a problem parsing the response. Please reload the page to ensure that the information was saved. If it was not saved please contact your website developer and report this error to them.';
                            }
                            ap.ui.showError(msg);
                            ap.ui.unblock();
                        },
                        extraData: {
                            apIframeSubmit: 'yes'
                        }
                    });
                }
                $('a.cancel,a.close', self.box).click(function (){
                    self.box.dialog('close');
                    if ($.isFunction(self.options.close)) {
                        self.options.close.call(self, self.box, this, self.options);
                    }
                    return false;
                });
            });
        },
        close: function() {
            this.box.remove();
            delete this.box;
        }
    };
    
    ap.ui.displayOrder = function(options) {
        this.position = null;
        this.itemSelect = null;
        this.itemWrapper = null;
        this.init(options);
    };
    ap.ui.displayOrder.prototype = {
		init: function(options) {
        	var self = this;
	        this.position = $(options.positionSelector);
	        this.itemWrapper = $(options.itemSelector);
	        this.itemSelect = $('select', this.itemWrapper);
	        var itemLoading = $('<div style="display: none;">Loading...</div>').insertAfter(this.itemSelect);
	        var order = $(options.orderSelector);
	
	        if (this.itemSelect.get(0).options.length == 0) {
	            // There are no items to set "before" or "after" to.
//	            this.position.emptySelect(1);
	        	this.limit();
	        }
	
	        // Setup the toggle link and hidden field to turn changing display order on or off
	        if (options.changeSelector != undefined && options.toggleSelector != undefined && options.orderWrapperSelector != undefined) {
	            changeLink = $(options.changeSelector);
	            changeLink.click(function() {
	                var link = $(this);
	                link.blur();
	                if (link.html() == 'change') {
	                    link.html('do not change display order');
	                    $(options.orderWrapperSelector).show();
	                    $(options.toggleSelector).val('change');
	                } else {
	                    link.html('change');
	                    $(options.orderWrapperSelector).hide();
	                    $(options.toggleSelector).val('no_change');
	                }
	                return false;
	            });
	        }
	
	        this.position.change(function() {
	            var val = $(this).val();
	            if (val == 'before' || val == 'after') {
	                self.itemWrapper.css('visibility', 'visible');
	
	                if (options.url != undefined && options.url.length > 0) {
	                    var loadUrl = options.url;
	                    if (options.urlValue != undefined && options.urlValue instanceof Array) {
	                        $.each(options.urlValue, function(i, valueData) {
	                            if (valueData.selector != undefined && valueData.url != undefined) {
	                                var v = $(valueData.selector).val();
	                                if (v != undefined && v.length > 0) {
	                                    loadUrl += valueData.url + v;
	                                }
	                            }
	                        });
	                    }
	
	                    self.itemSelect.hide();
	                    itemLoading.css('display', 'inline');
	                    $.ajax({
	                        dataType: 'json',
	                        success: function(data) {
	                            self.itemSelect.loadSelect(data).show();
	                            itemLoading.hide();
	                        },
	                        type: 'get',
	                        url: loadUrl
	                    });
	                } else {
	                    self.itemSelect.css('display', 'inline');
	                    itemLoading.hide();
	                }
	            } else {
	                self.itemWrapper.css('visibility', 'hidden');
	            }
	        });
	
	        this.itemSelect.change(function() {
	            var val = $(this).val();
	            if (val > 0) {
	                order.val(val);
	            }
	        });
	
	        this.itemSelect.parents("form:first").ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
	            // Only hide the elements if the form url is the same as the AJAX post request
	            if ((ap.location.getHostUrl() + ajaxOptions["url"]) == this.action) {
	                self.itemWrapper.css('visibility', 'hidden');
	                if (self.position.get(0).options.length == 1) {
	                    self.resetPositionSelect();
	                }
	                self.itemSelect.val('after');
	            }
	        });
	    },
        afterLoading: function() {
            if (this.itemSelect.get(0).options.length == 0) {
                // There are no items to set "before" or "after" to.
                //this.position.emptySelect(1);
            	this.limit();
                this.itemWrapper.css('visibility', 'hidden');
            } else {
                // If only "first" and "last" are set in the position select add "before" and "after"
                if (this.position.get(0).options.length == 1) {
                    this.resetPositionSelect();
                    this.itemWrapper.css('visibility', 'hidden');
                }
            }
        },
        limit: function() {
        	this.position.emptySelect();
        	this.position.addSelectOption('last', 'Last');
        },
        resetPositionSelect: function() {
        	this.position.emptySelect();
        	this.position.addSelectOption('first', 'First');
            this.position.addSelectOption('last', 'Last');
            this.position.addSelectOption('before', 'Before');
            this.position.addSelectOption('after', 'After');
            this.position.val('last');
        }
    };

    /**
    * Provides functionality for opening a dialog box that
    * contains file upload capabilities and a file browser.
    *
    * Options:
    * fileType: (string) The type of file to upload. "file" or "image"
    *
    */
    ap.ui.fileBrowser = function(options) {
        var self = this;

        // Holds the jQuery object for the upload form
        this.uploadForm = null;
        // Holds the jQuery object for the form around the file information
        this.fileInfoWrapper = null;
        // Holds the uploaded file information
        this.fileInfo = {};

        // Holds the general options
        this.options = {
            callback: null,
            fileType: 'file',
            imageHeight: 0,
            imageWidth: 0,
            path: '/docs/',
            title: 'File Browser',
        	outsideWebRoot: false
        };
        $.extend(this.options, options);

        // Setup the dialog box
        this.dialog = new ap.ui.dialog({title: this.options.title}, {ajaxForm: false, load: function(dialog) {self.dialogLoaded(dialog);}});
    };
    ap.ui.fileBrowser.prototype = {
        /**
        * Handles opening the dialog box
        */
        open: function() {
            var url = apAdminBaseUrl + 'browser/upload?path=' + this.options.path;
            if (this.options.fileType == 'file') {
                url += '&type=file';
                if (this.options.outsideWebRoot == true) {
                	url += '&owr=1';
                }
            } else if (this.options.fileType == 'image') {
                url += '&type=image';
                url += '&height=' + this.options.imageHeight;
                url += '&width=' + this.options.imageWidth;
            }
            this.dialog.open(url);
        },
        /**
        * Callback for after the dialog box opens.
        * Sets up the upload form and the links for using the
        * uploaded file or for uploading a new file
        * @param object dialogBox jQuery object of the dialog box
        */
        dialogLoaded: function(dialogBox) {
            var self = this;
            this.uploadForm = $('form', dialogBox).show();
            this.fileInfoWrapper = $('#fileUploadInfo', dialogBox).hide();
            var formOptions = {
                form: this.uploadForm,
                uploads: true,
                rules: {file: {required: true}},
                ajax: {
                    callback: function(data) {self.fileUploaded(data);}
                },
                showErrors: function(map, errs) {this.defaultShowErrors(errs);},
                uploadBlockMsg: 'Please wait while the file is uploaded...'
            }
            if (this.options.fileType == 'image') {
                formOptions.uploadBlockMsg = 'Please wait while the image is uploaded...';
            }
            new ap.ui.form(formOptions);

            // Setup file information links
            $('#useUploadedFile').click(function() {
                // Call the callback function to handle the uploaded/selected file information
                if (typeof self.options.callback == 'function') {
                    self.options.callback.call(self, self.fileInfo);
                }
                self.dialog.close();
                $(this).blur();
                return false;
            });
            $('#uploadNewFile').click(function() {
                self.fileInfoWrapper.hide();
                self.uploadForm.show();
                $(this).blur();
                return false;
            });
        },
        /**
        * Callback function for after a file is uploaded.
        * Handles displaying the file information.
        * @param object data Information about the uploaded file
        */
        fileUploaded: function(data) {
            this.fileInfo = {};
            if (typeof data.filename == 'string' && typeof data.path == 'string' && typeof data.fileType == 'string') {
                this.fileInfo = data;
                if (data.fileType == 'image') {
                    $('#uploadedImageThumb').html('<img src="' + apAdminBaseUrl + 'browser/view/thumbnail?path=' + data.path + '" /><div class="smallText">(thumbnail)</div>');
                } else {
                    $('#uploadedImageThumb').empty();
                }
                $('#uploadedFileName').html('<div class="ap-filetype ap-filetype-' + data.fileType + '">' + data.filename + '</div>');
                this.uploadForm.hide();
                this.fileInfoWrapper.show();
            }
        }
    }

    /**
    * Provides functionality for the formFileBrowser UI component
    *
    * options:
    * button: (string) The selector for the button to open the file browser
    * fileInfoWrapper (string) The selector for the div where selected file information will go
    * fileType: (string) The file type to upload. "file" or "image"
    * hiddenField: (string) The selector for the hidden field where the file path will be stored
    * imageHeight: (int) The height constraints in pixels for the uploaded image
    * imageWidth: (int) The width constraints in pixels for the uploaded image
    * path: (string) The directory path to where the file should be uploaded
    * title: (strint) The title for the file browser dialog box
    *
    * If the fileType is "image" and imageHeight and imageWidth are not set, then
    * no height or width constraints will be placed on the uploaded image.
    */
    ap.ui.fileUpload = function(options) {
        var self = this;

        // Holds the general options
        this.options = {
            button: '',
            fileInfoWrapper: '',
            fileType: 'file',
            hiddenField: '',
            hiddenFieldOwr: '',
            imageHeight: 0,
            imageWidth: 0,
            path: '/docs/',
            title: 'File Browser',
            onFileLoaded: null,
            onFileRemoved: null,
        	outsideWebRoot: false
        };
        $.extend(this.options, options);
        
        // Holds the file info wrapper
        this.fileInfo = $(this.options.fileInfoWrapper);

        // Setup the "remove" functionality
        $('a.delete', this.fileInfo).unbind('click').click(function() {
            self.remove();
            $(this).blur();
            return false;
        });

        // Holds an instance of the fileBrowser object
        this.fileBrowser = new ap.ui.fileBrowser({
            callback: function(fileData) {self.handleFile(fileData);},
            fileType: this.options.fileType,
            imageHeight: this.options.imageHeight,
            imageWidth: this.options.imageWidth,
            path: this.options.path,
            title: this.options.title,
            outsideWebRoot: this.options.outsideWebRoot
        });

        // If the button is setup then set it's onclick event to open the file browser
        $(this.options.button).unbind('click').click(function() {
            $(this).blur();
            self.fileBrowser.open();
            return false;
        });
    };
    ap.ui.fileUpload.prototype = {
        /**
        * Callback function for after the file is selected
        * from the file browser.
        */
        handleFile: function(fileData) {
            var self = this;
            // Set the file path in the hidden field
            $(this.options.hiddenField).val(fileData.path);
           	$(this.options.hiddenFieldOwr).val(fileData.owr);
            // Inject the file information into the DOM.
            var fileView = '<div class="ap-filetype ap-filetype-' + fileData.fileType + ' smallText" style="margin: 5px 0;">';
            if (this.options.showFullPath == false) {
            	fileView += fileData.filename;
            } else {
            	fileView += fileData.fullPath;
            }
            fileView += '</div>';
            this.fileInfo.html(fileView);
            this.fileInfo.append('<div class="smallText">(<a href="#" class="delete">remove</a>)</div>');
            if (fileData.fileType == 'image') {
            	var holder = $('<div class="ap-uploaded-file"></div>');
            	var imgLink = $('<a href="' + fileData.path + '"></a>');
            	var img = $('<img src="' + apAdminBaseUrl + 'browser/view/thumbnail?path=' + fileData.path + '" />');
            	imgLink.append(img);
            	holder.append(imgLink);
            	this.fileInfo.append(holder);
            	// Only do vertical centering for image set images
            	if (this.fileInfo.hasClass('formImgSetImage')) {
	            	var imgH = img.height();
	            	if (imgH < 120) {
	            		img.css('top', (120 - imgH) / 2);
	            	}
            	}
            	$(imgLink).fancybox({imageScale: true, enableEscapeButton: true});
            }
            
            // Setup the "remove" functionality
            $('a.delete', this.fileInfo).click(function() {
                self.remove();
                $(this).blur();
                return false;
            });
            // Update the button text for updating the file
            var btnText = 'Choose a different file';
            if (this.options.fileType == 'image') {
                btnText = 'Choose a different image';
            }
            $(this.options.button).find('span').html(btnText);
            if (this.options.onFileLoaded != null) {
            	this.options.onFileLoaded.call(this, this.fileInfo);
            }
        },
        remove: function()
        {
            // Revert the button text back to it's original value
            var btnText = 'Choose a file';
            if (this.options.fileType == 'image') {
                btnText = 'Choose an image';
            }
            $(this.options.button).find('span').html(btnText);
            // Clear the file path
            $(this.options.hiddenField).val('');
            // Clear the file info
            this.fileInfo.empty();
            
            if (this.options.onFileRemoved != null) {
            	this.options.onFileRemoved.call(this, this.fileInfo);
            }
        },
        collectionCallback: function(item) {
        	var opts = $.extend({}, this.options);
        	var now = new Date().getTime();
        	var buttonId = 'fileBrowser-' + now
        	var fileInfoWrapperId = 'fileBrowserInfo-' + now;
        	var hiddenFieldId = 'fileBrowserPath-' + now;
        	var hiddenFieldOwr = 'fileBrowserOwr-' + now;
        	
        	opts.button = '#' + buttonId;
        	opts.fileInfoWrapper = '#' + fileInfoWrapperId;
        	opts.hiddenField = '#' + hiddenFieldId;
        	opts.hiddenFieldOwr = '#' + hiddenFieldOwr;
        	
        	var btnText = 'Choose a file';
            if (this.options.fileType == 'image') {
                btnText = 'Choose an image';
            }
            
        	$(this.options.button, item).attr('id', buttonId).find('span').html(btnText);;
        	$(this.options.fileInfoWrapper, item).attr('id', fileInfoWrapperId).empty();
        	$(this.options.hiddenField, item).attr('id', hiddenFieldId).val('');
        	$(this.options.hiddenFieldOwr, item).attr('id', hiddenFieldOwr).val('');
        	new ap.ui.fileUpload(opts);
        }
    };

    /**
    * Provides common methods for working with forms
    */
    ap.ui.form = function(options) {
        var self = this;
        // Holds the form instance
        this.form = $(options.form);
        delete options.form;

        this.ajax = {
            callback: null, // Holds the callback function to execute after the form successfully submits
            grid: '#indexPage table.ap-tbl', // Holds the ap.grid to updated after the form is submitted
            is: false, // Holds whether or not the form will be submitted via ajax
            show: '', // Holds the name of the page to show after the form successfully submits
            load: '', // The name of the page to load the loadUrl into after the form successfully submits. This is used if "show" is not used.
            loadUrl: '', // The URL to load in the "load" page. This is used if "show" is not used.
            tabIndex: 0, // The index of the tab to show after the form was submitted.
            type: 'add' // Holds the type of form. add or edit
        };

        if (options.ajax != undefined && typeof options.ajax == 'object') {
            $.extend(this.ajax, options.ajax);
            this.ajax.is = true;
            delete options.ajax;
        }

        // Holds whether or not the form has file uploads
        this.hasUploads = false;
        if (options.uploads != undefined) {
            this.hasUploads = options.uploads;
            delete options.uploads;
        }

        this.saveActionClicked = false;

        // Holds the form options
        this.options = {
            errorPlacement: function(err, el) {err.insertBefore(el);},
            messages: {},
            rules: {},
            showErrors: this.showErrors,
            uploadBlockMsg: null
        };
        $.extend(this.options, options);

        /**
        * If the form is to be submitted via ajax or the form
        * has uploads then set a custom submit handler.
        */
//        if (this.ajax.is == true || this.hasUploads == true) {
        if (this.ajax.is == true) {
            if (this.options.submitHandler == undefined) {
                this.options.submitHandler = function(form) {
                    self.submit(form);
                };
            }
            $(':submit', this.form).click(function() {
                if (this.name == 'save') {
                    self.saveActionClicked = true;
                } else {
                    self.saveActionClicked = false;
                }
                $(this).blur();
                return true;
            });
        }
        this.validate();
    };

    ap.ui.form.prototype = {
        validate: function() {
            this.form.validate(this.options);
        },
        submit: function(form) {
            ap.ui.uploadBlock(this.options.uploadBlockMsg);
            if (this.ajax.is == true) {
                var self = this;
                $(form).ajaxSubmit({
                    complete: function() {
                        //ap.ui.unblock();
                    },
                    dataType: 'json',
                    error: function(request, msg) {
                        if (msg == 'parsererror') {
                            msg = 'The information was submitted and most likely saved, however there was a problem parsing the response. Please reload the page to ensure that the information was saved. If it was not saved please contact your website developer and report this error to them.';
                        }
                        ap.ui.showError(msg);
                        ap.ui.unblock();
                    },
                    extraData: {
                        apIframeSubmit: 'yes'
                    },
                    success: function(data) {
                    	if (typeof data == 'undefined' || data == null) {
                    		data = {};
                    	}
                        if (data.error) {
                            ap.ui.showError(data.error);
                            ap.ui.unblock();
                        } else {
                            if (self.ajax.type == 'add') {
                                self.reset();
                            } else {
                                self.resetFileFields();
                            }
                            if (typeof self.ajax.callback == 'function') {
                                self.ajax.callback.call(self, data);
                            }
                            if (data.table != undefined && self.ajax.grid && $.fn.apGrid != undefined) {
                                if (self.ajax.type == 'add') {
                                    $(self.ajax.grid).apGrid('addRow', data.table);
                                } else {
                                    if (data.table.id != undefined) {
                                        $(self.ajax.grid).apGrid('editRow', '.row-' + data.table.id, data.table);
                                    }
                                }
                            }
                            // Add the "view image" or "view file" link after each file upload if necessary
                            if (data.fileView && data.fileView.length > 0) {
                                $.each(data.fileView, function(i, file) {
                                    var field = $('input[name="' + file.name + '"]');
                                    if (field.length > 0) {
                                        while (field.next('.ap-view-file').length > 0) {
                                            field.next('.ap-view-file').remove();
                                        }
                                        field.after(file.view);
                                    }
                                });
                            }
                            if (data.refresh != undefined && self.ajax.refreshSelector != undefined && self.saveActionClicked == false) {
                            	ap.ui.page.load(self.ajax.refreshSelector, data.refresh, data.msg, self.ajax.tabIndex);
                            	return;
                            } else if (data.loadUrl != undefined && self.ajax.show.length > 0) {
                                ap.ui.page.load(self.ajax.show, data.loadUrl, data.msg, self.ajax.tabIndex);
                                return;
                            } else if (self.ajax.load.length > 0 && self.ajax.loadUrl.length > 0 && self.saveActionClicked) {
                            	var loadUrl = self.ajax.loadUrl;
                            	if (data.loadUrl != undefined) {
                            		loadUrl = data.loadUrl;
                            	}
                            	if (data.table != undefined && data.table.id != undefined) {
                            		loadUrl += '/apgridid/' + data.table.id;
                            	}
                                ap.ui.page.load(self.ajax.load, loadUrl, data.msg, self.ajax.tabIndex);
                                return; 
                            } else if (self.ajax.show.length > 0 && data.html != undefined) {
                            	ap.ui.page.loadHtml(self.ajax.show, data.html, data.msg);
                            } else if (self.ajax.show.length > 0 && self.saveActionClicked) {
                                ap.ui.page.show(self.ajax.show, true, self.ajax.tabIndex);
                                ap.ui.unblock();
                            } else {
                                ap.ui.unblock();
                            }
                            if (data.msg) {
                                ap.ui.showMessage(data.msg);
                            }

                        }
                    }
                });
            } else {
                if (this.saveActionClicked == true) {
                    $('<input type="hidden" name="save" value="yes" />').appendTo(form);
                }
                form.submit();
            }

        },
        // Resets the form
        reset: function() {
            // Reset all form fields
            this.form.resetForm();
            // Reset url key and app tag text values
            $('.ap-url-key-text', this.form).show().find('span').html('');
            $('.ap-url-key-tag', this.form).hide();
            $('.ap-app-tag-text', this.form).show().find('span').html('');
            $('.ap-app-tag-tag', this.form).hide();

            // Reset file browser fields
            $('.formFileBrowserInfo', this.form).empty();
            $('.formFileBrowserBtn', this.form).each(function() {
                var btn = $(this);
                if (btn.hasClass('formFileBrowserImage')) {
                    $('span', btn).html('Choose an image');
                } else {
                    $('span', btn).html('Choose a file');
                }
            });
            $('.formFileBrowserValue', this.form).val('');
        },
        resetFileFields: function() {
            $('input[type=file]', this.form).each(function() {
                var field = $(this);
                var clone = field.clone();
                var form = $('<form></form>').appendTo('body');
                form.append(clone).resetForm();
                field.replaceWith(clone);
                form.remove();
            });
        },
        /**
        * Handles displaying the errors.
        * Done in the context of the validator
        */
        showErrors: function(map, errs) {
            var n = this.numberOfInvalids(); var e;
            if (n > 0) {
                if (n > 1) { e = "There were " + this.numberOfInvalids() + " errors";}
                else {e = "There was 1 error";}
                e += "<ul>";
                $(errs).each(function(i, err) { e += "<li>" + err.message + "</li>";});
                e += "</ul>";
                ap.ui.showError(e); this.defaultShowErrors(errs);
            } else {
                ap.ui.hideError(); this.defaultShowErrors(errs);
            }
            $(".ui-tabs-panel").filter(function() {
                // get panels that have an error that is visible
                return $(this).find(".error").filter(function() {return $(this).not(':hidden');}).length > 0;
            }).each(function() {
                var num = /\d/.exec(this.id); $(this).parents("div.ui-tabs:first").children("ul").find("li").eq(num).addClass("error");
            }).end().filter(function() {
                return $(this).find(".error").filter(function() {return $(this).not(':hidden');}).length == 0;
            }).each(function () {
                var num = /\d/.exec(this.id); $(this).parents("div.ui-tabs:first").children("ul").find("li").eq(num).removeClass("error");
            });

        }
    };

    /**
    * Helper object for setting up a new apGrid.
    */
    ap.ui.grid = function(options) {
    	var self = this;
    	
        this.opts = {
        	// The buttons to add to the grid	
    		buttons: [],
            // Any apGrid specific options
            gridOpts: {},
            // The plural version of the item in each row. Used in messages.
            plural: '',
            // The singular version of the item in each row. Used in messages.
            singular: '',
            // The table selector
            tableSelector: 'table.ap-tbl'
        };
        $.extend(this.opts, options);

        this.gridOpts = {
            noDataContainer: '.ap-no-data-msg',
            toolbar: [],
            tree: false
        };
        if (typeof this.opts.gridOpts == 'object') {
	        $.each(this.opts.gridOpts, function(key, val) {
	        	self.gridOpts[key] = val;
	        });
        }
        
        if ($.isArray(this.opts.buttons) || typeof this.opts.buttons == 'object') {
        	$.each(this.opts.buttons, function() {
        		self.addButton(this);
        	});
        }
        
        $(this.opts.tableSelector).apGrid(this.gridOpts);
    };
    
    ap.ui.grid.prototype = {
		addButton: function(options) {
    		options.plural = this.opts.plural;
    		options.singular = this.opts.singular;
    		this.gridOpts.toolbar.push(new ap.ui.gridBtn(options));
    	}
    };
    
    /**
     * Object for creating grid buttons
     * @param object options
     */
    ap.ui.gridBtn = function(options) {
    	var opts = {
			// Options only used in this object and not in ap.grid
			action: 'ajax',
			confirm: false,
			confirmMsg: 'Are you sure?',
			errorMsg: 'There was a problem completing the action because you most likely don\'t have permission to complete the action.',
			error404Msg: 'There was a problem completing the action due to a system error. Please try again later.',
			plural: 'Item',
			removeRows: false,
			singular: 'Items',
			url: null,
			// Options used in ap.grid
			btnClass: null,
			disabled: true,
			display: true,
			event: 'click',
			position: 'after',
			text: 'button',
			wrapClass: ''
		};
    	
		$.extend(opts, options);
		
		var self = this;
		$.each(opts, function (k, v) {
			self[k] = v;
		});
    };
    ap.ui.gridBtn.prototype = {
		onEvent: function(table, rows, checkboxes, gridOptions, btn) {
    		if (btn.action == 'ajax') {
    			var count = checkboxes.boxes.length;
        		var item = count > 1 ? count + ' selected ' + btn.plural : 'selected ' + btn.singular;
        		var process = false;
        		
        		if (btn.confirm == false) {
        			process = true;
        		} else {
        			var msg = btn.confirmMsg.replace('{#item}', item, 'gi');
        			if (confirm(msg)) {
        				process = true; 
        			}
        		}
        		
        		if (process) {
        			if (btn.removeRows == true) {
        				// Just hide the rows right now so that they can be brought back if there are any errors
	        			$.each(rows, function(i, row) {
	        				table.apGrid('hideRow', row);
	        			});
	        			table.apGrid('update');
        			}
        			ap.ui.showActivityIndicator();
        			$.ajax({
        				data: checkboxes.data,
        				dataType: 'json',
        				error: function (request, textStatus, errorThrown) {
	        				if (request.status == '404') {
	        					var msg = btn.error404Msg.replace('{#item}', item, 'gi');
	        					msg.replace('{#plural}', btn.plural, 'gi');
	        					msg.replace('{#singular}', btn.singular, 'gi');
	        					ap.ui.showError(msg);
	        				} else if (textStatus == 'parsererror') {
	        					ap.ui.showError('The information was submitted and most likely saved, however there was a problem parsing the response. Please reload the page to ensure that the information was saved. If it was not saved please contact your website developer and report this error to them.');
	        				} else {
	        					var msg = btn.errorMsg.replace('{#item}', item, 'gi');
	        					msg.replace('{#plural}', btn.plural, 'gi');
	        					msg.replace('{#singular}', btn.singular, 'gi');
	        					ap.ui.showError(msg);
	        				}
	        				if (btn.removeRows == true) {
		        				$.each(rows, function(i, row) {
		        					table.apGrid('showRow', row);
		        				});
		        				table.apGrid('update');
	        				}
	        			},
	        			success: function(response) {
	        				if (response.error == undefined) {
	        					ap.ui.showMessage(response.msg);
	        					var highlight = true;
	        					if (btn.removeRows == true) {
		        					$.each(rows, function(i, row) {
		        						table.apGrid('removeRow', row);
		        					});
		        					table.apGrid('update');
		        					highlight = false;
	        					}
	        					if (response.table != undefined) {
	        						// Update one or more columns for all selected rows
	        						if (response.table.columns != undefined && $.isArray(response.table.columns)) {
	        							// response.table example: response.table.columns[{key: 'colKey', content: 'content'}]
	        							$.each(response.table.columns, function(i, column){
	        								if (column.key != undefined && column.content != undefined) {
	        									table.apGrid('editColumn', column.key, rows, column.content);
	        								}
	        							});
	        							highlight = false;
	        						}
	                            }
	        					// Update content if necessary
	        					if (response.update != undefined && $.isArray(response.update)) {
	        						// response.update example: [{selector: '#selector', content: 'content'}, {selector: '#selector2', content: 'content 2'}]
	        						$.each(response.update, function(){
	        							if (this.selector != undefined && this.content != undefined) {
	        								$(this.selector).html(this.content);
	        							}
	        						});
	        					}
	        				} else {
	        					ap.ui.showError(response.error);
	        					if (btn.removeRows == true) {
		        					$.each(rows, function(i, row) {
		        						table.apGrid('showRow', row);
		        					});
		        					table.apGrid('update');
	        					}
	        					highlight = false;
	        				}
	        				if (highlight) {
	        					table.apGrid('hightlightRows', rows);
	        				}
	        				table.apGrid('unselectAll');
	        				table.apGrid('uncheckSelectAllBox');
	        			},
	        			type: 'POST',
	        			url: btn.url
        			});
        		}
    		} else if (btn.action == 'post') {
    			table.parents('form:first').attr('action', btn.url).attr('method', 'post').submit();
    		}
    	}
    };
    

    /**
    * Handles the My Account link
    */
    ap.myAccount = {
        init: function() {
            var self = this;
            $('#ap-my-account').click(function() {
                var dialog = new ap.ui.dialog({title: 'My Account'}, {ajaxForm: true});
                dialog.open(apAdminBaseUrl + 'user/account');
                return false;
            });
        }
    };

    /**
    * Provides functionality for showing and hiding the My Apps links
    */
    ap.myApps = {
        isOpen: false,
        container: null,
        containerInner: null,
        accordion: null,
        isAccordion: false,
        titlebar: null,
        link: null,
        overlay: null,
        height: 0,
        init: function() {
            var self = this;
            this.container = $('#ap-toolbar-nav');
            if (this.container.length > 0) {
                this.link = $('#ap-my-apps');
                this.containerInner = $('#ap-toolbar-nav-inner');
                this.titlebar = $('div.ap-titlebar', this.container);
                this.overlay = $('<div class="ui-widget-overlay" style="display: none; cursor: pointer; z-index: 99;"></div>').appendTo('body');
                this.overlay.click(function() {
                    self.close();
                });
                this.accordion = $('#ap-apps-nav', this.container);
                this.setHeight();
                //                $('div.ap-apps-nav-header', this.container).click(function() {
                //                    $(this).next().toggle();
                //                    return false;
                //                }).next().hide();

                this.link.click(function() {
                    if (self.isOpen) {
                        self.link.blur();
                        self.close();
                    } else {
                        self.link.blur();
                        self.open();
                    }
                    return false;
                });

                var closeLink = $('a.ap-titlebar-close', this.container).hover(
                function() {
                    closeLink.addClass('ui-state-hover');
                },
                function() {
                    closeLink.removeClass('ui-state-hover');
                }
                ).click(function() {
                    self.close();
                    $(this).blur();
                    closeLink.removeClass('ui-state-hover');
                    return false;
                });
            }
        },
        setHeight: function() {
            if (this.container.length != undefined && this.container.length > 0) {
                this.height = ap.window.height * 0.75;
                if (this.height > 500) {
                    this.height = 500;
                }
                this.container.height(this.height);
                this.overlay.height(ap.document.height);
                this.overlay.width(ap.document.width);
            }
        },
        open: function() {
            var self = this;
            this.overlay.show();
            this.isOpen = true;
            if (!self.isAccordion) {
                self.accordion.hide();
            }
            this.container.slideDown('fast', function() {
                self.containerInner.height(self.height - self.titlebar.height() - 32);
                if (!self.isAccordion) {
                    self.accordion.show();
                    self.accordion.accordion({active: false, autoHeight: false, collapsible: true});
                    self.isAccordion = true;
                }
            });
            this.link.addClass('active');

            ap.ui.disableBodyScroll();
        },
        close: function() {
            this.overlay.hide();
            this.isOpen = false;
            this.container.slideUp('fast', function() {
                ap.myApps.link.removeClass('active');
            });
            ap.ui.enableBodyScroll();
        }
    };

    ap.ui.page = {
        pages: {},
        pageToShow: null,
        blocking: false,
        tinymce: false,
        currentId: null,
        register: function(id, options) {
            var opt = {
                obj: '',
                url: ''
            };
            $.extend(opt, options);
            opt.obj = $('#' + id);
            this.pages[id] = opt;

            if (id == this.pageToShow) {
                this.show(id);
            }
        },
        show: function(selector, reset, tabIndex) {
            ap.ui.hideMessageAndError();
            window.scroll(0, 0);
            if (this.pages[selector] != undefined) {
            	if (reset == true || reset == undefined) {
            		this.removeTinymce();
            	}
                $.each(this.pages, function(id, page) {
                    if (id != selector) {
                        $('#' + id).hide();
                    }
                });
                
                var currentPage = $('#' + selector);
                if (reset == true || reset == undefined) {
                    $('form', currentPage).resetForm();
                    $('.ap-url-key-text', currentPage).show().find('span').html('');
                    $('.ap-url-key-tag', currentPage).hide();
                    $('.ap-app-tag-text', currentPage).show().find('span').html('');
                    $('.ap-app-tag-tag', currentPage).hide();
                }
                this.currentId = selector;
                currentPage.show();
                $(':text.focus, textarea.focus', currentPage).focus();
                tabIndex = tabIndex || 0;
                $('.ui-tabs:first').tabs('select', tabIndex);
                ap.ui.setupFloatingButtons();
                return false;
            } else {
                this.pageToShow = selector;
            }
            return true;
        },
        loadHtml: function(id, html, msg) {
        	ap.ui.hideMessageAndError();
            if (this.pages[id] != undefined) {
            	this.removeTinymce();
            	this.currentId = id;
            	$('#' + id).html(html);
            	ap.ui.page.show(id, false);
                ap.ui.setupFloatingButtons();
                ap.externalLinks();
                ap.ui.setupFormFocus();
                if (msg != undefined) {
                    ap.ui.showMessage(msg);
                }
            	ap.ui.unblock();
            }
            return false;
        },
        load: function(id, url, msg, tabIndex) {
            ap.ui.hideMessageAndError();
            if (this.pages[id] != undefined) {
            	this.removeTinymce();
            	this.currentId = id;
                ap.ui.block();
                var self = this;
                $('#' + id).hide().load(url, function() {
                    ap.ui.page.show(id, false);
                    ap.ui.setupFloatingButtons();
                    ap.externalLinks();
                    ap.ui.setupFormFocus();
                    if (msg != undefined) {
                        ap.ui.showMessage(msg);
                    }
                    ap.ui.unblock();
                    if (ap.ui.pageOnload[id] != undefined && typeof ap.ui.pageOnload[id] == 'function') {
                        ap.ui.pageOnload[id].call();
                    }
                    if (tabIndex > 0) {
                        $('.ui-tabs:first').tabs('select', tabIndex);
                    }
                    self.loadHiddenTinymce('#' + id);
                });
            } else {
            	window.location = url;
            }
            return false;
        },
        loadHiddenTinymce: function(selector) {
        	if (typeof tinyMCE != 'undefined') {
        		if (typeof selector != 'undefined') {
    				$('.needs-editor', $(selector)).each(function() {
    					var id = this.id;
    					if ($('#' + id + ':visible').length > 0) {
	    					var e = tinyMCE.get(id);
	    					if (typeof e == 'undefined') {
	    						tinyMCE.execCommand('mceAddControl', false, id);
	    						$('#' + id).removeClass('needs-editor');
	    					}
    					}
    				});
        		}
        	}
        },
        removeTinymce: function() {
        	if (this.currentId != null && this.currentId.length > 0 && typeof tinyMCE != 'undefined') {
        		var currId = this.currentId;
        		if (currId.substring(0, 1) != '#') {
        			currId = '#' + currId;
        		}
    			$('.ap-editor', $(currId)).each(function() {
    				var id = this.id;
    				try {
	    				tinyMCE.triggerSave();
	    				tinyMCE.execCommand('mceFocus', false, this.id);
	    				tinyMCE.execCommand('mceRemoveControl', false, this.id);
    				} catch (e) {
    				} finally {
    					$('#' + id).css('visibility', 'hidden');
    				}
    			});
    		}
        },
        loadView: function(id, url, link, type) {
        	this.removeTinymce();
        	this.currentId = id;
            ap.ui.hideMessageAndError();
            ap.ui.block();
            type = type || 'replace';
            var self = this;
            $.get(url, function(response) {
            	if (type == 'replace') {
                $(id).replaceWith(response);
            	} else if (type == 'refresh') {
            		$(id).html(response);
            	}
                ap.ui.unblock();
            }, 'html');
            if (link != undefined) {
                link = $(link);
                link.parents('div.ap-viewby:first').find('a').removeClass('active');
                link.addClass('active');
                link.blur();
            }
            return false;
        },
        deletePage: function(options) {
        	var self = this;
        	var id = options.id;
        	var useGrid = typeof options.grid != 'undefined';
        	if (useGrid) {
        		var grid = $(options.grid);
        		grid.apGrid('hideRow', 'tr.row-' + id);
        	}
            $(options.form).ajaxSubmit({
                success: function(response) {
                    if (response.error == undefined) {
                        if (typeof options.load != 'undefined') {
                        	var tabIndex = options.tabIndex || 0;
                    		self.load(options.load, options.loadUrl, response.msg, tabIndex);
                        } else {
                        	ap.ui.showMessage(response.msg);
                        	if (useGrid) {
    	                        grid.apGrid('removeRow', 'tr.row-' + id);
    	                        grid.apGrid('update');
                            }
                        }
                    } else {
                        ap.ui.showError(response.error);
                        if (useGrid) {
	                        grid.apGrid('showRow', 'tr.row-' + id);
	                        grid.apGrid('update');
                        }
                    }
                },
                dataType: 'json'
            });
            if (typeof options.show != 'undefined') {
	            if (options.tabIndex) {
	            		this.show(options.show, true, options.tabIndex);
	            } else {
	            	this.show(options.show);
	            }
            }
        },
        copyPage: function(options) {
            var grid = $(options.grid);
            var id = options.id;
            var self = this;
            ap.ui.block();
            $(options.form).ajaxSubmit({
                success: function(response) {
                    if (response.error == undefined) {
                        self.load(options.show, response.loadUrl, response.msg);
                        if (response.table != undefined) {
                            grid.apGrid('addRow', response.table);
                        }
                    } else {
                        ap.ui.showError(response.error);
                        ap.ui.unblock();
                    }
                },
                dataType: 'json'
            });
        }
    };

    ap.ui.pageSection = {
		sections: {},
		register: function(id, show) {
			this.sections[id] = {obj: $('#' + id)};
			if (show == true) {
				this.show(id);
			}
    	},
    	show: function(selector) {
    		ap.ui.hideMessageAndError();
            window.scroll(0, 0);
            if (this.sections[selector] != undefined) {
            	$.each(this.sections, function(id, section) {
                    if (id != selector) {
                        section.obj.hide();
                    }
                });
            	this.sections[selector].obj.show();
            }
            return false;
    	}
    };
    
    ap.ui.multiselect = function(options) {
        if (options.selector != undefined) {
            var select = $(options.selector);
            // Need to set the height and width to the select menu so that the multiselect component can be created with the correct dimensions.
            var height = select.height();
            var width = select.width() * 2;
            if (width < 600) { // To create enough width for the left column
                width = 600;
            }
            if (height > 0) {
                height += 50;
            } else {
                height = $('option', select).length * 30;
            }
            select.multiselect({height: height, width: width, droppable: 'none', dividerLocation: 0.5});
        }
    };

    /**
    * Holds an object of callback methods for when loading pages.
    * Usage: in the javascript file for the current page set
    * ap.ui.pageOnload.pageId = function() {} where "pageId" is the
    * id of the page to show.  In there place any javascript that will
    * be used to initialize this loaded page.
    */
    ap.ui.pageOnload = {};
    
    /**
    * Sets a select menu to have it's options reloaded via an AJAX call when
    * another form field is changed.
    */
    ap.ui.selectChange = function(options) {
        if (options.selector != undefined && options.url != undefined) {
            this.opts = options;
            this.rel = false;
            
            if (options.relSelector != undefined && options.relSelector.length > 0) {
                this.rel = $(options.relSelector);
            }
            this.select = $(options.selector);
            this.loading = $('<div style="display: none;">Loading...</div>').insertAfter(this.select);

            var self = this;

            if (this.opts.value != undefined) {
            	this.selectedValue = this.opts.value;
            } else {
            	this.selectedValue = this.select.val();
        	}	
            this.select.change(function() {
            	self.selectedValue = self.select.val();
            });
            /**
             * Get which button was clicked. Only want to reload the select menu
             * if the 'save' button was not clickeds
             */
            this.saveActionClicked = false;
            this.saveContinueClicked = false;
            var form = this.select.parents('form:first');
            $(':submit', form).click(function() {
                if (this.name == 'save') {
                    self.saveActionClicked = true;
                    self.saveContinueClicked = false;
                } else {
                    self.saveActionClicked = false;
                    if (this.name == 'saveContinue') {
                    	self.saveContinueClicked = true;
                    } else {
                    	self.saveContinueClicked = false;
                    }
                }
                $(this).blur();
                return true;
            });
            form.ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
                // Only hide the elements if the form url is the same as the AJAX post request
                if ((ap.location.getHostUrl() + ajaxOptions['url']) == this.action && self.saveActionClicked == false) {
                    self.load();
                }
            });
            
            if (this.rel != false) {
                this.rel.change(function() {
                    self.load();
                });
                this.rel.change();
            }
        }
    };

    ap.ui.selectChange.prototype = {
        load: function() {
            var self = this;

            var url = this.opts.url;
            if (this.rel != false) {
                url += '/' + this.rel.val();
            }
            // Get any extra parts of the URL based off of the values of other form fields
            if (this.opts.extras != undefined && this.opts.extras instanceof Array) {
                $.each(this.opts.extras, function(i, extra) {
                    if (extra.selector != undefined && extra.url != undefined) {
                        var v = $(extra.selector).val();
                        if (v != undefined && v.length > 0) {
                            url += extra.url + v;
                        }
                    }
                });
            }
            if (this.opts.loadingShowInline == undefined || this.opts.loadingShowInline == false) {
                this.loading.show().height(this.select.height() + 2); // 2 for the select menu 1px border
            } else {
                this.loading.css('display', 'inline');
            }
            this.select.hide();

            $.ajax({
                dataType: 'json',
                success: function(data) {
                    self.select.loadSelect(data);
                    self.loading.hide();
                    self.select.css('display', 'inline');
                    // The selected value should be unique so after the menu loads again, try to select the appropriate value.
                    // This is needed for edit screens when the select menu loads immediately but the correct value must be selected
                  	self.select.val(self.selectedValue);
                    if (self.opts.callback) {
                        ap.execute(self.opts.callback, self.select);
                    }
                    
                },
                type: 'get',
                url: url
            });
        }
    };

    /**
    * Enables content to be shown or hidden based on the selected
    * value of a select menu.
    */
    ap.ui.selectToggle = {
        init: function(select, toggleElements) {
            select = $(select);


            if (select.length > 0) {
                // Handle when the select menu value changes
                select.change(function() {
                    ap.ui.selectToggle.change(this, toggleElements);
                });
                // Handle the initial selected value of the select menu
                this.change(select.get(0), toggleElements);
            }
        },
        change: function(select, toggleElements) {
            var value = select.options[select.selectedIndex].value;
            $.each(toggleElements, function(toggleValue, toggleEl) {
                if (toggleValue == value) {
                	$(toggleEl).show();
                	ap.ui.page.loadHiddenTinymce(toggleEl);
                	
                    //$(toggleEl).slideDown('fast', function() {ap.ui.page.loadHiddenTinymce();});
                } else {
                	$(toggleEl).hide();
                    //$(toggleEl).slideUp('fast');
                }
            });
        }
    };

    /**
     * Enables a select menu to update another field (most likely a text field) when 
     * it's value changes.
     * 
     * @param array options
     */
    ap.ui.selectUpdate = function(options) {
    	if (options.selector != undefined && options.relSelector != undefined) {
    		this.opts = options;
    		this.init();
    	}
    };
    
    ap.ui.selectUpdate.prototype = {
		opts: {},
		init: function() {
			var self = this;
			var field = $(this.opts.relSelector);
			$(this.opts.selector).change(function() {
            	field.val(this.options[this.selectedIndex].text);
            });
    	}
    };
    
    /**
    * Provides functionality to show or hide content by clicking links
    *
    * Options:
    * showLink: The selector for the link to click on to show the content
    * hideLink: The selector for the link to hide the content
    * elToShow: The selector for the element to initially show
    * elToHide: The selector for the element to initially hide
    * display: (Optional) The css display value to use. Defaults to "block"
    */
    ap.ui.showHide = {
        init: function(options) {
            var showLink = $(options.showLink);
            var hideLink = $(options.hideLink);
            var elToShow = $(options.elToShow);
            var elToHide = $(options.elToHide);
            var display = options.display || 'block';
            showLink.click(function() {
                elToShow.css('display', display);
                elToHide.hide();
                hideLink.css('display', 'inline');
                showLink.hide();
                $(this).blur();
                return false;
            });
            hideLink.click(function() {
                elToHide.css('display', display);
                elToShow.hide();
                showLink.css('display', 'inline');
                hideLink.hide();
                $(this).blur();
                return false;
            });
        }
    };

    /**
    * Creates horizontal or vertical tabs
    */
    ap.ui.tabs = function() {
    	this.nav = null;
    	return this;
    };
    ap.ui.tabs.prototype = {
        vertical: function(selector, options) {
            selector = $(selector);
            var self = this;
            if (selector.length > 0) {
                this.nav = selector.children('ul');
                var opts = {
                	show: function(event, ui) {
                		self.setHeight(ui.panel);
                		ap.ui.page.loadHiddenTinymce(ui.panel);
                	}
                };
                $.extend(opts, options);
                selector.tabs(opts).addClass('ap-vtabs').addClass('ui-helper-clearfix');
                $('li', this.nav).removeClass('ui-corner-top').addClass('ui-corner-tl').addClass('ui-corner-bl');
                selector.children('div').removeClass('ui-corner-bottom').addClass('ui-corner-all').css('minHeight', this.nav.height());
                setTimeout(function() {self.initVertical();}, 5);
            }
        },
        initVertical: function(selector) {
        	selector = selector || 'body';
            $('.ap-vtabs', selector).each(function() {
                var container = $(this);
                var height = container.children('ul').height();
                container.children('div').filter(':visible').css('minHeight', height);
            });
        },
        horizontal: function(selector, options) {
            selector = $(selector);
            var self = this;
            if (selector.length > 0) {
            	var opts = {
                	show: function(event, ui) {
                		self.initVertical(ui.panel);
                		ap.ui.page.loadHiddenTinymce(ui.panel);
                	}
                };
                $.extend(opts, options);
                selector.tabs(opts).addClass('ap-htabs').removeClass('ap-helper-clearfix');
            }
        },
        setHeight: function(panel, height) {
            height = height || 0;
            if (height == 0 && this.nav != undefined) {
                height = this.nav.height();
            }
            $(panel).css('minHeight', height);
        }
    };

    /**
    * Provides the javascript functionality for the formTextEditor UI component
    *
    * options:
    * wrapper: (string) The selector for the div wrapper around the text editor
    */
    ap.ui.tagEditor = function(options) {
        if (options.wrapper != undefined) {
            var self = this;
            var wrapper = $(options.wrapper);
            this.textarea = $('textarea', wrapper);
            $('ul a', wrapper).click(function() {
                var link = $(this);
                var linkClass = link.attr('class');
                var url = '';
                switch (linkClass) {
                    case 'apTagLinkHeader': self.openDialog('header', 'Header Tags'); break;
                    case 'apTagLinkContent': self.openDialog('content', 'Content Block Tags'); break;
                    case 'apTagLinkNavigation': self.openDialog('navigation', 'Navigation Tags'); break;
                    case 'apTagLinkSnippet': self.openDialog('snippet', 'Snippet Tags'); break;
                    case 'apTagLinkMiscellaneous': self.openDialog('miscellaneous', 'Miscellaneous Tags'); break;
                    case 'apTagLinkBreadcrumb': self.textarea.apCaret({text: '{ap_template:breadcrumb}'}); break;
                }
                return false;
            });
        }
    };

    ap.ui.tagEditor.prototype = {
        openDialog: function(url, title)
        {
            var self = this;
            url = apAdminBaseUrl + 'tags/' + url;
            ap.ui.setupDialog(url, {title: title}, {load: function(dialog) {
                var dialogObj = this;
                $('a', dialog).click(function() {
                    dialogObj.close();
                    self.textarea.apCaret({text: $(this).html()});
                    return false;
                });
            }});
        }
    };

    /**
    * Provides functionality to copy the value from one text box to another and
    * optionally to another HTML element.
    *
    * Options:
    * original: The selector for the original textbox
    * target: The selector for the target textbox
    * targetHtml: (Optional) The selector for the additional HTML element to copy the original value to.
    */
    ap.ui.textCopy = {
        init: function(options) {
            var original = $(options.original);
            var target = false;
            if (options.target != undefined) {
                var target = $(options.target);
            }
            var targetHtml = false;
            if (options.targetHtml != undefined) {
                targetHtml = $(options.targetHtml);
            }
            var format = options.format || 'plain';
            var originalFormat = options.originalFormat || 'plain';
            var value;
            original.apTextChange(function() {
                if (format == 'url') {
                    value = ap.filter.key(this.value);
                } else if (format == 'tag') {
                    value = ap.filter.tag(this.value);
                } else {
                    value = this.value;
                }
                if (target) {
                    target.val(value);
                }
                if (targetHtml) {
                    targetHtml.html(value);
                }
                if (originalFormat == 'url') {
                    this.value = value;
                }
            });
        }
    };

    /**
    * Provides functionality for URL Key text boxes
    */
    ap.ui.urlKey = {
        init: function(options) {
            var keyBox = $(options.keyBox);
            var keyText = $(options.keyText);
            var form = keyBox.parents('form:first');
            var allowBrackets = options.allowBrackets || false;
            keyBox.apTextChange(function() {
                this.value = ap.filter.key(this.value, allowBrackets);
                keyText.html(this.value);
            });
            if (options.relBox != undefined) {
                $(options.relBox, form).apTextChange(function() {
                    var value = ap.filter.key(this.value, allowBrackets);
                    keyBox.val(value);
                    keyText.html(value);
                });
            }
            if (options.relBoxes != undefined && options.relBoxes.length > 0) {
                $.each(options.relBoxes, function(i, box) {
                    $(box, form).apTextChange(function() {
                        var value = '';
                        // Loop through all of the rel boxes again to build the value
                        $.each(options.relBoxes, function(i, relBox) {
                            var relBox = $(relBox, form);
                            if (relBox.length > 0) {
                                value += ' ' + relBox.val();
                            }
                        });
                        value = ap.filter.key(value, allowBrackets);

                        keyBox.val(value);
                        keyText.html(value);
                    });
                });
            }
            $(options.keyEdit).click(function() {
                $(this).blur();
                $(options.keyTextWrapper).hide();
                $(options.keyBoxWrapper).show().children(':input').focus();
                return false;
            });
        }
    };
})(jQuery);