AJS.Editor = (function($) { return {
    // Save the last edit mode in case the user changes to preview and from there to the other edit mode...
    // then we will have to convert the markup to XHTML or vice verca.
    lastEditMode: null,
    lastKnownGoodContent: null,
    contentHasChangedSinceLastAutoSave: false,
    isDraftSaved: false,
    originalWikiContent: "",

    syncTitleFieldWithForm: function() {
        var hiddenContentTitle = AJS.$("#hidden-content-title");
        if (hiddenContentTitle.length) {
            // Title field has been moved out of form to top of page,
            // copy the current field value into hidden field if value written.
            var title = "";

            var titleWrittenField = AJS.$("#titleWritten");
            if (!titleWrittenField.length || titleWrittenField.val() != "false") {
                // Creating a page - title may be "New Page" as placeholder, don't copy.
                title = AJS.$("#content-title").val();
            }

            hiddenContentTitle.val(title);
        }
    },

    // Save/Cancel fire unload, but draft shouldn't be saved.
    isSubmitting: false,

    // Flag used to determine if handleUnload function should run.
    isUnloaded: false,

    hasContentChanged : function () {
        var rte = AJS.params.useWysiwyg && this.inRichTextMode();
        if (!rte && !this.contentHasChangedSinceLastAutoSave)
            return false;

        return this.editorHasContentChanged(rte);
    },

    editorHasContentChanged: function (isRTEMode) {
        if (isRTEMode)
            return this.Adapter.editorHasContentChanged();

        return this.originalWikiContent != this.getCurrentFormContent();
    },

    /**
     * Saves a draft.
     *
     * @param options
     *      async : whether the xml http request should operate synchronously or asynchronously
     *      onSuccessHandler : callback that is invoked on draft save success. Function should be formatted like this: function (responseData, isEditingNewPage) {}
     *      onErrorHandler : callback that is invoked on draft save error. Function should be formatted like this: function (responseBody) {}
     *      forceSave : forces a draft save even if there are no content changes (that is, AJS.Editor.hasContentChanged() == false)
     */
    saveDraft: function (options) {
        var defaults = { async: true };
        if (typeof options == "boolean") { // to enable backwards compatability with saveDraft(boolean async) {}
            options = { async: options };
        } else if (typeof options == "number") { // firebug is reporting that this method is being called with weird numbers - I can't for life of me find in the code where
            options = defaults;
        } else {
            options = AJS.$.extend({}, defaults, options);
        }

        if (!AJS.params.saveDrafts || AJS.Editor.isSubmitting || (!options.forceSave && !AJS.Editor.hasContentChanged())) {
            return;
        }
        AJS.Editor.syncTitleFieldWithForm();
        var form = AJS.Editor.getCurrentForm();
        var draftData = {
            pageId : AJS.params.pageId,
            type : AJS.params.draftType,
            title : AJS.$("#hidden-content-title").val(),
            content : AJS.Editor.getCurrentFormContent()
        };

        var newSpaceKey = AJS.$("#newSpaceKey");
        if (newSpaceKey.length) {
            draftData.spaceKey = newSpaceKey.val();
        } else {
            draftData.spaceKey = encodeURIComponent(AJS.params.spaceKey);
        }
        var originalVersion = AJS.$("#originalVersion");
        if (originalVersion.length) {
            draftData.pageVersion = parseInt(originalVersion.val(), 10);
        }

        var draftStatus = AJS.$("#draft-status");
        var resetWysiwygContent = AJS.params.useWysiwyg && AJS.Editor.inRichTextMode();

        var jsTime = function (date) { // dodgy time function
            var h = date.getHours();
            var m = date.getMinutes();
            var ampm = h > 11 ? "PM" : "AM";
            h = h % 12;
            return (h == 0 ? "12" : h) + ":" + (m < 10 ? "0" : "") + m + " " + ampm;
        };

        var saveDraftCallback = function (response) {
            AJS.Editor.contentHasChangedSinceLastAutoSave = false;
            if (resetWysiwygContent) {
                AJS.Editor.Adapter.editorResetContentChanged();
            }
            if (response.success)
            {
                AJS.Editor.isDraftSaved = true;
                var detail = {};
                try {
                    detail = eval("(" + response.response + ")");
                } catch (e) {
                    // ignore exception in eval
                }
                var time = detail.time || jsTime(new Date());
                draftStatus.removeClass("error");
                if (AJS.params.newPage)
                    draftStatus.html(AJS.format(AJS.params.draftSavedMessageNew, time));
                else
                    draftStatus.html(AJS.format(AJS.params.draftSavedMessage, time, "<a id='view-diff-link-heartbeat' class='view-diff-link' href=#>", "</a>"));
                if (!AJS.params.contentId || AJS.params.contentId === "0")
                    AJS.params.contentId = detail.draftId;
                if (AJS.$.isFunction(options.onSuccessHandler)) {
                    options.onSuccessHandler(detail, AJS.params.newPage);
                }
            } else {
                draftStatus.addClass("error");
                draftStatus.html(response.response);
                if (AJS.$.isFunction(options.onErrorHandler)) {
                    options.onErrorHandler(response.response);
                }
            }
        };
        draftStatus.html(AJS.params.draftSavingMessage);
        DraftAjax.saveDraft(draftData, form.xhtml.value == "true", {
            callback: saveDraftCallback,
            async: options.async,
            errorHandler: function () { saveDraftCallback({ success: false, response: AJS.params.draftSavingTimedOutMessage }); },
            timeout: 30000 // 30 seconds
        });
    },

    // function to send the form to discard/use the draft
    sendFormDraft: function(flagName) {
        this.handleBeforeUnload = function() {};
        var form = this.getCurrentForm();

        this.addHiddenElement(form, flagName, "true");
        this.addHiddenElement(form, "contentChanged", "" + this.hasContentChanged());
        this.addHiddenElement(form, "pageId", AJS.params.pageId);
        if (!form.spaceKey) {
            this.addHiddenElement(form, "spaceKey", AJS.params.spaceKey);
        }

        form.action =  (AJS.params.newPage ? "create" : "edit") + AJS.params.draftType + ".action";
        form.submit();
    },

    /**
     * Returns a relative URL to resume the draft saved for this page
     */
    getResumeDraftUrl: function () {
        var urlParts = [];
        urlParts.push(contextPath);
        urlParts.push("/pages/" + (AJS.params.newPage ? "create" : "edit") + AJS.params.draftType + ".action");
        urlParts.push("?useDraft=true");
        urlParts.push("&pageId=" + AJS.params.pageId);
        urlParts.push("&contentChanged=" + this.hasContentChanged());
        this.getCurrentForm().spaceKey && urlParts.push("&spaceKey=" + AJS.params.spaceKey);
        return urlParts.join("");
    },

    addHiddenElement : function (form, name, value) {
        var el = document.createElement("input");
        el.type = "hidden";
        el.name = name;
        el.value = value;
        form.appendChild(el);
    },

    getCurrentFormContent : function() {
        var form = this.getCurrentForm();
        if (AJS.params.useWysiwyg && form.xhtml.value == 'true') {
            return this.Adapter.getEditorHTML();
        }
        if (form.markupTextarea) {
            return form.markupTextarea.value;
        }
    },

    /* This function will be invoked when the form gets submitted. */
    contentFormSubmit: function(e) {
        this.handleBeforeUnload = function() {};
        this.syncTitleFieldWithForm();

        AJS.$("#locationShowing").val("" + AJS.isVisible("#location_div"));
        AJS.$("#labelsShowing").val("" + AJS.isVisible("#labels_div"));

        // CONF-12750 Disable the title field outside the form
        // to prevent Safari 2.0 from sending the "title" field twice
        AJS.$(".editable-title #content-title").attr("disabled","disabled");

        this.isSubmitting = this.checkCaptchaResponse(e);
        return this.isSubmitting;
    },

    // Method checks whether the captchaResponse textfield is empty.
    checkCaptchaResponse: function(e) {
        if (e.target.name == "cancel") {
            return true;
        }

        var captchaTextField = AJS.$("#captchaResponse");

        if (captchaTextField.val() == "") {
            AJS.$("#captchaError").css("display", "block");
            window.scroll(0, 0);
            e.stopPropagation();
            return false;
        }
        return true;
    },

    heartbeat: function() {
        HeartbeatAjax.startActivity(AJS.params.pageId, AJS.params.draftType,
            function(activityResponses) {
                var otherUsersAreEditing = activityResponses.length;
                if (otherUsersAreEditing) {
                    var outerspan = AJS.$("#other-users-span");
                    outerspan.empty();
                    for (var i = 0; i < otherUsersAreEditing; ++i) {
                        if (i > 0) {
                            outerspan.append(", ");
                        }

                        var activityResponse = activityResponses[i];
                        outerspan.append(AJS('a').attr('href', AJS.params.contextPath + '/display/~' + encodeURIComponent(activityResponse.userName)).text(activityResponse.fullName));
                        if (activityResponse.lastEditMessage != null) {
                            outerspan.append(" ").append(AJS('span').addClass('smalltext').text(activityResponse.lastEditMessage));
                        }
                    }
                }
                AJS.setVisible("#heartbeat-div", !!otherUsersAreEditing);
            }
        );
    },

    disableFrame: function(body) {
        //disable all forms, buttons and links in the iframe
        AJS.$("form", body).each(function() {
            AJS.$(this).unbind();
            this.onsubmit = function() {
                return false;
            };
        });
        AJS.$("a", body).each(function() {
            AJS.$(this).attr("target", "_top").unbind();
        });
        AJS.$("input", body).each(function() {
            AJS.$(this).unbind();
        });
    },

    /* This function should be invoked when the preview frame has finished loading its content.
       It is responsible for updating the height of frame body to the actual content's height.
      */
      previewFrameOnload: function (body, iframe) {
          AJS.Editor.disableFrame(body);
          var $iframe = AJS.$(iframe || "#previewArea iframe"),
              prevHeight = 0,
              counter = 0,
              content = AJS.$("#main", body)[0],
              originalHeight = $iframe.height();

          content && (function () {
              var height = content.scrollHeight;
              if (prevHeight != height) {
                  if (height != $iframe.height()) {
                      $iframe.height(Math.max(height, originalHeight)); // never make it smaller than the default height
                  }
                  prevHeight = height;
                  counter = 0;
              } else {
                  counter++;
              }
              // uppper limit check for content height changes
              if (counter < 500) {
                  setTimeout(arguments.callee, 500);
              }
          })();
      },

    showRichText : function (show) {
        if (!AJS.params.useWysiwyg)
            return;

        AJS.setVisible("#wysiwyg", show);
        AJS.setCurrent("#wysiwygTab", show);

        if (show) {
            this.Adapter.onShowEditor();
            // now we are in rich text mode, and may change the content, so any value in lastKnownGoodContent is obsolete
            this.lastKnownGoodContent = null;
            AJS.$("#main").addClass("active-richtext");
        }
        else {
            this.Adapter.onHideEditor();
            AJS.$("#main").removeClass("active-richtext");
        }
    },

    showMarkup: function (show) {
        var form = this.getCurrentForm(),
            fname1 = (show ? "removeClass" : "addClass"),
            fname2 = (show ? "addClass" : "removeClass");
        AJS.$("#markup")[fname1]("hidden");
        AJS.$("#markupTab")[fname2]("current");
        AJS.$("#sidebar")[fname1]("hidden");
        AJS.$("#addcomment-sidebar")[fname1]("hidden");
        AJS.$(form)[fname2]("markup");
        AJS.$("#linkinserters")[fname1]("hidden");
        AJS.$("#main")[fname2]("active-wikimarkup");
    },

    showPreview : function (show) {
        var fname1 = (show ? "removeClass" : "addClass"),
            fname2 = (show ? "addClass" : "removeClass");
        AJS.$("#preview")[fname1]("hidden");
        AJS.$("#previewTab")[fname2]("current");
        AJS.$("#main")[fname2]("active-preview");
    },

    /**
    * Set up the page for rich text or markup editing
    */
    setMode : function(mode) {
        var wasRichText = this.inRichTextMode();
        var form = this.getCurrentForm();

        if (mode != AJS.params.actionPreview) {
            AJS.$("input[name=xhtml]", this.getCurrentForm()).val(mode == AJS.params.actionRichtext);
        }

        if (AJS.params.remoteUser && AJS.params.useWysiwyg) {
            this.showDefaultEditorLinks(mode);
        }

        // DON'T CHANGE THE ORDERING OF SHOWS
        // FIREFOX RENDERING GLITCHES WHEN PAGE LOADS TOO QUICKLY (if showMarkup() isn't first)
        if (mode == AJS.params.actionRichtext) {
            this.showMarkup(false);
            this.showRichText(true);
            this.showPreview(false);
        }
        else if (mode == AJS.params.actionMarkup) {
            this.showMarkup(true);
            this.showRichText(false);
            this.showPreview(false);

            // CONF-18837. IE8 needs px size to avoid textarea scrolling-on-selection bug
            if ($.browser.msie && $.browser.version.charAt() == 8) {
                var wikiMarkupElement = AJS.$("#markup");
                AJS.$("#markupTextarea").width(wikiMarkupElement.width()).height(wikiMarkupElement.height());
            }
        } else if (mode == AJS.params.actionPreview) {
            if (wasRichText) {
                // get the editor content in case we come back to wiki-markup
                this.lastKnownGoodContent = this.Adapter.getEditorHTML();
            }
            this.showPreview(true);
            this.showRichText(false);
            this.showMarkup(false);
        }

        AJS.$("input[name=mode]", form).val(mode);
    },

    /**
     * Returns the ID of the appropriate content object to use when rendering the editor's content.
     * For pages, blogs, existing comments or drafts it is the ID of that object.
     * For new comments it is the ID of the page or blog to which the comment belongs.
     */
    getContentId : function() {
        if (+AJS.params.contentId)
            return AJS.params.contentId;
        if (+AJS.params.pageId)
            return AJS.params.pageId;
        return "0"; // ensure we always return "0" or an actual id.
    },

    changeMode : function(newMode) {

        //## allowModeChange() only exists when WYSIWYG is enabled, so don't do a check otherwise (CONF-4935)
        // if the editor is in a state where the mode chnage will break things (e.g. not yet fully initialised)
        // don't allow the change
        if (AJS.params.useWysiwyg && this.inRichTextMode() && !AJS.Editor.Adapter.allowModeChange()) {
            return false;
        }

        var oldMode = AJS.$("input[name=mode]", this.getCurrentForm()).val();
        if (oldMode == newMode) {
            return false;
        }

        this.showWaitImage(true);

        if (AJS.params.saveDrafts) {
            // If the contentId is "0" we want to make sure we
            // save the draft before loading the content (by attempting to force it to run synchronously).
            var async = (AJS.params.contentId === "0" ? false : true);
            this.saveDraft(async);
        }

        var contentId = this.getContentId();
        if (newMode == AJS.params.actionMarkup) {
            if (oldMode == AJS.params.actionPreview) {
                if (AJS.Editor.lastEditMode == AJS.params.actionMarkup) { // Markup -> Preview -> Markup (no conversion)
                    this.replysetTextArea(null);
                }
                else { // WYSIWYG -> Preview -> Markup (convert HTML to wiki markup)
                    WysiwygConverter.convertXHtmlToWikiMarkupWithoutPage(AJS.Editor.lastKnownGoodContent, contentId, this.replysetTextArea);
                }
            }
            else { // WYSIWYG -> Markup, so just convert
                WysiwygConverter.convertXHtmlToWikiMarkupWithoutPage(AJS.Editor.Adapter.getEditorHTML(), contentId, this.replysetTextArea);
            }
        }
        else if (newMode == AJS.params.actionRichtext) {
            // If the current mode is preview...
            if (oldMode == AJS.params.actionPreview && AJS.Editor.lastEditMode == AJS.params.actionRichtext) {
                // WYSIWYG -> Preview -> WYSIWYG
                // We don't need to reload or convert the contents of the tinyMCE editor
                this.replysetEditorValue(null);
            } else {
                // Markup -> Preview -> WYSIWYG
                // Convert the markup to be used with WYSIWYG
                // Markup -> WYSIWYG, so just grab the contents of the markup textarea and convert it to be used with WYSIWYG
                WysiwygConverter.convertWikiMarkupToXHtmlWithoutPageWithSpaceKey(AJS.$("#markupTextarea").val(), contentId, AJS.params.spaceKey, this.replysetEditorValue);
            }
        }
        else { // Preview
            var queryParams = { "contentId": contentId,
                                "contentType": AJS.params.contentType,
                                "spaceKey": AJS.params.spaceKey };

            if (oldMode == AJS.params.actionRichtext) { // WYSIWYG -> Preview
                AJS.Editor.lastEditMode = AJS.params.actionRichtext;
                AJS.Editor.lastKnownGoodContent = queryParams.xHtml = AJS.Editor.Adapter.getEditorHTML();
            }
            else { // Markup -> Preview
                AJS.Editor.lastEditMode = AJS.params.actionMarkup;
                queryParams.wikiMarkup = AJS.$("#markupTextarea").val();
            }
            AJS.$.post(AJS.params.contextPath + "/pages/rendercontent.action", queryParams, AJS.Editor.replysetPreviewArea);
        }

        return false;
    },

    showWaitImage : function (flag) {
        AJS.$("#wysiwygWaitImage").css("visibility", (flag ? "visible" : "hidden"));
    },

    replysetTextArea : function (s) {
        AJS.Editor.showWaitImage(false);
        AJS.Editor.setMode(AJS.params.actionMarkup);
        if (s != null) {
            AJS.$("#markupTextarea").val(s);
            if (AJS.params.saveDrafts)
            {
                AJS.Editor.originalWikiContent = s;
            }
        }
    },

    replysetEditorValue : function (s) {
        AJS.Editor.showWaitImage(false);
        AJS.Editor.setMode(AJS.params.actionRichtext);
        AJS.Editor.Adapter.setEditorValue(s);
    },

    replysetPreviewArea : function (html) {
        AJS.Editor.showWaitImage(false);
        AJS.Editor.setMode(AJS.params.actionPreview);
        // Set the iframe source to an empty JS statement to avoid secure/nonsecure warnings on https, without
        // needing a back-end call.
        var src = AJS.params.staticResourceUrlPrefix + "/blank.html";
        AJS.$("#previewArea").html('<iframe src="' + src + '" scrolling="no" frameborder="0"></iframe>');
        var iframe = AJS.$("#previewArea iframe")[0];
        var doc = iframe.contentDocument || iframe.contentWindow.document;
        doc.write(html);
        doc.close(); // for firefox
    },

    inRichTextMode : function () {
        return AJS.$("input[name=mode]", this.getCurrentForm()).val() == AJS.params.actionRichtext;
    },

    // Called by Adapter oninit
    onInit : function () {
        AJS.Editor.setMode(AJS.params.editorMode);
    },

    handleUnload : function() {
        if (AJS.Editor.isUnloaded) {
            return;
        }

        AJS.Editor.isUnloaded = true;
        if (AJS.params.saveDrafts) {
            AJS.Editor.saveDraft(false);
        }
    },

    /**
     * Returns a string which represents the message to display when a user navigates away from editing a page.
     */
    handleBeforeUnload: function() {
        if (typeof seleniumAlert != "undefined") { // TODO: Find a better way to detect Selenium.
            return;
        }

        // You can't rely on the draft being saved before this.
        if (AJS.Editor.hasContentChanged()) {
            if (AJS.params.saveDrafts) {
                return AJS.params.onBeforeUnloadMessageDraft;
            }

            return AJS.params.onBeforeUnloadMessageLost;
        }
        else if (AJS.Editor.isDraftSaved) {
            return AJS.params.onBeforeUnloadMessageDraft;
        }
    },

    storeTextareaBits: function (doNotFocus) {
        return AJS.Editor.Markup.storeTextareaBits(this.getCurrentForm(), AJS.$("#markupTextarea")[0], doNotFocus);
    },

    setRichTextDefault : function (value) {
        AjaxUserProfileEditor.setPreferenceUserEditWysiwyg(value);
        AJS.Editor.editorPreference = (value ? AJS.params.actionRichtext : AJS.params.actionMarkup);
        AJS.$("#makeRichTextDefault").addClass("hidden");
        AJS.$("#makeMarkupDefault").addClass("hidden");
    },

    // Hide and show the "make default" editor links, based on what mode the user is currently in
    showDefaultEditorLinks : function (currentMode) {
        var defaultIsWysiwyg = (AJS.Editor.editorPreference == AJS.params.actionRichtext);
        var showRichTextDefault, showMarkupDefault = false;

        // If we are in MARKUP mode, show the text to set markup as default
        if (defaultIsWysiwyg && currentMode == AJS.params.actionMarkup) {
            showMarkupDefault = true;
        }
        // If we are in RICHTEXT mode, show the text to set richtext as default
        else if (!defaultIsWysiwyg && currentMode == AJS.params.actionRichtext) {
            showRichTextDefault = true;
        }

        AJS.$("#makeRichTextDefault")[showRichTextDefault ? "removeClass" : "addClass"]("hidden");
        AJS.$("#makeMarkupDefault")[showMarkupDefault ? "removeClass" : "addClass"]("hidden");
    },

    contentChangeHandler : function () {
        this.contentHasChangedSinceLastAutoSave = true;
    },

    getCurrentForm : function() {
        return AJS.$("form[name=" + AJS.params.formName + "]")[0];
    },

    openMacroBrowser : function(e) {
        var t = AJS.Editor,
            mb = AJS.MacroBrowser,
            textarea = $("#markupTextarea");

        // store the current selection & scroll for later when we insert macro
        var range = t.Markup.selection = textarea.selectionRange();
        t.Markup.scrollTop = textarea.scrollTop();

        var selectedMacro = mb.getSelectedMacro(range.textBefore, textarea.val());
        mb.open({
            markupMode : true,
            selectedMacro : selectedMacro,
            selectedMarkup : range.text,
            onComplete : AJS.Editor.macroBrowserComplete,
            onCancel : AJS.Editor.macroBrowserCancel
        });
        return AJS.stopEvent(e);
    },

    // Constructs and inserts the macro markup from the insert macro page.
    macroBrowserComplete : function(macro) {
        var t = AJS.Editor,
            textarea = $("#markupTextarea"),
            m = AJS.MacroBrowser.selectedMacro;
        if (m) { // select and replace the current macro markup
            textarea.selectionRange(m.startIndex, m.startIndex + m.markup.length);
        }
        else if (t.Markup.selection) {
            textarea.selectionRange(t.Markup.selection.start, t.Markup.selection.end);
        }
        textarea.selection(macro.markup);
        textarea.scrollTop(t.Markup.scrollTop);
    },
    macroBrowserCancel : function() {
        var t = AJS.Editor,
            textarea = $("#markupTextarea");
        if (t.Markup.selection) {
            textarea.selectionRange(t.Markup.selection.start, t.Markup.selection.end);
        }
        textarea.scrollTop(t.Markup.scrollTop);
    }
};})(AJS.$);

AJS.toInit(function ($) {

    AJS.Editor.editorPreference = AJS.params.editorMode;

    $("#wysiwygTab a:first").click(function (e) {
        AJS.Editor.changeMode(AJS.params.actionRichtext);
        return AJS.stopEvent(e);
    });

    $("#markupTab a:first").click(function (e) {
        AJS.Editor.changeMode(AJS.params.actionMarkup);
        return AJS.stopEvent(e);
    });

    $("#previewTab a:first").click(function (e) {
        AJS.Editor.changeMode(AJS.params.actionPreview);
        return AJS.stopEvent(e);
    });

    $("#makeRichTextDefault").click(function (e) {
        AJS.Editor.setRichTextDefault(true);
        return AJS.stopEvent(e);
    });

    $("#makeMarkupDefault").click(function (e) {
        AJS.Editor.setRichTextDefault(false);
        return AJS.stopEvent(e);
    });

    $("#editor-insert-macro").click(AJS.Editor.openMacroBrowser);

    $("#markupTextarea").select(function () {
        AJS.Editor.storeTextareaBits(true);
    }).keyup(function (e) {
        AJS.Editor.contentChangeHandler();

        if (e.ctrlKey) {
            if (e.keyCode == 77) {// bind ctrl+m to insert image
                $("#editor-insert-image").click();
                return false;
            }
            if (e.shiftKey && e.keyCode == 65) { // bind ctrl+shift+a to insert macro
                $("#editor-insert-macro").click();
                return false;
            }
        }
    }).change(function () {
        AJS.Editor.contentChangeHandler();
    });

    $(".submit-buttons").click(function (e) {
        AJS.Editor.contentFormSubmit(e);
    });

    $(".editor-template-link").click(function (e) {
        var form = AJS.$("#createpageform")[0];

        if ((AJS.Editor.hasContentChanged() || AJS.Editor.isDraftSaved) && !confirm(AJS.params.templateOverwiteMessage)) {
            return;
        }

        form.action = "createpage-choosetemplate.action";
        AJS.Editor.contentFormSubmit(e);
        form.submit();
    });

    if (AJS.params.useWysiwyg) {
        var errorHandler = function(message) {
            AJS.Editor.showWaitImage(false);
            // Ignore DWR errors because they almost always occur when users
            // click a link or submit during draft/heartbeat transmission.
            // Displaying a message when this occurs is just annoying.
        };
        // Initialisation
        DWREngine.setErrorHandler(errorHandler);
        DWREngine.setWarningHandler(errorHandler);
        // We should note here that the content has NOT finished loading
        AJS.Editor.Adapter.addOnInitCallback(AJS.Editor.onInit);
        AJS.Editor.Adapter.editorOnLoad();
    }

    // bind the function to be run when the preview frame is loaded
    $(window).bind("render-content-loaded", function(e, body) {
        var iframe = $("#previewArea iframe");
        if (iframe.contents().find("body")[0] == body) {
            AJS.Editor.previewFrameOnload(body, iframe);
        }
    });

    window.onbeforeunload = function() {
        return AJS.Editor.handleBeforeUnload();
    };

    if (AJS.params.saveDrafts) {
        $(window).unload(AJS.Editor.handleUnload);
        DraftAjax.getDraftSaveInterval(function (interval) {
                setInterval(AJS.Editor.saveDraft, interval);
            }
        );
    }

    if (AJS.params.heartbeat && AJS.params.pageId != "0") {
        AJS.Editor.heartbeat();
        HeartbeatAjax.getHeartbeatInterval(
            function (interval) { setInterval(AJS.Editor.heartbeat, interval); }
        );
    }

    // Move title field to place of title text
    var titleText = $("#title-text");
    var titleField = $("#content-title");
    if (titleText.length && titleField.length) { //only true for edit page screen in default theme
        var div = document.createElement("div");
        $(div).addClass("editable-title");
        $(div).append(titleField);
        if (!$.browser.msie) { // IE can't use full width due to CSS bugs
            $(window).load(function () { // wait until images are loaded
                if (jQuery("#title-heading img.logo").length > 0) {
                    jQuery(div).css("marginLeft", jQuery("#title-heading img.logo").width() + 10 + "px"); // adjust for custom logos
                }
                else {
                    jQuery(div).css("marginLeft", 0);
                }
            });
        }
        titleText.replaceWith(div);

        // Hidden field title will exist for pages created from links.
        var hiddenFields = $("#hidden-content-title");
        if (!hiddenFields.length) {
            var hiddenField = document.createElement("input");
            hiddenField.id = "hidden-content-title";
            hiddenField.type = "hidden";
            hiddenField.name = "title";
            hiddenField = $(hiddenField);

            var titleWrittenField = $("#titleWritten");
            if (!titleWrittenField.length || titleWrittenField.val() != "false") {
                hiddenField.val(titleField.val());
            }

            var editorDiv = $("#wiki-editor");
            editorDiv.before(hiddenField);
        }
    }

    AJS.Editor.originalWikiContent = AJS.Editor.getCurrentFormContent();
});
/*
 * jQuery Form Plugin
 * version: 2.33 (22-SEP-2009)
 * @requires jQuery v1.2.6 or later
 *
 * Examples and documentation at: http://malsup.com/jquery/form/
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 */
;(function($) {

/*
	Usage Note:
	-----------
	Do not use both ajaxSubmit and ajaxForm on the same form.  These
	functions are intended to be exclusive.  Use ajaxSubmit if you want
	to bind your own submit handler to the form.  For example,

	$(document).ready(function() {
		$('#myForm').bind('submit', function() {
			$(this).ajaxSubmit({
				target: '#output'
			});
			return false; // <-- important!
		});
	});

	Use ajaxForm when you want the plugin to manage all the event binding
	for you.  For example,

	$(document).ready(function() {
		$('#myForm').ajaxForm({
			target: '#output'
		});
	});

	When using ajaxForm, the ajaxSubmit function will be invoked for you
	at the appropriate time.
*/

/**
 * ajaxSubmit() provides a mechanism for immediately submitting
 * an HTML form using AJAX.
 */
$.fn.ajaxSubmit = function(options) {
	// fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
	if (!this.length) {
		log('ajaxSubmit: skipping submit process - no element selected');
		return this;
	}

	if (typeof options == 'function')
		options = { success: options };

	var url = $.trim(this.attr('action'));
	if (url) {
		// clean url (don't include hash vaue)
		url = (url.match(/^([^#]+)/)||[])[1];
   	}
   	url = url || window.location.href || '';

	options = $.extend({
		url:  url,
		type: this.attr('method') || 'GET'
	}, options || {});

	// hook for manipulating the form data before it is extracted;
	// convenient for use with rich editors like tinyMCE or FCKEditor
	var veto = {};
	this.trigger('form-pre-serialize', [this, options, veto]);
	if (veto.veto) {
		log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
		return this;
	}

	// provide opportunity to alter form data before it is serialized
	if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
		log('ajaxSubmit: submit aborted via beforeSerialize callback');
		return this;
	}

	var a = this.formToArray(options.semantic);
	if (options.data) {
		options.extraData = options.data;
		for (var n in options.data) {
		  if(options.data[n] instanceof Array) {
			for (var k in options.data[n])
			  a.push( { name: n, value: options.data[n][k] } );
		  }
		  else
			 a.push( { name: n, value: options.data[n] } );
		}
	}

	// give pre-submit callback an opportunity to abort the submit
	if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
		log('ajaxSubmit: submit aborted via beforeSubmit callback');
		return this;
	}

	// fire vetoable 'validate' event
	this.trigger('form-submit-validate', [a, this, options, veto]);
	if (veto.veto) {
		log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
		return this;
	}

	var q = $.param(a);

	if (options.type.toUpperCase() == 'GET') {
		options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
		options.data = null;  // data is null for 'get'
	}
	else
		options.data = q; // data is the query string for 'post'

	var $form = this, callbacks = [];
	if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
	if (options.clearForm) callbacks.push(function() { $form.clearForm(); });

	// perform a load on the target only if dataType is not provided
	if (!options.dataType && options.target) {
		var oldSuccess = options.success || function(){};
		callbacks.push(function(data) {
			$(options.target).html(data).each(oldSuccess, arguments);
		});
	}
	else if (options.success)
		callbacks.push(options.success);

	options.success = function(data, status) {
		for (var i=0, max=callbacks.length; i < max; i++)
			callbacks[i].apply(options, [data, status, $form]);
	};

	// are there files to upload?
	var files = $('input:file', this).fieldValue();
	var found = false;
	for (var j=0; j < files.length; j++)
		if (files[j])
			found = true;

	var multipart = false;
//	var mp = 'multipart/form-data';
//	multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);

	// options.iframe allows user to force iframe mode
   if (options.iframe || found || multipart) {
	   // hack to fix Safari hang (thanks to Tim Molendijk for this)
	   // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
	   if (options.closeKeepAlive)
		   $.get(options.closeKeepAlive, fileUpload);
	   else
		   fileUpload();
	   }
   else
	   $.ajax(options);

	// fire 'notify' event
	this.trigger('form-submit-notify', [this, options]);
	return this;


	// private function for handling file uploads (hat tip to YAHOO!)
	function fileUpload() {
		var form = $form[0];

		if ($(':input[name=submit]', form).length) {
			alert('Error: Form elements must not be named "submit".');
			return;
		}

		var opts = $.extend({}, $.ajaxSettings, options);
		var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);

		var id = 'jqFormIO' + (new Date().getTime());
		var $io = $('<iframe id="' + id + '" name="' + id + '" src="about:blank" />');
		var io = $io[0];

		$io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });

		var xhr = { // mock object
			aborted: 0,
			responseText: null,
			responseXML: null,
			status: 0,
			statusText: 'n/a',
			getAllResponseHeaders: function() {},
			getResponseHeader: function() {},
			setRequestHeader: function() {},
			abort: function() {
				this.aborted = 1;
				$io.attr('src','about:blank'); // abort op in progress
			}
		};

		var g = opts.global;
		// trigger ajax global events so that activity/block indicators work like normal
		if (g && ! $.active++) $.event.trigger("ajaxStart");
		if (g) $.event.trigger("ajaxSend", [xhr, opts]);

		if (s.beforeSend && s.beforeSend(xhr, s) === false) {
			s.global && $.active--;
			return;
		}
		if (xhr.aborted)
			return;

		var cbInvoked = 0;
		var timedOut = 0;

		// add submitting element to data if we know it
		var sub = form.clk;
		if (sub) {
			var n = sub.name;
			if (n && !sub.disabled) {
				options.extraData = options.extraData || {};
				options.extraData[n] = sub.value;
				if (sub.type == "image") {
					options.extraData[name+'.x'] = form.clk_x;
					options.extraData[name+'.y'] = form.clk_y;
				}
			}
		}

		// take a breath so that pending repaints get some cpu time before the upload starts
		setTimeout(function() {
			// make sure form attrs are set
			var t = $form.attr('target'), a = $form.attr('action');

			// update form attrs in IE friendly way
			form.setAttribute('target',id);
			if (form.getAttribute('method') != 'POST')
				form.setAttribute('method', 'POST');
			if (form.getAttribute('action') != opts.url)
				form.setAttribute('action', opts.url);

			// ie borks in some cases when setting encoding
			if (! options.skipEncodingOverride) {
				$form.attr({
					encoding: 'multipart/form-data',
					enctype:  'multipart/form-data'
				});
			}

			// support timout
			if (opts.timeout)
				setTimeout(function() { timedOut = true; cb(); }, opts.timeout);

			// add "extra" data to form if provided in options
			var extraInputs = [];
			try {
				if (options.extraData)
					for (var n in options.extraData)
						extraInputs.push(
							$('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
								.appendTo(form)[0]);

				// add iframe to doc and submit the form
				$io.appendTo('body');
				io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
				form.submit();
			}
			finally {
				// reset attrs and remove "extra" input elements
				form.setAttribute('action',a);
				t ? form.setAttribute('target', t) : $form.removeAttr('target');
				$(extraInputs).remove();
			}
		}, 10);

		var domCheckCount = 50;

		function cb() {
			if (cbInvoked++) return;

			io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);

			var ok = true;
			try {
				if (timedOut) throw 'timeout';
				// extract the server response from the iframe
				var data, doc;

				doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;

				var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
				log('isXml='+isXml);
				if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
				 	if (--domCheckCount) {
						// in some browsers (Opera) the iframe DOM is not always traversable when
						// the onload callback fires, so we loop a bit to accommodate
						cbInvoked = 0;
						setTimeout(cb, 100);
						return;
					}
					log('Could not access iframe DOM after 50 tries.');
					return;
				}

				xhr.responseText = doc.body ? doc.body.innerHTML : null;
				xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
				xhr.getResponseHeader = function(header){
					var headers = {'content-type': opts.dataType};
					return headers[header];
				};

				if (opts.dataType == 'json' || opts.dataType == 'script') {
					// see if user embedded response in textarea
					var ta = doc.getElementsByTagName('textarea')[0];
					if (ta)
						xhr.responseText = ta.value;
					else {
						// account for browsers injecting pre around json response
						var pre = doc.getElementsByTagName('pre')[0];
						if (pre)
							xhr.responseText = pre.innerHTML;
					}
				}
				else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
					xhr.responseXML = toXml(xhr.responseText);
				}
				data = $.httpData(xhr, opts.dataType);
			}
			catch(e){
				ok = false;
				$.handleError(opts, xhr, 'error', e);
			}

			// ordering of these callbacks/triggers is odd, but that's how $.ajax does it
			if (ok) {
				opts.success(data, 'success');
				if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
			}
			if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
			if (g && ! --$.active) $.event.trigger("ajaxStop");
			if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');

			// clean up
			setTimeout(function() {
				$io.remove();
				xhr.responseXML = null;
			}, 100);
		};

		function toXml(s, doc) {
			if (window.ActiveXObject) {
				doc = new ActiveXObject('Microsoft.XMLDOM');
				doc.async = 'false';
				doc.loadXML(s);
			}
			else
				doc = (new DOMParser()).parseFromString(s, 'text/xml');
			return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
		};
	};
};

/**
 * ajaxForm() provides a mechanism for fully automating form submission.
 *
 * The advantages of using this method instead of ajaxSubmit() are:
 *
 * 1: This method will include coordinates for <input type="image" /> elements (if the element
 *	is used to submit the form).
 * 2. This method will include the submit element's name/value data (for the element that was
 *	used to submit the form).
 * 3. This method binds the submit() method to the form for you.
 *
 * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
 * passes the options argument along after properly binding events for submit elements and
 * the form itself.
 */
$.fn.ajaxForm = function(options) {
	return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
		$(this).ajaxSubmit(options);
		return false;
	}).bind('click.form-plugin', function(e) {
		var $el = $(e.target);
		if (!($el.is(":submit,input:image"))) {
			return;
		}
		var form = this;
		form.clk = e.target;
		if (e.target.type == 'image') {
			if (e.offsetX != undefined) {
				form.clk_x = e.offsetX;
				form.clk_y = e.offsetY;
			} else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
				var offset = $el.offset();
				form.clk_x = e.pageX - offset.left;
				form.clk_y = e.pageY - offset.top;
			} else {
				form.clk_x = e.pageX - e.target.offsetLeft;
				form.clk_y = e.pageY - e.target.offsetTop;
			}
		}
		// clear form vars
		setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10);
	});
};

// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
$.fn.ajaxFormUnbind = function() {
	return this.unbind('submit.form-plugin click.form-plugin');
};

/**
 * formToArray() gathers form element data into an array of objects that can
 * be passed to any of the following ajax functions: $.get, $.post, or load.
 * Each object in the array has both a 'name' and 'value' property.  An example of
 * an array for a simple login form might be:
 *
 * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
 *
 * It is this array that is passed to pre-submit callback functions provided to the
 * ajaxSubmit() and ajaxForm() methods.
 */
$.fn.formToArray = function(semantic) {
	var a = [];
	if (this.length == 0) return a;

	var form = this[0];
	var els = semantic ? form.getElementsByTagName('*') : form.elements;
	if (!els) return a;
	for(var i=0, max=els.length; i < max; i++) {
		var el = els[i];
		var n = el.name;
		if (!n) continue;

		if (semantic && form.clk && el.type == "image") {
			// handle image inputs on the fly when semantic == true
			if(!el.disabled && form.clk == el) {
				a.push({name: n, value: $(el).val()});
				a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
			}
			continue;
		}

		var v = $.fieldValue(el, true);
		if (v && v.constructor == Array) {
			for(var j=0, jmax=v.length; j < jmax; j++)
				a.push({name: n, value: v[j]});
		}
		else if (v !== null && typeof v != 'undefined')
			a.push({name: n, value: v});
	}

	if (!semantic && form.clk) {
		// input type=='image' are not found in elements array! handle it here
		var $input = $(form.clk), input = $input[0], n = input.name;
		if (n && !input.disabled && input.type == 'image') {
			a.push({name: n, value: $input.val()});
			a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
		}
	}
	return a;
};

/**
 * Serializes form data into a 'submittable' string. This method will return a string
 * in the format: name1=value1&amp;name2=value2
 */
$.fn.formSerialize = function(semantic) {
	//hand off to jQuery.param for proper encoding
	return $.param(this.formToArray(semantic));
};

/**
 * Serializes all field elements in the jQuery object into a query string.
 * This method will return a string in the format: name1=value1&amp;name2=value2
 */
$.fn.fieldSerialize = function(successful) {
	var a = [];
	this.each(function() {
		var n = this.name;
		if (!n) return;
		var v = $.fieldValue(this, successful);
		if (v && v.constructor == Array) {
			for (var i=0,max=v.length; i < max; i++)
				a.push({name: n, value: v[i]});
		}
		else if (v !== null && typeof v != 'undefined')
			a.push({name: this.name, value: v});
	});
	//hand off to jQuery.param for proper encoding
	return $.param(a);
};

/**
 * Returns the value(s) of the element in the matched set.  For example, consider the following form:
 *
 *  <form><fieldset>
 *	  <input name="A" type="text" />
 *	  <input name="A" type="text" />
 *	  <input name="B" type="checkbox" value="B1" />
 *	  <input name="B" type="checkbox" value="B2"/>
 *	  <input name="C" type="radio" value="C1" />
 *	  <input name="C" type="radio" value="C2" />
 *  </fieldset></form>
 *
 *  var v = $(':text').fieldValue();
 *  // if no values are entered into the text inputs
 *  v == ['','']
 *  // if values entered into the text inputs are 'foo' and 'bar'
 *  v == ['foo','bar']
 *
 *  var v = $(':checkbox').fieldValue();
 *  // if neither checkbox is checked
 *  v === undefined
 *  // if both checkboxes are checked
 *  v == ['B1', 'B2']
 *
 *  var v = $(':radio').fieldValue();
 *  // if neither radio is checked
 *  v === undefined
 *  // if first radio is checked
 *  v == ['C1']
 *
 * The successful argument controls whether or not the field element must be 'successful'
 * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
 * The default value of the successful argument is true.  If this value is false the value(s)
 * for each element is returned.
 *
 * Note: This method *always* returns an array.  If no valid value can be determined the
 *	   array will be empty, otherwise it will contain one or more values.
 */
$.fn.fieldValue = function(successful) {
	for (var val=[], i=0, max=this.length; i < max; i++) {
		var el = this[i];
		var v = $.fieldValue(el, successful);
		if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
			continue;
		v.constructor == Array ? $.merge(val, v) : val.push(v);
	}
	return val;
};

/**
 * Returns the value of the field element.
 */
$.fieldValue = function(el, successful) {
	var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
	if (typeof successful == 'undefined') successful = true;

	if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
		(t == 'checkbox' || t == 'radio') && !el.checked ||
		(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
		tag == 'select' && el.selectedIndex == -1))
			return null;

	if (tag == 'select') {
		var index = el.selectedIndex;
		if (index < 0) return null;
		var a = [], ops = el.options;
		var one = (t == 'select-one');
		var max = (one ? index+1 : ops.length);
		for(var i=(one ? index : 0); i < max; i++) {
			var op = ops[i];
			if (op.selected) {
				var v = op.value;
				if (!v) // extra pain for IE...
					v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
				if (one) return v;
				a.push(v);
			}
		}
		return a;
	}
	return el.value;
};

/**
 * Clears the form data.  Takes the following actions on the form's input fields:
 *  - input text fields will have their 'value' property set to the empty string
 *  - select elements will have their 'selectedIndex' property set to -1
 *  - checkbox and radio inputs will have their 'checked' property set to false
 *  - inputs of type submit, button, reset, and hidden will *not* be effected
 *  - button elements will *not* be effected
 */
$.fn.clearForm = function() {
	return this.each(function() {
		$('input,select,textarea', this).clearFields();
	});
};

/**
 * Clears the selected form elements.
 */
$.fn.clearFields = $.fn.clearInputs = function() {
	return this.each(function() {
		var t = this.type, tag = this.tagName.toLowerCase();
		if (t == 'text' || t == 'password' || tag == 'textarea')
			this.value = '';
		else if (t == 'checkbox' || t == 'radio')
			this.checked = false;
		else if (tag == 'select')
			this.selectedIndex = -1;
	});
};

/**
 * Resets the form data.  Causes all form elements to be reset to their original value.
 */
$.fn.resetForm = function() {
	return this.each(function() {
		// guard against an input with the name of 'reset'
		// note that IE reports the reset function as an 'object'
		if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
			this.reset();
	});
};

/**
 * Enables or disables any matching elements.
 */
$.fn.enable = function(b) {
	if (b == undefined) b = true;
	return this.each(function() {
		this.disabled = !b;
	});
};

/**
 * Checks/unchecks any matching checkboxes or radio buttons and
 * selects/deselects and matching option elements.
 */
$.fn.selected = function(select) {
	if (select == undefined) select = true;
	return this.each(function() {
		var t = this.type;
		if (t == 'checkbox' || t == 'radio')
			this.checked = select;
		else if (this.tagName.toLowerCase() == 'option') {
			var $sel = $(this).parent('select');
			if (select && $sel[0] && $sel[0].type == 'select-one') {
				// deselect all other options
				$sel.find('option').selected(false);
			}
			this.selected = select;
		}
	});
};

// helper fn for console logging
// set $.fn.ajaxSubmit.debug to true to enable debug logging
function log() {
	if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
		window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
};

})(jQuery);

jQuery.fn.sizeToFit = function () {
    var $ = jQuery;
    this.each(function () {
        var content = this;
        var container = $(this).parent();
        var outerHeight = container.height();
        container.children().each(function () {
            if (this != content) {
                outerHeight -= $(this).outerHeight();
            }
        });
        var paddingAndBorderHeight = $(this).outerHeight() - $(this).height();
        $(this).css("height", Math.max(0, outerHeight - paddingAndBorderHeight) + "px");
    });
    return this;
};

AJS.Editor.ImageDialog = AJS.Editor.ImageDialog || {
    /**
     * Listeners called just before the image dialog is shown
     */
    beforeShowListeners: [],

    /**
     * Listeners called after thumbnails of images have been drawn
     */
    afterThumbnailsDisplayedListeners: []
};

AJS.toInit(function ($) {
    AJS.wikiAttrToString = function (attr) {
        var res = [];
        for (var prop in attr) if (attr.hasOwnProperty(prop)) {
            res.push(typeof attr[prop] == "boolean" ? attr[prop] ? prop : "" : prop + "=" + attr[prop]);
        }
        return res.length ? "|" + res.join(",") : "";
    };

    AJS.Editor.insertImageDialog = function (insertCallback, cancelCallback) {
        var dim = 100,
            ERROR_MAX_LENGTH = 97;
        var selectedName = "",
            popup = new AJS.Dialog(800, 590, "insert-image-dialog");

        // For pages and blogs this is their own pageId. For comments, pageId is the page they are on.
        // For drafts it is contentId.
        var attachmentSourceContentId = AJS.params.attachmentSourceContentId;

        var buildParams = function(context) {
            var res = {},
                align = $(".img-align", context).val(),
                thumb = !!$(".img-thumbnail", context).attr("checked"),
                border = !!$(".img-border", context).attr("checked");
            align != "none" && (res.align = align);
            thumb && (res.thumbnail = true);
            border && (res.border = "1");
            return res;
        };

        function killDialog() {
            popup.hide().remove();
            $(document).unbind(".insert-image");
            cancelCallback && cancelCallback();
        }

        $(document).bind("keydown.insert-image", function (e) {
            if (e.which == 27 && !$("#fancy_overlay").is(":visible")) {
                killDialog();
                return AJS.stopEvent(e);
            }
        });

        popup.addHeader(AJS.getTemplate("dialogTitle").toString());
        popup.addPanel(AJS.getTemplate("attachedTabTitle").toString(), AJS.renderTemplate("attachedImages", AJS.getTemplate("imagePropertiesForm")), "attachments-panel");
        popup.addPanel(AJS.getTemplate("webImageTitle").toString(), AJS.renderTemplate("webImage", AJS.getTemplate("imagePropertiesForm")), "web-image-panel");
        popup.addButton(AJS.getTemplate("insertButton").toString(), function (dialog) {
            var imageParams = buildParams(dialog.getCurrentPanel().body);
            dialog.remove();
            $(document).unbind(".insert-image");
            insertCallback && insertCallback(selectedName, imageParams, attachmentSourceContentId);
        });
        popup.addButton(AJS.getTemplate("cancelButton").toString(), killDialog);
        popup.get("panel:0").setPadding(0);
        popup.get("panel:1").setPadding(0);
        popup.get("panel:0").select();
        // Unfortunately we need to access the insert button by id since the name can vary depending on language.
        var insertButton = popup.get("button:0")[0].item;
        insertButton.attr("disabled", "disabled");

        AJS.log(AJS.Editor.ImageDialog.beforeShowListeners.length + " beforeShow listeners registered.");
        $.each(AJS.Editor.ImageDialog.beforeShowListeners, function () {
            this();
        });
        popup.show();

        $("select.img-align").focus();
        $("input.image-url", popup.popup.element).bind("keyup click", function (e) {
            var val = $(this).val();
            selectedName = val;
            insertButton.attr("disabled", (val != "" && val != "http://" ? "" : "disabled"));
            if (e.which == 13) {
                $("input.image-preview", popup.popup.element).click();
                popup.get("button:0")[0].item.focus(); // move focus to Insert button
            }
        });
        $("input.image-preview", popup.popup.element).click(function () {
            var container = $(this).closest("div");
            var src = container.find("input.image-url").val();
            var preview = container.find(".image-preview-area");
            var throbber = container.find(".image-preview-throbber");
            throbber.removeClass("hidden");
            var killSpinner = Raphael.spinner(throbber[0], 60, "#666");
            var error = container.find(".image-preview-error");
            preview.addClass("faraway");
            error.addClass("hidden");
            preview.html("");
            $("<img>").load(function () {
                killSpinner();
                throbber.addClass("hidden");
                preview.removeClass("faraway");
            }).error(function () {
                killSpinner();
                throbber.addClass("hidden");
                error.removeClass("hidden");
            }).appendTo(preview).attr("src", src);
        });

        var uploadForm = $("#upload-attachment form");
        var uploadingMessage = $("#upload-attachment .image-uploading");
        var uploadingError = $("#upload-attachment .warning");

        /**
         * Clear any existing errors.
         */
        AJS.Editor.ImageDialog.clearErrors = function () {
            uploadingError.addClass("hidden");
            uploadingError.empty();
            $("#attached-images").sizeToFit();
        };

        /**
         * Displays errors. Subsequent calls will not overwrite existing messages but append to them.
         * To clear existing messages, use AJS.Editor.ImageDialog.clearErrors().
         * @param messages an array of error messages
         */
        AJS.Editor.ImageDialog.displayErrors = function (messages) {
            if (!messages || !messages.length) {
                return;
            }
            uploadingError.removeClass("hidden");
            var $errors = $("ul", uploadingError);
            if (!$errors.length) {
                $errors = $("<ul></ul>");
                $errors.appendTo(uploadingError);
            }
            $.each(messages, function(index, value) {
                if (!value) {
                    return;
                }
                $("<li>" + value.substring(0, Math.min(ERROR_MAX_LENGTH, value.length)) + (value.length > ERROR_MAX_LENGTH ? "&hellip;" : "") + "</li>").attr("title", value).appendTo($errors);
            });
            $("#attached-images").sizeToFit();
        };

        /**
         * Returns a CSS selector to locate the the thumbnails/images container of this dialog.
         * (for plugins).
         */
        AJS.Editor.ImageDialog.imagesContainerSelector = "#attached-images .image-list";

        uploadForm.ajaxForm({
            dataType: "json",
            data: {
                contentId: attachmentSourceContentId,
                responseFormat: "html" // ensure response comes back as HTML for IE compatibility
            },
            resetForm: true,
            beforeSubmit: function () {
                AJS.Editor.ImageDialog.setUploadInProgress(true);
                AJS.Editor.ImageDialog.clearErrors();
            },
            error: function (xhr) {
                AJS.Editor.ImageDialog.setUploadInProgress(false);
                AJS.Editor.ImageDialog.displayErrors([AJS.params.imageUploadError]);
                AJS.log("Response from server was: " + xhr.responseText);
            },
            success: function (response) {
                AJS.Editor.ImageDialog.setUploadInProgress(false);
                var errors = [].concat(response.validationErrors || []).concat(response.actionErrors || []).concat(response.errorMessage || []);
                if (errors.length > 0) {
                    AJS.Editor.ImageDialog.displayErrors(errors);
                    return;
                }
                AJS.Editor.ImageDialog.refreshWithLatestImages($.map(response.attachmentsAdded || [], function (element) {
                    return element.name;
                }));
            }
        });
        uploadForm.find("input:file").change(function () { uploadForm.submit(); });

        function renderImage(img) {
            if (Math.max(img.thumbnailWidth, img.thumbnailHeight) > dim) {
                if (img.thumbnailHeight > img.thumbnailWidth) {
                    img.thumbnailWidth = img.thumbnailWidth * dim / img.thumbnailHeight;
                    img.thumbnailHeight = dim;
                } else {
                    img.thumbnailHeight = img.thumbnailHeight * dim / img.thumbnailWidth;
                    img.thumbnailWidth = dim;
                }
            }
            var nonceUrl = img.thumbnailUrl + (img.thumbnailUrl.indexOf("?") + 1 ? "&" : "?") + "nonce=" + (+new Date);
            var result = $(AJS.renderTemplate("imageDialogImage", nonceUrl, img.thumbnailWidth, img.thumbnailHeight,
                    (100 - img.thumbnailHeight) / 2, img.downloadUrl, img.name));
            result.find(".image-container").andSelf().hover(function () {
                $(this).addClass("hover");
            }, function () {
                $(this).removeClass("hover");
            });
            result.find("img").load(function () {
                result.find(".image-container").removeClass("loading");
            });
            result.click(function (e) {
                $("#attached-images .selected").removeClass("selected");
                result.addClass("selected").focus();
                selectedName = this.name = this.name || $(".caption", this).text();
                insertButton.attr("disabled", "");
                return AJS.stopEvent(e); // prevent propagation to container, which when clicked deselects
            });
            result.dblclick(function () {
                $(this).click();
                insertButton.click();
            });
            $(".zoom", result).fancybox({
                padding: 0,
                zoomSpeedIn: 500,
                zoomSpeedOut: 500,
                overlayShow: true,
                overlayOpacity: 0.5
            });
            return result;
        }

        var imageContainer = $("#attached-images");
        $(document).bind("keydown.insert-image", function (e) {
            if (!imageContainer.is(":visible")) return;
            if ($("#fancy_overlay").is(":visible")) {
                if (e.which == 32) { // space bar
                    $("#fancy_close").click();
                    e.preventDefault();
                    e.stopPropagation();
                    return false;
                }
            } else {
                function moveSelection(delta) {
                    var results = $(".attached-image", imageContainer);
                    var selected = $(".attached-image.selected", imageContainer);
                    var index = results.index(selected) + delta;
                    if (index < 0) index = results.length - 1;
                    if (index >= results.length) index = 0;

                    var next = results.eq(index);
                    next.click().focus();
                    imageContainer.simpleScrollTo(next);
                }

                if (e.which == 37) { // left
                    moveSelection(-1);
                    return AJS.stopEvent(e);
                } else if (e.which == 38) { // up
                    moveSelection(-4);
                    return AJS.stopEvent(e);
                } else if (e.which == 39) { // right
                    moveSelection(1);
                    return AJS.stopEvent(e);
                } else if (e.which == 40) { // down
                    moveSelection(4);
                    return AJS.stopEvent(e);
                } else if (e.which == 32 && $(".attached-image.selected").length > 0) { // space bar
                    $(".attached-image.selected .zoom").click();
                    return AJS.stopEvent(e);
                } else if (e.which == 13 && !insertButton.is(":disabled")) { // enter
                    insertButton.click();
                    return AJS.stopEvent(e);
                }
            }
        });

        /**
         * Activates/deactivates the in progress animation and message
         * @param inprogress whether or not the dialog should indicate something is in progress or not
         * @param message [optional] a custom message to use in place of the default
         */
        AJS.Editor.ImageDialog.setUploadInProgress = function (inprogress, message) {
            if (inprogress) {
                uploadForm.addClass("hidden");
                uploadingMessage.removeClass("hidden");
                message ? uploadingMessage.html(message) : uploadingMessage.html(AJS.renderTemplate("imageUploading"));
            } else {
                uploadForm.removeClass("hidden");
                uploadingMessage.addClass("hidden");
            }
        };

        /**
         * Fetch and render all latest images
         * @param justAttached [optional] an array of filenames that have just been attached (we want to promote these in some way)
         */
        AJS.Editor.ImageDialog.refreshWithLatestImages = function (justAttached) {
            justAttached = $.map(justAttached || [], function (filename) {
                return filename && filename.toLowerCase();
            }); // ensure we dealing with lowercase filenames
            $.ajax({
                type: "GET",
                url: AJS.params.contextPath + "/pages/attachedimages.action",
                dataType: "json",
                data: {
                    contentId: attachmentSourceContentId
                },
                error: function () {
                    $("#attached-images .loading-message").remove();
                    $("#attached-images").append(AJS.renderTemplate("imageDialogErrorRetrievingAttachments"));
                },
                success: function (data) {
                    // clean up container first before inserting images
                    $("#attached-images .loading-message").remove();
                    $("#attached-images .image-list").empty();
                    $("#attached-images .no-attachments").remove();

                    $(data.images || []).each(function () {
                        if (this.name && $.inArray(this.name.toLowerCase(), justAttached) != -1) {
                            $("#attached-images .image-list").prepend(renderImage(this));
                        } else {
                            $("#attached-images .image-list").append(renderImage(this));
                        }
                    });
                    if ($("#attached-images .image-list li").length == 0) {
                        $("#attached-images").append(AJS.renderTemplate("imageDialogNoAttachments"));
                    }
                    $("#attached-images").sizeToFit().click(function () {
                        // deselect when clicking outside the images
                        selectedName = null;
                        insertButton.attr("disabled", "disabled");
                        $(this).find(".selected").removeClass("selected");
                    });
                    !!justAttached.length && $("#attached-images .image-list li:first").click(); // ensure the first image is selected

                    AJS.log(AJS.Editor.ImageDialog.afterThumbnailsDisplayedListeners.length + " afterThumbnailsDisplayed listeners registered.");
                    $.each(AJS.Editor.ImageDialog.afterThumbnailsDisplayedListeners, function () {
                        this();
                    });

                    // handle non-thumbnailable files
                    var notThumbnailableErrors = [];
                    var imageFilenames = $.map(data.images || [], function (image) {
                        return image.name && image.name.toLowerCase();
                    });
                    $.each(justAttached, function (index, value) {
                        $.inArray(value, imageFilenames) == -1 && notThumbnailableErrors.push(AJS.renderTemplate("imageNotThumbnailable", value));
                    });
                    notThumbnailableErrors && AJS.Editor.ImageDialog.displayErrors(notThumbnailableErrors);
                }
            });
        };
        AJS.Editor.ImageDialog.refreshWithLatestImages();
    };

    $("#editor-insert-image").click(function (e) {
        AJS.Editor.storeTextareaBits();
        var textarea = document.getElementById("markupTextarea");
        AJS.Editor.insertImageDialog(function (selectedName, params) {
            AJS.Editor.Markup.insertOrUpdateText(
                AJS.format("\n!{0}{1}!\n", selectedName, AJS.wikiAttrToString(params)),
                textarea);
        });
        return AJS.stopEvent(e);
    });
});

AJS.Editor.Markup = {
    
    // This function stores the selected and unselected text for the textarea in hidden fields on the form
    // This should be called before insertOrUpdateText.
    storeTextareaBits : function (currentForm, textAreaObject, doNotFocus) {
        if (textAreaObject.selectionStart != null) {
            // for netscape, mozilla, gecko
            textAreaObject.sel = textAreaObject.value.substr(textAreaObject.selectionStart, textAreaObject.selectionEnd - textAreaObject.selectionStart);
            textAreaObject.sel1 = textAreaObject.value.substr(0, textAreaObject.selectionStart);
            textAreaObject.sel2 = textAreaObject.value.substr(textAreaObject.selectionEnd);
            currentForm.selectedText.value = textAreaObject.sel;
        }
        else {
            if (document.selection && document.selection.createRange) {
                // for ie
                try {
                    // Focus the textarea so IE will get the correct selection range.
                    !doNotFocus && currentForm.elements[AJS.params.parametersName].focus();
                }
                catch (e) {
                    // ignore
                }
                var ieRange = document.selection.createRange();
                textAreaObject.caretPos = ieRange.duplicate();
                currentForm.selectedText.value = ieRange.text;
            }
        }

        return currentForm.selectedText.value;
    },

    // Inserts text into the text area specified (currently compatible with Netscape, mozilla, ie)
    // Note: for IE compatibility, storeCaret(this) must be called in the onclick, onselect and onkeyup events
    // of the text area object specified
    insertOrUpdateText: function (text, textAreaObject) {
        if (window.getSelection && textAreaObject.selectionStart && textAreaObject.selectionStart != null) {
            textAreaObject.value = textAreaObject.sel1 + text + textAreaObject.sel2;
            textAreaObject.focus();
            textAreaObject.selectionStart = textAreaObject.selectionEnd = textAreaObject.sel1.length + text.length;
        } else if (textAreaObject.createTextRange && textAreaObject.caretPos) {
            // for IE
            // IE supports createTextRange(), test for non-null caretPos (if its been set)
            var caretPos = textAreaObject.caretPos;
            caretPos.text = caretPos.text.charAt(caretPos.text.length - 1) == ' ' ? text + ' ' : text;
        } else {
            // for ie users that don't set the caret OR
            // for other browsers
            // just append link at the end of the current text inside the text area
            textAreaObject.value += text;
        }
    }
};
AJS.MacroBrowser = (function($) { return {
    hasInit: false,
    metadataList : [],
    aliasMap: {}, // maps each alias to the corresponding macro name
    fields: {}, // stores fields for a given macro form.

    // converts wiki markup into a macro object
    parseMacro : function(macroMarkup) {
        var macroParts = macroMarkup.match(/(\{(.+?)(?::(.*?(?=[^\\]\}).)?)?\})(?:((?:\n|.)*?)\{\2\})?/);
        var macro = {
            markup:     macroParts[0], // entire macro markup
            startTag:   macroParts[1], // full start tag
            name :      macroParts[2], // macro name
            paramStr:   macroParts[3],
            bodyMarkup: macroParts[4], // macro bodyMarkup text
            params: {}
        };
        if (macro.markup) {
            var beforeAndAfter = macroMarkup.split(macro.markup);
            macro.beforeTag = beforeAndAfter[0];
            macro.afterTag = beforeAndAfter[1];
        }
        if (macro.paramStr) {
            var paramStrs = macro.paramStr.split("|");
            $(paramStrs).each(function(i, param) {
                var index = param.indexOf("=");
                if (index < 0 && !macro.params[""]) { // unnamed parameter
                    macro.params[""] = param;
                }
                else {
                    macro.params[param.substring(0, index)] = param.substring(index+1);
                }
            });
        }
        return macro;
    },

    // Gets the current selected macro if any.
    // It assumes that the cursor is in the start tag of a macro, if selected.
    getSelectedMacro : function(beforeSelection, wikiText) {
        // Finds all the text before the last '{' but doesn't have a '}'. This also handles escaped '{'s.
        var m = /^(?:.|\n)*[^\\](?={(?:\\}|[^}])+$)/m.exec(" " + beforeSelection + " "); // spaces required for when { is the first or last character
        if (!m) return null;

        var startIndex = m[0].substring(1).length;

        var macro = AJS.MacroBrowser.parseMacro(wikiText.substring(startIndex));
        macro.startIndex = startIndex;
        macro.params = {};
        if (macro.paramStr) {
            // grabs all pairs divided by "=" and separated by "|"
            macro.paramStr.replace(/(?=(?:^|\|)(.*?)(?:=(.*?))?(?:\||$))/g, function (a, name, value) {
                if ((!value || value == "") && !macro.params[""]) { // unnamed parameter
                    macro.params[""] = name;
                } else {
                    macro.params[name] = value;
                }
            });
        }
        return macro;
    },

    // Creates a div for a single macro parameter.
    makeParameterDiv : function(macroInformation, param, macroConfig) {
        var t = this, field;
        var type = param.type.name;

        // Plugin point - other JS files can define more specific field-builders based on macro name, param name and
        // type.
        if (macroConfig) {
            var builder = macroConfig.fields && macroConfig.fields[type];
            if (builder && typeof builder != "function") {
                // Types can be overridden for specific parameters - so the "type" object contains a "name" function.
                builder = builder[param.name];
            }
            if (typeof builder == "function") {
                field = builder.call(macroConfig, param);
            }
        }
        // If no override specific to the macro, look for general overrides specific to the parameter type.
        if (!field) {
            if (!(type in t.ParameterFields && typeof t.ParameterFields[type] == "function")) {
                type = "string";
            }
            field = t.ParameterFields[type](param);
        }

        t.fields[param.name] = field;
        var paramDiv = field.paramDiv;
        var input = field.input;

        var paramId = "macro-param-" + param.name;
        paramDiv.attr("id", "macro-param-div-" + param.name);
        input.addClass("macro-param-input").attr("id", paramId);
        if(param.hidden) {
            paramDiv.hide();
        }

        // Use param label and desc or correct fallback.
        var pluginKey = macroInformation.pluginKey;
        if (param.displayName == t.makeDefaultKey(pluginKey, macroInformation.macroName, "param", param.name, "label")) {
            param.displayName = param.name;
        }
        if (param.description == t.makeDefaultKey(pluginKey, macroInformation.macroName, "param", param.name, "desc")) {
            param.description = "";
        }

        var labelText = param.displayName;
        if (param.required) {
            labelText += " *";
            paramDiv.addClass("required");  // set class against div, not input, to allow styling of label if nec
        }
        $("label", paramDiv).attr("for", paramId).text(labelText);

        if (param.description) {
            paramDiv.append(AJS.clone("#macro-param-desc-template").html(param.description));
        }
        return paramDiv;
    },
    // Creates a div for a macro body.
    makeBodyDiv : function(body, selectedMacro) {
        var t = AJS.MacroBrowser;
        var bodyDiv = AJS.clone("#macro-body-template");

        $("textarea", bodyDiv).val((selectedMacro && selectedMacro.bodyMarkup) || t.settings.selectedMarkup || "");

        if (body.label) {
            $("label", bodyDiv).text(body.label);
        }
        if (body.description) {
            bodyDiv.append(AJS.clone("#macro-param-desc-template").html(body.description));
        }
        if(body.hidden) {
            bodyDiv.hide();
        }
        return bodyDiv;
    },
    // Checks and returns true if all the required macro parameters have values.
    // It disables the insert/preview buttons if false.
    processRequiredParameters: function() {
        var blankRequiredInputs = $("#macro-insert-container .macro-param-div.required .macro-param-input")
        .filter(function() {
            var val = $(this).val();
            return (val == null || val == "");
        });
        var hasAllRequiredData = (blankRequiredInputs.length == 0);
        var disabled = hasAllRequiredData ? "" : "disabled";
        var classFn = disabled ? "addClass" : "removeClass";

        AJS.$("#macro-browser-dialog button.ok").attr("disabled", disabled);
        AJS.$("#macro-browser-dialog .macro-preview-header .refresh-link").attr("disabled", disabled)[classFn]("disabled");

        return hasAllRequiredData;
    },

    /**
     * Called when a parameter field value changes.
     */
    paramChanged: function () {
        // TODO - Could be used to preview?
        AJS.MacroBrowser.processRequiredParameters();
    },

    // Loads the given macro json in the browser's insert macro page.
    loadMacroInBrowser : function(macroInformation, mode) {
        if (!macroInformation || !macroInformation.formDetails) {
            alert(AJS.params.unknownMacroMessage);
            return;
        }

        $("#save-warning-span").addClass("hidden"); // gadgets may have put it in this state
        var t = AJS.MacroBrowser, detail = macroInformation.formDetails;
        var macroName = detail.macroName;
        var macroConfig = t.Macros[macroName];
        var placeHolderTitle = mode=="edit" ? t.editTitle : t.insertTitle;
        AJS.MacroBrowser.dialog.gotoPage(1).addHeader(placeHolderTitle.replace(/\{0\}/, macroInformation.title));

        // Update the button label
        var okButton = AJS.$("#macro-browser-dialog .button-panel .ok");
        if (mode == "edit") {
            okButton.text(AJS.params.saveButtonLabel);
        } else {
            okButton.text(AJS.params.insertButtonLabel);
        }

        // Macro description and documentation link
        AJS.$("#macro-insert-container .macro-name").val(macroName);
        var macroDescription = macroInformation.extendedDescription ? macroInformation.extendedDescription : macroInformation.description;
        var macroDesc = AJS.clone("#macro-summary-template .macro-desc").prepend(macroDescription),
            macroDiv = $("#macro-insert-container .macro-input-fields").html(macroDesc);
        if (detail.documentationUrl) {
            var doco = AJS.clone("#macro-doco-link-template");
            AJS.$("a", doco).attr("href", detail.documentationUrl);
            macroDesc.append(doco);
        } else if (!macroDesc.text()) { // remove the div and its padding style
            macroDesc.remove();
        }

        if (detail.body) {
            var pluginKey = macroInformation.pluginKey;
            if (detail.body.label == t.makeDefaultKey(pluginKey, macroName, "body", "label")) {
                detail.body.label = "";
            }
            if (detail.body.description == t.makeDefaultKey(pluginKey, macroName, "body", "desc")) {
                detail.body.description = "";
            }
            var body = t.makeBodyDiv(detail.body, t.selectedMacro);
        }

        // for macros without parameter info, we display the notation help (if any)
        if (detail.freeform) {
            var paramStr = (t.selectedMacro && t.selectedMacro.paramStr) || "",
                freeform = AJS.clone("#macro-freeform-template");
            $(".macro-name-display", freeform).text(macroName + ": ");
            $(".macro-text", freeform).val(paramStr);
            if (body) {
                body = body.append("{" + macroName + "}");
                AJS.$(".macro-freeform-input", freeform).after(body); // body goes before notation help
            }
            if (detail.notationHelp) { // notation help is in table cell markup
                var notationHelpCells = AJS.$(detail.notationHelp).children();
                if (notationHelpCells[0]) {
                    // populate input field with example macro markup, if any
                    if (!t.selectedMacro) {
                        // Take macro example HTML and replace line-breaks with newlines, dropping all other tags.
                        var example = AJS.$(notationHelpCells[0]).html().replace(/<br>/gi, "\n").replace(/<[^>]+>/gi, "");
                        var index = example.indexOf("{" + macroName);
                        if (index > -1) {
                            var macro = t.parseMacro(example.substring(index));
                            if (macro.paramStr) {
                                $(".macro-freeform-input input", freeform).val(macro.paramStr);
                            }
                            if (macro.bodyMarkup) {
                                // Remove any leading/trailing newlines and other whitespace from the body. 
                                $(".macro-body-div textarea", freeform).val(macro.bodyMarkup.replace(/^\n|\n$/g, "").replace(/^\s+|\s+$/gm, ""));
                            }
                        }
                    }
                    $(".macro-example", freeform).append(notationHelpCells[0].innerHTML).removeClass("hidden");
                }
                if (notationHelpCells[1]) {
                    $(".macro-help", freeform).append(notationHelpCells[1].innerHTML).removeClass("hidden");
                }
            }
            macroDiv.append(freeform);
        } else { // macros with parameter info
            if (body) {
                macroDiv.append(body);
            }
            // Parameters may have dependencies so all fields need to be created before values are set.
            $(detail.parameters).each(function() {
                    macroDiv.append(t.makeParameterDiv(macroInformation, this, macroConfig));
            });

            var selectedParams = t.selectedMacro ? $.extend({}, t.selectedMacro.params) : {}; // make a copy

            // Fully-implemented macros may have JS overrides defined in a Macro object.
            if (macroConfig && typeof macroConfig.beforeParamsSet == "function") {
                selectedParams = macroConfig.beforeParamsSet(selectedParams, !t.selectedMacro);
            }

            var bodyParamMap = {};
            if (macroConfig && typeof macroConfig.populateBodyParams == "function") {
                bodyParamMap = macroConfig.populateBodyParams(body);
            }

            $(detail.parameters).each(function() {
                var param = this,
                    value = selectedParams[param.name];

                if (value != null) {
                    delete selectedParams[param.name];
                } else {
                // try looking for aliased parameters
                    $(param.aliases).each(function() {
                        if (selectedParams[this]) {
                            value = selectedParams[this];
                            delete selectedParams[this];
                        }
                    });
                }
                if (value == null) {
                    if(bodyParamMap[param.name]) {
                        value = bodyParamMap[param.name];
                    }
                    else {
                        value = param.defaultValue;
                    }
                }
                if (value != null) {
                    t.fields[param.name].setValue(value);
                }
            });

            // Any remaining "selectedParameters" are unknown for the current Macro details.
            t.unknownParams = selectedParams;
        }
        // open all links in a new window
        $("a", macroDiv).click(function() {
            window.open(this.href, '_blank').focus();
            return false;
        });

        if (!$("#macro-browser-dialog:visible").length) {
            t.showBrowserDialog();
        }

        var firstInput = $("input:visible:first", macroDiv);
        if (firstInput.length) {
            firstInput.focus();
            if (!t.selectedMacro && firstInput.val() != "") {
                // prefilled data in new macro form - select first field for easy overwrite.
                firstInput[0].select();
            }
        }
        t.previewMacro(macroInformation); // load preview with default params
    },

    makeParamStringFromMap : function(paramMap) {
        var strs = [];

        if (paramMap[""]) {
            strs.push(paramMap[""]); // no named param must be first
            delete paramMap[""]; // todo find out why this delete is here
        }

        for (var param in paramMap) {
            strs.push(param + "=" + paramMap[param]);
        }

        return strs.join("|");
    },

    // Constructs the macro markup from the insert macro page
    getMacroMarkupFromForm : function(macro) {
        var macroName = $("#macro-insert-container .macro-name").val(),
            startTag = "{" + macroName,
            freeform = $("#macro-insert-container .macro-text"),
            params,
            t = AJS.MacroBrowser;

        if (freeform.length) {
            params = freeform.val();
        } else {
            var paramMap = {}, bodyParamMap ={} , paramDetails = macro.formDetails.parameters;

            // Get parameter markup
            $(paramDetails).each(function() {
                var paramInput = AJS.$("#macro-param-" + this.name);
                var paramVal = paramInput.val();
                if (paramInput.attr("type") == "checkbox") {
                    paramVal = "" + paramInput.attr("checked");
                }
                if(this.forBody) {
                    if(paramVal){
                        bodyParamMap[this.name] = paramVal;
                    }
                }
                else if (paramVal && (this.hidden || (!this.defaultValue || this.defaultValue != paramVal))) { // only add the parameter value if its not the default
                    paramMap[this.name] = paramVal;
                }
            });
            if (t.unknownParams) {
                $.each(t.unknownParams, function(key, value) {
                    paramMap[key] = value; // TODO - can do this better with extend? dT
                });
            }
            var macroConfig = t.Macros[macroName];
            if (macroConfig && typeof macroConfig.beforeParamsRetrieved == "function") {
                paramMap = macroConfig.beforeParamsRetrieved(paramMap);
            }

            params = t.makeParamStringFromMap(paramMap);
        }
        if (params) {
            startTag += ":" + params;
        }
        startTag += "}";

        var res = {
            name : macroName,
            startTag : startTag,
            markup : startTag,
            bodyMarkup : "",
            hasBody : AJS.$("#macro-insert-container .macro-body-div").length > 0
        };

        if (res.hasBody) {
            var theBodyMarkup = AJS.$("#macro-insert-container .macro-body-div textarea").val();

            macroConfig = t.Macros[macroName];
            if(macroConfig && macroConfig.applySpecialBodyHandling){
                theBodyMarkup = macroConfig.applySpecialBodyHandling(macro, theBodyMarkup, bodyParamMap);
            }

            res.bodyMarkup = theBodyMarkup;
            res.markup += res.bodyMarkup;
            res.markup += "{" + macroName + "}";
        }
        return res;
    },

    // Makes an ajax request to render the macro markup and updates the preview
    previewMacro : function(macro) {
        var t = AJS.MacroBrowser;
        $("#macro-insert-container .macro-preview").html("");
        if (!t.processRequiredParameters()) {
            AJS.log("previewMacro: missing required params");
            return;
        }
        AJS.log("previewMacro: required params ok");
        t.showPreviewWaitImage(true);
        
        var wikiMarkup = t.getMacroMarkupFromForm(macro).markup,
        macroConfig = AJS.MacroBrowser.Macros[macro.macroName];
        if (macroConfig && macroConfig.prepareMacroForPreview) {
            wikiMarkup = macroConfig.prepareMacroForPreview(wikiMarkup);
        }

        var queryParams = { "contentId": AJS.Editor.getContentId(),
                            "contentType": AJS.params.contentType,
                            "spaceKey": AJS.params.spaceKey,
                            "wikiMarkup": wikiMarkup };
        // Use post cause wiki markup can be quite long
        $.post(AJS.params.contextPath + "/pages/rendercontent.action", queryParams, function(html) {
            AJS.MacroBrowser.showPreviewWaitImage(false);
            // Set the iframe source to an empty JS statement to avoid secure/nonsecure warnings on https, without 
            // needing a back-end call.
            var src = AJS.params.staticResourceUrlPrefix + "/blank.html";
            var previewDiv = AJS.$("#macro-insert-container .macro-preview");

            previewDiv.html('<iframe src="' + src + '" frameborder="0" name="macro-browser-preview-frame" id="macro-preview-iframe"></iframe>');

            AJS.log("previewMacro: Created iframe");
            var iframe = AJS.$("#macro-insert-container .macro-preview iframe")[0];
            var doc = iframe.contentDocument || iframe.contentWindow.document;
            doc.write(html);
            doc.close(); // for firefox
            var errorSpan = $("div.error span.error", doc);
            if (errorSpan.length) {
                AJS.log("Error rendering markup : " + wikiMarkup);
            }

            AJS.log("previewMacro: rendered");
        });
    },
    showPreviewWaitImage : function(flag) {
        if (flag) {
            $("#macro-browser-preview-link").attr("disabled", true).addClass("disabled");
            var throbber = AJS("div").addClass("macro-loading");
            $("#macro-browser-preview").append(throbber);
            AJS.MacroBrowser.previewSpinner = Raphael.spinner(throbber[0], 60, "#666");
            AJS.MacroBrowser.previewSpinner.throbber = throbber;
        } else if (AJS.MacroBrowser.previewSpinner) {
            $("#macro-browser-preview").removeClass("macro-loading");
            AJS.MacroBrowser.previewSpinner();
            AJS.MacroBrowser.previewSpinner.throbber.remove();
            delete AJS.MacroBrowser.previewSpinner;
            $("#macro-browser-preview-link").attr("disabled", false).removeClass("disabled");
        }
    },
    // This gets called on the preview window's onload to re-adjust the height of the frame
    previewOnload: function(body) {
        var selectedMacroName = AJS.MacroBrowser.dialog.selectedMacro.macroName;
        var macroConfig = AJS.MacroBrowser.Macros[selectedMacroName];
        if (macroConfig && macroConfig.postPreview) {
            macroConfig.postPreview(AJS.$("#macro-preview-iframe")[0], AJS.MacroBrowser.dialog.selectedMacro);
        }
        AJS.Editor.disableFrame(body);
        // open all links in a new window
        $(body).click(function(e) {
            if (e.target.tagName.toLowerCase() === "a") {
                var a = e.target;
                var link = $(a).attr("href");
                if (link && link.indexOf("#") != 0 && link.indexOf(window.location) == -1) {
                    window.open(link, '_blank').focus();
                }
                return false;
            }
        });
    },

    //Override this method to provide alternate behaviour for mapping selected macro -> details to use
    getMacroDetailsFromSelectedMacro : function(selectedMacroName) {
        for (var i = 0, len = this.metadataList.length; i < len; i++) {
            var tempMacro = this.metadataList[i];
            if (tempMacro.macroName == selectedMacroName) {
                return tempMacro;
            }
        }
    },

    /**
     * Called when the user either clicks the Macro Browser button (in Rich-text or Wiki editors) or clicks Edit in a
     * macro placeholder in the RTE.
     *
     * Note that the macro browser is not initialsed/loaded until opened for the first time.
     *
     * @param settings macro browser settings include:
     *      selectedMacro : macro "object" as returned by getSelectedMacro
     *      selectedMarkup : string of selected markup from Wiki Markup editor when no macro selected
     *      selectedHTML : string of selected HTML from RTE when no macro selected
     *      onComplete : function to call when Macro Browser's "Insert" button is pressed
     *      onCancel : function to call when Macro Browser is closed when incomplete
     */
    open : function(settings) {
        if (!settings) {
            settings = {};
            AJS.log("No settings to open the macro browser.");
        }
        var t = AJS.MacroBrowser;
        if (!t.hasInit) { // init the macro browser for the first time
            AJS.log("init macro browser");
            t.showBrowserSpinner(true);

            if (t.originalMacroMetadata) {
                t.initBrowser(t.originalMacroMetadata);
            }
            else { // ajax request not returned yet, set a flag to init the browser later
                t.initMacroBrowserAfterRequest = settings;
                return;
            }
        }
        t.openMacroBrowser(settings);
    },
    // This method must be called after the dialog has been initialised
    openMacroBrowser: function(settings) {
        var t = AJS.MacroBrowser;
        t.settings = settings;
        t.selectedMacro = settings.selectedMacro;
        var selectedMacroName = (t.selectedMacro && t.selectedMacro.name) || settings.presetMacroName;

        if (selectedMacroName) {
            selectedMacroName = t.aliasMap[selectedMacroName.toLowerCase()] || selectedMacroName.toLowerCase();
            var macro;
            var macroConfig = t.Macros[selectedMacroName];
            if (macroConfig && t.selectedMacro) {
                if (typeof macroConfig.updateSelectedMacro == "function") {
                    macroConfig.updateSelectedMacro(t.selectedMacro);
                }
                var macroDetailsFunction = macroConfig.getMacroDetailsFromSelectedMacro;
                if(macroDetailsFunction) {
                    macro = macroDetailsFunction(t.metadataList, t.selectedMacro);
                } else {
                    macro = AJS.MacroBrowser.getMacroDetailsFromSelectedMacro(selectedMacroName);
                }
            } else {
                macro = AJS.MacroBrowser.getMacroDetailsFromSelectedMacro(selectedMacroName);
            }

            AJS.log("Open macro browser to edit macro: "+ selectedMacroName);
            $("#macro-browser-dialog button.back").hide();
            t.replicateSelectMacro(macro, "edit");
        }
        else {
            $("#macro-browser-dialog button.back").show();
            t.showBrowserDialog(); // we must show then go to panel - this order is important for IE6
            t.dialog.gotoPanel(0, 0);
            $("#macro-browser-search").val("").keyup().focus(); // clear out search box
        }
    },
    showBrowserDialog: function() {
        AJS.MacroBrowser.dialog.show();
        AJS.MacroBrowser.showBrowserSpinner(false);
    },
    // Called when dialog is closed by Inserting/Saving.
    complete : function (dialog) {
        var t = this;
        var macro = t.dialog.selectedMacro;
        var macroConfig = AJS.MacroBrowser.Macros[macro.macroName];
        if (macroConfig && macroConfig.manipulateMarkup) {
            macroConfig.manipulateMarkup(macro);
        }

        var markup = t.getMacroMarkupFromForm(macro);

        t.close();
        if (t.settings.onComplete) {
            t.settings.onComplete(markup);
        }
    },
    // Called when dialog is closed by various cancel buttons or via Esc key.
    cancel : function() {
        var t = AJS.MacroBrowser;
        t.close();
        if (typeof t.settings.onCancel == "function") {
            t.settings.onCancel();
        }
    },
    close : function() {
        var t = this;
        t.unknownParams = {};
        t.fields = {};
        t.dialog.hide();
    },
    // Replicates the user behaviour of selecting a macro and displaying the insert macro page
    replicateSelectMacro : function (macro, mode) {
        AJS.MacroBrowser.dialog.selectedMacro = macro;
        AJS.MacroBrowser.loadMacroInBrowser(macro, mode);
    },
    makeDefaultKey : function() {
        return $.makeArray(arguments).join(".");
    },
    showBrowserSpinner: function(flag) {
        var elm = AJS.Editor.inRichTextMode() ? ".defaultSkin span.mce_conf_macro_browser" : "#editor-insert-macro";
        if (flag) {
            $(elm).addClass("wait");
        } else {
            $(elm).removeClass("wait");
        }
    },
    // Loads the categories and macros into the dialog
    initBrowser : function(data) {
        if (!data.categories || !data.macros) {
            alert(AJS.params.loadBrowserErrorMessage);
            AJS.MacroBrowser.showBrowserSpinner(false);
            return false;
        }
        var startTime = new Date(), t = AJS.MacroBrowser;
        var dialog = t.dialog = AJS.ConfluenceDialog({
            width : 865,
            height: 530,
            id: "macro-browser-dialog",
            onCancel: t.cancel
        });

        dialog.addHeader(data.title);
        t.editTitle = data.editTitle;
        t.insertTitle = data.insertTitle;

        // sort the categories and macros
        data.categories.sort(function(one, two) {
            return (one.displayName.toLowerCase() > two.displayName.toLowerCase() ? 1 : -1);
        });
        // Clean up unset titles and descriptions before sorting
        for(var i=0, ii=data.macros.length; i<ii; i++) {
            var macro = data.macros[i];
            if (macro.title == t.makeDefaultKey(macro.pluginKey, macro.macroName, "label")) {
                macro.title = macro.macroName.charAt(0).toUpperCase() + macro.macroName.substring(1).replace(/-/g, ' ');
            }
            if (macro.description == t.makeDefaultKey(macro.pluginKey, macro.macroName, "desc")) {
                macro.description = "";
            }
        }
        data.macros.sort(function(one, two) { return (one.title.toLowerCase() > two.title.toLowerCase() ? 1 : -1); });

        var makeCategoryList = function(id) {
            return $("#macro-summaries-template").clone().attr("id", "category-" + id);
        };
        var makeMacroSummary = function(macro) {
            var macroDiv = AJS.clone("#macro-summary-template")
            .click(function(e) {
                if (t.settings.nestingMacros && ($.inArray(macro.macroName, t.settings.nestingMacros) > -1)) {
                    alert(AJS.params.nestingSameMacroNotAllowedMessage);
                    return AJS.stopEvent(e);
                }
                dialog.selectedMacro = macro;
                AJS.MacroBrowser.loadMacroInBrowser(macro, "insert");
            });
            if (macro.icon) {
                var iconLocation = (macro.icon.relative ? AJS.params.staticResourceUrlPrefix : "") + macro.icon.location;
                if(!macro.icon.relative && AJS.$.browser.msie && !window.location.href.indexOf("https") && iconLocation.indexOf("https")) {
                    macroDiv.prepend("<span class='macro-icon-holder icon-" + macro.macroName + "'></span>");
                }
                else {
                    macroDiv.prepend("<img src='" +  iconLocation + "' alt='icon' " +
                        "width='" + macro.icon.width + "' height='" + macro.icon.height + "' title='" + macro.title + "'/>");
                }
            } else {
                macroDiv.prepend("<span class='macro-icon-holder icon-" + macro.macroName + "'></span>");
            }

            $(".macro-title", macroDiv).text(macro.title);
            $(".macro-desc", macroDiv).prepend(macro.description);
            return macroDiv;
        };
        var categoryDivs = { all: makeCategoryList("all") };

        // Content on the right, setup macro list items
        for(var i=0, ii=data.macros.length; i<ii; i++) {
            var macro = data.macros[i];
            macro.id = "macro-" + (macro.alternateId ? macro.alternateId : macro.macroName);
            if (!macro.hidden) {
                var macroDiv = makeMacroSummary(macro).attr("id", macro.id);
                categoryDivs.all.append(macroDiv);
                for (var j=0, jj=macro.categories.length; j<jj; j++) {
                    var catKey = macro.categories[j];
                    categoryDivs[catKey] = categoryDivs[catKey] || makeCategoryList(catKey);
                    categoryDivs[catKey].append(makeMacroSummary(macro).attr("id", catKey + "-" + macro.id));
                }
            }
            var desc = (macro.description && macro.description.replace(/,/g, ' ')) || "";
            macro.keywords = [macro.macroName, macro.title, desc].join(',');
            t.metadataList.push(macro);
            for (var j=0, jl=macro.aliases.length; j<jl; j++) {
                t.aliasMap[macro.aliases[j].toLowerCase()] = macro.macroName.toLowerCase();
            }
        }

        // menu on the left, setup category panels
        dialog.addPanel(AJS.params.categoryAllLabel, categoryDivs["all"]);
        for (var i=0, ii=data.categories.length; i<ii; i++) {
            var category = data.categories[i];
            dialog.addPanel(category.displayName, categoryDivs[category.name] || makeCategoryList(category.name), category.name)
              .getPanel(i).setPadding(0); // remove the default dialog padding
        }
        dialog.addButton(AJS.params.cancelButtonLabel, function () { AJS.MacroBrowser.cancel(); }, "cancel");

        // prepare insert macro page
        var insertMacroBody = AJS.$("#macro-insert-template").clone().attr("id", "macro-insert-container");
        $(".macro-preview-container .macro-preview", insertMacroBody).attr("id", "macro-browser-preview");

        $(".macro-preview-container .macro-preview-header .refresh-link", insertMacroBody).attr("id", "macro-browser-preview-link")
        .click(function(e) {
            AJS.MacroBrowser.previewMacro(dialog.selectedMacro);
            return AJS.stopEvent(e);
        });

        dialog.addPage()
        .addPanel("X", insertMacroBody, "macro-input-panel")
        .addButton(AJS.params.backButtonLabel, function(dialog) {
            dialog.prevPage();
            $("#macro-browser-search").focus();
        }, "back left")
        .addButton(AJS.params.insertButtonLabel, function () { AJS.MacroBrowser.complete(); }, "ok")
        .addButton(AJS.params.cancelButtonLabel, function () { AJS.MacroBrowser.cancel(); }, "cancel")
        .getPanel(0).setPadding(0); // remove the default dialog padding
        $("#macro-browser-dialog .button-panel .ok").before("<span id='save-warning-span' class='hidden'/>");

        var filterSearch = function(text) {
            var ids = null;
            if (text != '') {
                if (dialog.getCurrentPanel() != dialog.getPanel(0)) {
                    dialog.gotoPanel(0);
                }
                var options = {  splitRegex: /[\s\-]+/ };
                var filteredSummaries = AJS.filterBySearch(t.metadataList, text, options);
                ids = {};
                $.each(filteredSummaries, function() {
                    ids[this.id] = this;
                });
            }
            $("#macro-browser-dialog .panel-body #category-all .macro-list-item").each(function() {
                (!ids || this.id in ids) ? $(this).show() : $(this).hide();
            });
        };

        // add search box
        var searchForm = $("<form id='macro-browser-search-form'><input type='text'/></form>");
        var searchInput = $("input", searchForm)
            .attr("id", "macro-browser-search")
            .keyup(function(e) {
                filterSearch($.trim(searchInput.val()));
            })
            .focus(function(e) {
                var searchInput = $(e.target);
                if (searchInput.hasClass("blank-search")) {
                    searchInput.removeClass("blank-search").val("");
                }
                e.target.select();
            })
            .blur(function (e) {
                var searchInput = $(e.target);
                if ($.trim(searchInput.val()) == "") {
                    searchInput.addClass("blank-search").val(AJS.params.blankSearchText);
                }
            })
            .blur();
        searchForm.submit(function(e) {
            var filteredMacros = $("#macro-browser-dialog .panel-body #category-all .macro-list-item:visible");
            if ($.trim(searchInput.val()) != "" && filteredMacros.length == 1) {
                // Only one macro found with search - select it.
                filteredMacros.click();
            }
            return AJS.stopEvent(e);
        });
        dialog.page[0].header.prepend(searchForm);
        dialog.page[0].ontabchange = function(newPanel, oldPanel) {
            if (newPanel != dialog.getPanel(0, 0)) {
                // Moving away from the "All" macro panel; reset the search value if present
                if (!searchInput.hasClass("blank-search")) {
                    searchInput.val('').blur();
                }
                filterSearch("");
            }
        };

        dialog.gotoPanel(0, 0);
        dialog.ready = true;
        t.hasInit = true;

        var time = (new Date()).getTime() - startTime.getTime();
        AJS.log("loading macro browser took " + time + "ms");
        return true;
    }
};})(AJS.$);

AJS.toInit(function($) {
    // bind the function to be run when the macro browser preview frame is loaded
    $(window).bind("render-content-loaded", function(e, body) {
        var iframe = $("#macro-preview-iframe");
        if (iframe.contents().find("body")[0] == body) {
            AJS.MacroBrowser.previewOnload(body);
        }
    });

    setTimeout(function() {
        var t = AJS.MacroBrowser;
        AJS.$.ajax({
            type: "GET",
            dataType: "json",
            url: AJS.params.contextPath + "/plugins/macrobrowser/browse-macros.action",
            success: function(data) {
                t.originalMacroMetadata = data;
                if (t.initMacroBrowserAfterRequest) {
                    t.initBrowser(data);
                    t.openMacroBrowser(t.initMacroBrowserAfterRequest);
                }
            },
            error: function(e) {
                AJS.log("Error requesting macro browser metadata:");
                AJS.log(e);
                t.originalMacroMetadata = {};
            }
        });
    }, 500); // we don't need to request for the metadata immediately
});

/**
 * Returns an object wrapper for a parameter-div jQuery object and the input in
 * that div that stores the internal parameter value (as opposed to the display
 * field, although they may be the same).
 */
AJS.MacroBrowser.Field = function (paramDiv, input, options) {
    options = options || {};

    var setValue = options.setValue || function (value) {
        input.val(value);
    };

    var getValue = options.getValue || function () {
        return input.val();
    };
    
    input.change(options.onchange || AJS.MacroBrowser.paramChanged);

    return {
        paramDiv : paramDiv,
        input : input,
        setValue : setValue,
        getValue : getValue
    };
};

/**
 * ParameterFields defines default "type" logic for fields in the Macro
 * Browser's Insert/Edit Macro form.
 * 
 * Each method in this object corresponds to a parameter type as defined in the
 * MacroParameterType enum.
 */
AJS.MacroBrowser.ParameterFields = (function ($) { 

    /*
     * The underlying AJS dropDown component takes options and this function
     * is responsible for creating the standard options the parameter fields
     * specified in this file need.
     * 
     * In this case, the 'standardOptions' are the selectionHandler handler
     * the dropDown component will use when items are selected. This is set
     * to be the onselect function passed.
     */
    var createStandardDropDownOptions = function (onselect) {
        return {
                selectionHandler: function (e, selection) {
                    onselect(selection);
                    this.hide();   
                    e.preventDefault();
                }
           };
    };
        
    /**
     * Quick search drop down post processor that will handle the case when there are no
     * matches
     */
    var handleNoMatches = function (list) {
        // remove the "search for" at the bottom of the list
        $("ol.last", list).remove();

        // check if there are items in the drop down. If none then display a
        // message telling the user this
        if ($("ol", list).length == 0) {
            var noSuggestions = AJS.clone("#macro-param-smartfield-no-suggestion-template");
            list.append(noSuggestions.find("ol"));
        }
    };     
    
    /**
     * Standard function to place the drop down.
     */
    var dropDownPlacer = function (input) {
        return function(dropDown) {
            var placer = AJS("div");
            placer.addClass("macro-param-dropdown-wrapper aui-dd-parent");
            placer.append(dropDown);
            input.after(placer);
            // dropDownEscapeHandler(input, dropDown);
        };
    };
    
    /**
     * Standard function that will ensure the 'escape' keypress closes the drop down and
     * not the Macro Browser dialog itself.
     * 
     * TODO - doesn't work - the dropdown.js has already seen the escape key and has hidden
     */
    var dropDownEscapeHandler = function (input, dropDown) {
        // escape key handling for the drop down - we want the drop down to be dismissed and not the whole dialog
        input.keyup(function (e) {
            if (e.keyCode == 27)  {
                var parent = input.parent();
                if (!parent) {
                    return;
                }
                
                // if the drop down is visible then hide it on escape
                if ($(".aui-dropdown:not(.hidden)", parent).length) {
                    dropDown.hide();
                    return AJS.stopEvent(e);
                }
                
                // otherwise don't use the escape keyup event
                return;
            }
        });        
    };
    
    
    /**
     * This is not used by any of the current field implementation. However, we have big plans for this function
     * once we support multi-valued fields.
     */
    var makeAutoListField = function (param, value, options, queryParams, makeListItem, setValue) {
        options = options || {}; 

        /*
         * Search for pages/blog-posts by au
         */
        var paramDiv = AJS.clone("#macro-param-hidden-text-template");
        var autocomplete = AJS.$("input[type='text']", paramDiv);
        var input = AJS.$("input[type='hidden']", paramDiv);

        var list = makeItemList(param, input, autocomplete, options.onchange);

        var onselect = function(selection) {
            // User makes selection from the search drop-down.
            var span = $("span", selection);
            var props = AJS.$.data(span[0], "properties");
            var item = makeListItem(props);
            addItemToList(list, item.value, item.display);
            autocomplete.focus();
        };

        /**
         * Needs to be changed so that the query parameter is appended in quicksearch
         */
        autocomplete.quicksearch("/json/contentnamesearch.action?" + queryParams(), function(dd) {
            $("ol.last", dd).remove();  // no need for "Search for" at bottom.
            $("a", dd).click(function(e) {
                e.preventDefault();
                (options.onselect || onselect)(e.target);
            });
        });

        if (value) {
            (options.setValue || setValue)(value, list);
        }

        return AJS.MacroBrowser.Field(paramDiv, input);
    };

    /**
     * Update the dependencies of the identified parameter with the supplied value.
     */
    var updateDependencies = function (paramName, dependencies, value) {
        if (dependencies && dependencies.length) {
            for ( var i = 0, length = dependencies.length; i < length; i++) {
                AJS.MacroBrowser.fields[dependencies[i]].dependencyUpdated(paramName, value);                        
            }
        }
    };

    return {
      "updateDependencies" : updateDependencies,
    	
      "username" : function(param, options) {
            if (param.multiple) {
                return AJS.MacroBrowser.ParameterFields.string(param, options);
            }
        
            options = options || {};

            options.queryParams = options.queryParams || function () {
                return "/json/contentnamesearch.action?type=userinfo";
            };        
        
            var paramDiv = AJS.clone("#macro-param-template");
            var input = AJS.$("input[type='text']", paramDiv);

            // CONF-16859 - check if mandatory params are now filled
            if (param.required) {
                input.keyup(AJS.MacroBrowser.processRequiredParameters);
            }
            
            options.setValue = options.setValue || function (value) {
                input.val(value);
                updateDependencies(param.name, options.dependencies, input.val());                
                (typeof options.onchange == "function") && options.onchange.apply(input);
            };
        
            var onselect = options.onselect || function(selection) {
                // if the user selected the "no matches" message then do nothing
                if (!selection.hasClass("message")) {
                    var span = $("span", selection);
                    var contentProps = $.data(span[0], "properties");
                    var username = contentProps.href.substr(contentProps.href.lastIndexOf("/") + 2);  // HACK- should include username in result? dT
                    options.setValue(unescape(username));
                }
            };
        
            input.quicksearch(options.queryParams(), null, 
                { 
                    dropdownPostprocess : handleNoMatches,
                    dropdownPlacement : dropDownPlacer(input),
                    ajsDropDownOptions : createStandardDropDownOptions(onselect)
                } 
            );
            
            return AJS.MacroBrowser.Field(paramDiv, input);
        },        
        
      "spacekey" : function(param, options) {
        // for multple space keys just use a String field at the moment
        if (param.multiple) {
            return AJS.MacroBrowser.ParameterFields["string"](param, options);
        }
        
        options = options || {};

        options.queryParams = options.queryParams || function() {
            return "/json/contentnamesearch.action?type=spacedesc&type=personalspacedesc";
        };        
    
        var paramDiv = AJS.clone("#macro-param-template");
        var input = AJS.$("input[type='text']", paramDiv);
        
        // CONF-16859 - check if mandatory params are now filled
        if (param.required) {
            input.keyup(AJS.MacroBrowser.processRequiredParameters);
        } 
        
        options.setValue = options.setValue || function (value) {
            input.val(value);
            updateDependencies(param.name, options.dependencies, input.val());                
            (typeof options.onchange == "function") && options.onchange.apply(input);
        };
    
        var onselect = options.onselect || function(selection) {
            // if the user selected the "no matches" message then do nothing
            if (!selection.hasClass("message")) {
                // User makes selection from the search drop-down.
                var span = $("span", selection);
                var contentProps = $.data(span[0], "properties");
                options.setValue(contentProps.spaceKey);
            }
        };
    
        input.quicksearch(options.queryParams(), null, 
            { 
                dropdownPostprocess : handleNoMatches,
                dropdownPlacement : dropDownPlacer(input),
                ajsDropDownOptions : createStandardDropDownOptions(onselect)            
            }
        );
        
        return AJS.MacroBrowser.Field(paramDiv, input);
      },         

    "attachment" : function (param, options) {
            if (param.multiple) {
                return AJS.MacroBrowser.ParameterFields["string"](param,
                        options);
            }

            var paramDiv = AJS.clone("#macro-param-select-template");
            var input = AJS.$("select", paramDiv);

            options = options || {};
            options.setValue = options.setValue || function(value) {
                // check if the value being set is in the list of options
                // if not then add it as a new option - with an indication that
                // it is not a valid choice for this select box
                var foundOption = false;
                input.find("option").filter("[value=" + value + "]").each(function() {
                    foundOption = true;
                });

                if (!foundOption) {
                    input.append(AJS.$("<option/>").attr("value", value).html(value + " (" + AJS.params.notFound + ")"));
                    input.tempValue = value;
                } else {
                    delete input.tempValue;
                }
                
                // CONF-15415 : Spurious error thrown in IE6
                try {
                    input.val(value);
                } catch (err) {
                    AJS.log(err);
                }

                input.change();
            };

            var field = AJS.MacroBrowser.Field(paramDiv, input, options);
            field.updateDependencies = updateDependencies;
            field.getData = function(req) {
            	if (!((req.title && req.spaceKey) || req.pageId || req.draftId)) {
            		AJS.log("Not enough parameters to send attachmentsearch request");
                    return;	// not enough content info to get attachments
            	}

                var currentValue = input.tempValue || input.val();

                if (options.fileTypes) {
                    req.fileTypes = options.fileTypes;
                }
                
                var url = AJS.params.contextPath + (req.draftId ? "/json/draftattachmentsearch.action" : "/json/attachmentsearch.action");
                $.getJSON(url, req, function(data) {
                    if (data.error) {
                        return;
                    }

                    $("option", input).remove();
                    var attachments = data.attachments;
                    
                    // if there are no attachments on the page then populate the options with 
                    // a message stating this
                    if (!attachments.length) {
                        // AJS.log("attachment - No attachments so creating message. tempValue = " + input.tempValue);
                        input.append(AJS.$("<option/>").attr("value", "").html(AJS.params.noAppropriateAttachments));

                        if (input.tempValue) {
                            options.setValue(input.tempValue);
                        }
                    } else {
                        for (var i = 0, length = attachments.length; i < length; i++) {
                            input.append(AJS.$("<option/>").attr("value", attachments[i].fileName).html(attachments[i].fileName));
                        }
                        
                        currentValue = currentValue || input.tempValue;
                        options.setValue(currentValue || attachments[0].fileName);
                    }
                });
            };

            return field;
        },

    "confluence-content" : function (param, options) {
    
        // If multiple confluence-content field then only return a String at the moment
        if (param.multiple) {
            return AJS.MacroBrowser.ParameterFields["string"](param, options);
        }
        
        options = options || {};
        param.options = param.options || {};

        options.queryParams = function() {
            // Allow type override from XML descriptor
            var types = (param.options.type || "page,blogpost").split(",");
            for (var i = 0, len = types.length; i < len; i++) {
                types[i] = "type=" + types[i];
            }
            var typeStr = types.join("&");

            // Allow space override from XML descriptor
            var spaceStr = "";
            if (param.options.spaceKey) {
                if (param.options.spaceKey.toLowerCase() == "@self") {
                    param.options.spaceKey = AJS.params.spaceKey;
                }
                spaceStr = "&spaceKey=" + param.options.spaceKey;
            }

            return "/json/contentnamesearch.action?" + typeStr + spaceStr;
        };
    
        var makeContentLink = function (contentProps) {
    
            var spaceKey = contentProps.spaceKey;
            var pageTitle = contentProps.name;
            var href = contentProps.href;
            // HACK - blogpost may have something like href
            // "/confluence/display/ds/2009/04/23/Neewws"
            // This is the only reference to the date of the blogpost so it
            // needs processing.
            // TODO - ContentNameSearch should include pageId in contentProps
            // returned. dT
            if (contentProps.className == "content-type-blogpost") {
                var path = href.substr(AJS.params.contextPath.length + 1);
                var spaceIndex = path.indexOf("/" + spaceKey + "/");
                if (spaceIndex > -1) {
                    // has URL-friendly blogpost title, set to be like -
                    // ds:/2009/04/23/Neewws
                    var dateStart = spaceIndex + spaceKey.length + 1;
                    pageTitle = path.substring(dateStart);
                }
                else {
                    // TODO - if we have content id, use it! dT
                }
            }
            pageTitle = pageTitle.replace(/\+/g, " ");  // TODO - should do complete URL decode?
            return ((spaceKey && spaceKey != AJS.params.spaceKey) ? (spaceKey + ":") : "") + pageTitle;
        };
    
        var paramDiv = AJS.clone("#macro-param-template");
        var input = AJS.$("input[type='text']", paramDiv);
        
        // CONF-16859 - check if mandatory params are filled on keypresses in this field.
        if (param.required) {
            input.keyup(AJS.MacroBrowser.processRequiredParameters);
        }

        
        options.setValue = options.setValue || function (value) {
            input.val(value);
            // input.change();
            (typeof options.onchange == "function") && options.onchange.apply(input);
        };
    
        var onselect = options.onselect || function(selection) {
            // if the user selected the "no matches" message then do nothing
            if (!selection.hasClass("message")) {
                // User makes selection from the search drop-down.
                var span = $("span", selection);
                var contentProps = $.data(span[0], "properties");
                var contentLink = makeContentLink(contentProps);
                options.setValue(contentLink, input);
            }
        };
        
        // CONF-15438 - update any dependencies of the field when it is changed
        options.onchange = options.onchange || function (e) {
        	var val = input.val();
        	updateDependencies(param.name, options.dependencies, val);
        };
        
        /**
         * Function to add a tooltip with the space name to all items.
         * 
         * This function is also responsible for removing the 'Search For'
         * item that is returned last in the list by the content name search
         * backend.
         */
        var spaceDifferentiation = function (list) {
             // remove the "search for" at the bottom of the list
            $("ol.last", list).remove();

             // check if there are items in the drop down. If none then display a message telling the user this
            if ($("ol", list).length == 0) {
                var noSuggestions = AJS.clone("#macro-param-smartfield-no-suggestion-template");
                list.append(noSuggestions.find("ol"));
            } else {
                // Add a tooltip with the space name and content title
                $("a", list).each(function () {
                    var $a = $(this);
                    var $span = $a.find("span");
                    $a.attr("title", "(" + AJS.dropDown.getAdditionalPropertyValue($span, "spaceName") + ") " + $span.text());
                });
            }
        };

        input.quicksearch(options.queryParams(), null, 
            { 
                dropdownPostprocess : spaceDifferentiation,
                dropdownPlacement : dropDownPlacer(input),
                ajsDropDownOptions : createStandardDropDownOptions(onselect)
            } 
        );
    
        return AJS.MacroBrowser.Field(paramDiv, input, options);
    },     
    
    /**
     * Default field for all unknown types.
     */
    "string" : function (param, options) {

        var paramDiv = AJS.clone("#macro-param-template");
        var input = $("input", paramDiv);

        if (param.required) {
            input.keyup(AJS.MacroBrowser.processRequiredParameters);
        }
        
        return AJS.MacroBrowser.Field(paramDiv, input, options);
    },

    /**
     * A checkbox - assumes not true means false, not null.
     */
    "boolean" : function (param, options) {

        var paramDiv = AJS.clone("#macro-param-checkbox-template");
        var input = $("input", paramDiv);

        options = options || {};
        options.setValue = options.setValue || function (value) {
            if (/true/i.test(value) ||
                (/true/i.test(param.defaultValue) && !(/false/i).test(value))) {
                input.attr("checked", "checked");
            }
        };

        return AJS.MacroBrowser.Field(paramDiv, input, options);
    },

    "enum" : function (param, options) {
        if (param.multiple) {
            return AJS.MacroBrowser.ParameterFields["string"](param, options);
        }

        var paramDiv = AJS.clone("#macro-param-select-template");
        var input = $("select", paramDiv);
        if (!(param.required || param.defaultValue)) {
            input.append(AJS.$("<option/>").attr("value", ""));
        }
        $(param.enumValues).each(function() {
            input.append(AJS.$("<option/>").attr("value", this).html("" + this));
        });

        return AJS.MacroBrowser.Field(paramDiv, input, options);
    },
    
    /**
     * Like a "string" field but hidden.
     */
    "_hidden" : function (param, options) {

        var paramDiv = AJS.clone("#macro-param-hidden-template").hide();
        var input = $("input", paramDiv);

        return AJS.MacroBrowser.Field(paramDiv, input, options);
    }    
    
}; })(AJS.$);

AJS.MacroBrowser.Macros = {};


