/*
 * jQuery JSON Plugin
 * version: 2.1 (2009-08-14)
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 *
 * Brantley Harris wrote this plugin. It is based somewhat on the JSON.org 
 * website's http://www.json.org/json2.js, which proclaims:
 * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that
 * I uphold.
 *
 * It is also influenced heavily by MochiKit's serializeJSON, which is 
 * copyrighted 2005 by Bob Ippolito.
 */
 
(function($) {
    /** jQuery.toJSON( json-serializble )
        Converts the given argument into a JSON respresentation.

        If an object has a "toJSON" function, that will be used to get the representation.
        Non-integer/string keys are skipped in the object, as are keys that point to a function.

        json-serializble:
            The *thing* to be converted.
     **/
    $.toJSON = function(o)
    {
        if (typeof(JSON) == 'object' && JSON.stringify)
            return JSON.stringify(o);
        
        var type = typeof(o);
    
        if (o === null)
            return "null";
    
        if (type == "undefined")
            return undefined;
        
        if (type == "number" || type == "boolean")
            return o + "";
    
        if (type == "string")
            return $.quoteString(o);
    
        if (type == 'object')
        {
            if (typeof o.toJSON == "function") 
                return $.toJSON( o.toJSON() );
            
            if (o.constructor === Date)
            {
                var month = o.getUTCMonth() + 1;
                if (month < 10) month = '0' + month;

                var day = o.getUTCDate();
                if (day < 10) day = '0' + day;

                var year = o.getUTCFullYear();
                
                var hours = o.getUTCHours();
                if (hours < 10) hours = '0' + hours;
                
                var minutes = o.getUTCMinutes();
                if (minutes < 10) minutes = '0' + minutes;
                
                var seconds = o.getUTCSeconds();
                if (seconds < 10) seconds = '0' + seconds;
                
                var milli = o.getUTCMilliseconds();
                if (milli < 100) milli = '0' + milli;
                if (milli < 10) milli = '0' + milli;

                return '"' + year + '-' + month + '-' + day + 'T' +
                             hours + ':' + minutes + ':' + seconds + 
                             '.' + milli + 'Z"'; 
            }

            if (o.constructor === Array) 
            {
                var ret = [];
                for (var i = 0; i < o.length; i++)
                    ret.push( $.toJSON(o[i]) || "null" );

                return "[" + ret.join(",") + "]";
            }
        
            var pairs = [];
            for (var k in o) {
                var name;
                var type = typeof k;

                if (type == "number")
                    name = '"' + k + '"';
                else if (type == "string")
                    name = $.quoteString(k);
                else
                    continue;  //skip non-string or number keys
            
                if (typeof o[k] == "function") 
                    continue;  //skip pairs where the value is a function.
            
                var val = $.toJSON(o[k]);
            
                pairs.push(name + ":" + val);
            }

            return "{" + pairs.join(", ") + "}";
        }
    };

    /** jQuery.evalJSON(src)
        Evaluates a given piece of json source.
     **/
    $.evalJSON = function(src)
    {
        if (typeof(JSON) == 'object' && JSON.parse)
            return JSON.parse(src);
        return eval("(" + src + ")");
    };
    
    /** jQuery.secureEvalJSON(src)
        Evals JSON in a way that is *more* secure.
    **/
    $.secureEvalJSON = function(src)
    {
        if (typeof(JSON) == 'object' && JSON.parse)
            return JSON.parse(src);
        
        var filtered = src;
        filtered = filtered.replace(/\\["\\\/bfnrtu]/g, '@');
        filtered = filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
        filtered = filtered.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
        
        if (/^[\],:{}\s]*$/.test(filtered))
            return eval("(" + src + ")");
        else
            throw new SyntaxError("Error parsing JSON, source is not valid.");
    };

    /** jQuery.quoteString(string)
        Returns a string-repr of a string, escaping quotes intelligently.  
        Mostly a support function for toJSON.
    
        Examples:
            >>> jQuery.quoteString("apple")
            "apple"
        
            >>> jQuery.quoteString('"Where are we going?", she asked.')
            "\"Where are we going?\", she asked."
     **/
    $.quoteString = function(string)
    {
        if (string.match(_escapeable))
        {
            return '"' + string.replace(_escapeable, function (a) 
            {
                var c = _meta[a];
                if (typeof c === 'string') return c;
                c = a.charCodeAt();
                return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16);
            }) + '"';
        }
        return '"' + string + '"';
    };
    
    var _escapeable = /["\\\x00-\x1f\x7f-\x9f]/g;
    
    var _meta = {
        '\b': '\\b',
        '\t': '\\t',
        '\n': '\\n',
        '\f': '\\f',
        '\r': '\\r',
        '"' : '\\"',
        '\\': '\\\\'
    };
})(jQuery);

/**
 * editor_plugin_src.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

(function(tinymce, $) {
	tinymce.create('tinymce.plugins.EmotionsPlugin', {
		init : function(ed, url) {
			// Register commands
			ed.addCommand('mceEmotion', function() {
				ed.windowManager.open({
					file : tinyMCE.settings.plugin_action_base_path + '/emotions.action',
					width : 190 + parseInt(ed.getLang('emotions.delta_width', 0)),
					height : 140 + parseInt(ed.getLang('emotions.delta_height', 0)),
					inline : 1,
                    id: "insert-emoticon-dialog",
                    name : "emotions_dlg.title"
				}, {
					plugin_url : url
				});
			});

			// Register buttons
			ed.addButton('emotions', {title : 'emotions.emotions_desc', cmd : 'mceEmotion'});

            // CONFDEV-2652 - Deal with click of emoticons - reposition cursor to the left or right depending on where
            // the users clicked on the emoticon
            ed.onClick.add(function(ed, e) {
                var n = e.target, $n, rng;
                if(n.nodeName === 'IMG') {
                    $n = $(n);
                    if($n.hasClass('emoticon')) {
                        rng = ed.selection.getRng(true);
                        var clickOffsetX = e.offsetX || (e.layerX - n.x);
                        var width = n.width;
                        if(clickOffsetX < width / 2) {
                            // Click on left side - move cursor before
                            rng.setStartBefore(n);
                            rng.setEndBefore(n);
                        } else {
                            // Click on left side - move cursor after
                            rng.setStartAfter(n);
                            rng.setEndAfter(n);

                        }
                        ed.selection.setRng(rng);
                        ed.selection.collapse();

                        return tinymce.dom.Event.prevent(e);
                    }
                }
            });
		},

		getInfo : function() {
			return {
				longname : 'Emotions',
				author : 'Moxiecode Systems AB',
				authorurl : 'http://tinymce.moxiecode.com',
				infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/emotions',
				version : tinymce.majorVersion + "." + tinymce.minorVersion
			};
		}
	});

	// Register plugin
	tinymce.PluginManager.add('emotions', tinymce.plugins.EmotionsPlugin);
})(tinymce, AJS.$);

(function($) {

    var editorWin,
        editorDoc,
        containerWin,
        iframe,
        table,
        edHeight,
        $topToolbar,
        $bottomToolbar,
        extraChromeHeight = 0,
        shadowMargins = 5;

    var resizeEditor = function(extraHeight) {
        extraHeight = extraHeight || 0;
        extraChromeHeight = $topToolbar.height() + $bottomToolbar.height();
        edHeight = AJS.Editor.Adapter.getTinyMceEditorMinHeight(extraChromeHeight - extraHeight - shadowMargins);
        tinymce.DOM.setStyle(iframe, 'height', edHeight + 'px');
        tinymce.DOM.setStyle(table, 'height', edHeight + 'px');
        tinymce.DOM.setStyle(editorDoc.body, 'min-height', ($.browser.opera ? edHeight : (edHeight-20)) + 'px');
    };

	tinymce.create('tinymce.plugins.flextofullsizeplugin', {
	    init : function(ed,url){
            ed.onInit.add(function(e, l) {
                editorWin = ed.getWin();
                editorDoc = ed.getDoc();
                containerWin = editorWin.parent;
                iframe = tinymce.DOM.get(ed.id + '_ifr');
                table = tinymce.DOM.get(ed.id + '_tbl');
                $topToolbar = $("#toolbar");
                $bottomToolbar = $("#savebar-container");
                edHeight = iframe.clientHeight;

                $("body").css({"overflow": "hidden"});
                $(containerWin).bind('resize', function() {
                    resizeEditor();
                });
                $(document).bind("resize.resizeplugin", function(e, data) {
                    resizeEditor(data && data.height);
                });
                ed.onChange.add(function() {
                    resizeEditor();
                });
                ed.onInit.add(function() {
                    resizeEditor();
                });
            });
        },
        createControl : function(n, cm) {
			return null;
		},

		getInfo : function() {
			return {
				longname : 'flex editor to full size plugin',
                description : 'adjusts height of editor so it always occupies the right space in between the toolbars',
				author : 'Atlassian',
				authorurl : 'http://www.atlassian.com',
				version : "1.0"
			};
		}
	});

	tinymce.PluginManager.add('flextofullsize', tinymce.plugins.flextofullsizeplugin);
})(AJS.$);
//TODOXHTML extract some of the util methods from adapter to here
tinymce.confluence = tinymce.confluence || {};

(function ($) {
    var that;
    tinymce.confluence.NodeUtils = that = {

        /**
         * Controls the editor while updating placeholder nodes with new data and attributes.
         *
         * - Handles data and attribute update requests to the server.
         * - Prevents form submission while data is retrieved and updated in the editor.
         * - Provides feedback to the user as to the progress of a placeholder's update.
         *
         * @param node The existing DOM node that will be updated
         * @param request an object that contains parameters relevant to making an
         *        AJAX server request. Same interface as jQuery#ajax.
         *        Must include the 'url' and 'data' parameters.
         *        The default request is a POST that expects to
         *        send JSON and receive a text response.
         * @param options an object containing additional parameters for the replacement task.
         *        can include an action, which is a function to perform instead of
         *        the default behaviour (as defined by NodeUtils#replaceNode).
         * @returns a jQuery promise object to which you can attach callbacks.
         *
         * @note You will need to create your own undo steps before and/or after you call this function.
         */
        updateNode: function (node, request, options) {
            AJS.debug("updateNode called");
            var ed = tinymce.activeEditor,
                $oldNode = $(node, ed.getDoc()),
                retrieveNewNodeTask, // ajax call to server
                replaceOldNodeTask, // replacement of node
                callbacks = $.Deferred(); // whatever the calling function wants to do

            options = $.extend({}, options);

            // FIXME: Terrible, terrible clone of functionality found in page-editor.js
            var getButtons = function() {
                var saveButton = $('#rte-button-publish'),
                    overwriteButton = $('#rte-button-overwrite'),
                    editButton = $('#rte-button-edit'),
                    previewButton = $('#rte-button-preview');
                return [saveButton, overwriteButton, editButton, previewButton];
            };
            var disableSubmission = function(buttons) {
                var i, len = buttons.length;
                for(i=0; i < len; i++) {
                    buttons[i].addClass("disabled");
                }
                return function() {
                    for(i=0; i<len; i++) {
                        buttons[i].removeClass("disabled");
                    }
                }
            };

            replaceOldNodeTask = $.Deferred(function() {
                var reEnableSubmission = disableSubmission(getButtons()),
                    revertNodeState = that.prepareNodeForUpdate($oldNode);

                this.done(reEnableSubmission);
                this.fail(reEnableSubmission);

                // FIXME: revert node state on successs?
                options.revertNodeState = revertNodeState;
                this.fail(revertNodeState);

                this.done(function(node) {
                    // Let the calling functions do their thing first,
                    // then call nodeChanged to trigger tinymce-related things.
                    callbacks.resolve(node);
                    ed.nodeChanged();
                });
                this.fail(callbacks.reject);
            });

            request = request || {};
            request = $.extend({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                dataType: "text"
            }, request);

            retrieveNewNodeTask = $.ajax(request);

            retrieveNewNodeTask.done(function(nodeHtml) {
                // Do the actual replacement of the node.
                var task = that.replaceNode($oldNode, nodeHtml, options);
                // Hook in to the success and failure of the replacement.
                task.done(function(n) { replaceOldNodeTask.resolve(n); });
                task.fail(replaceOldNodeTask.reject);
            });
            retrieveNewNodeTask.fail(replaceOldNodeTask.reject);

            return callbacks.promise();
        },


        storeNodeState: function ($node, attrs) {
            var attributeCache = {}, attr;
            for (var i = 0, ii = attrs.length; i < ii; i++) {
                attr = attrs[i];
                attributeCache[attr] = $node.attr(attr);
            }
            return function() {
                for (var attr in attributeCache) {
                    $node.attr(attr, attributeCache[attr]);
                }
            }
        },

        prepareNodeForUpdate: function ($node) {
            var restoreNodeState = null;

            if ($node.is("img")) {
                restoreNodeState = that.storeNodeState($node, ["width", "height", "src", "class"]);

                // Update the image node to indicate we're doing something.
                $node.addClass("image-hotswap").attr({
                    "src": Confluence.getContextPath() + "/images/border/spacer.gif",
                    "width": $node.attr("width"),
                    "height": $node.attr("height")
                });
            }
            return restoreNodeState;
        },

        /**
         * Controls the replacement of old nodes in the editor with new ones.
         *
         * Handles replacement such that there should be no flashes of unstyled content,
         * as could be the case with images.
         *
         * @param oldNode The existing DOM node that will be replaced
         * @param newNodeHtml The html for the node that will override the oldNode
         * @param options an object containing additional parameters for the replacement task.
         *        can include an action, which is a function to perform instead of
         *        the default behaviour (replacing the oldNode with the newNodeHtml).
         * @returns a jQuery promise object to which you can attach callbacks.
         */
        replaceNode: function (oldNode, newNodeHtml, options) {
            var ed = tinymce.activeEditor,
                bm = ed.selection.getBookmark(2),
                task = $.Deferred(),
                doc = that.getDoc(),
                $newNode = $(newNodeHtml, doc),
                $oldNode = $(oldNode, doc),
                doTheReplace = function() {
                    options.action($oldNode, $newNode, options);
                    ed.selection.select($newNode[0], true);
                    ed.selection.collapse(false);
                    task.resolve($newNode[0]);
                };

            // The default action is to replace the old node with the new one.
            options = $.extend({
                action: function($old, $new) {
                    $old.replaceWith($new);
                }
            }, options);

            if ($newNode.is("img")) {
                /**
                 * Here we're delaying the replacement of the node until the image's src has been
                 * fully loaded by the browser.
                 */
                $newNode[0].onload = function() {
                    AJS.log("replaceNode: new node's src has been loaded by the browser.");
                    this.onload = null;
                    doTheReplace();
                };
                // In Opera, the src has to be defined after the onLoad callback or it won't work.
                if ($.browser.opera) {
                    $newNode[0].src = $newNode.attr('src');
                }
                /**
                 * Trigger rendering of the new DOM node, in turn requesting the image's src,
                 * ultimately triggering the onLoad event above.
                 */
                doc.createDocumentFragment().appendChild($newNode[0]);
            } else {
                doTheReplace();
            }
            return task.promise();
        },

        /**
         * Replaces whatever the active range in the editor is with the node.
         * If no text is selected in the editor, the node will be placed at the cursor position,
         * and the cursor will be placed after the new node.
         * @param newNode element to put into the editor.
         */
        replaceSelection: function(newNode) {
            var ed = tinymce.activeEditor,
                range = ed.selection.getRng(true),
                node, tmpId,
                selectRange;

          if ($.browser.msie && ~~$.browser.version < 9 ) {
            selectRange = range;
          } else {
            selectRange =  ed.getDoc().createRange();
          }

          //we have a mess off event handlers, so we need to make sure we clone them too.
          node = $(newNode).clone(true,false)[0];
          // Delete the currently-selected content
          range.deleteContents();
          //the text node can not be empty for some reason in ie
          if(range.startContainer.nodeType == 3 && range.startContainer.nodeValue === "") {
              range.startContainer.nodeValue = AJS.Editor.Adapter.HIDDEN_CHAR;
          }
          // Replace it with the new node
          range.insertNode(node);
          var text = ed.getDoc().createTextNode(AJS.Editor.Adapter.HIDDEN_CHAR);
          ed.dom.insertAfter(text,node);
          selectRange.setStartAfter(text);
          selectRange.setEndAfter(text);
          selectRange.collapse(false);
          ed.selection.setRng(selectRange);
          return node;
        },

        /**
         * For when you need to create elements on the correct document object.
         *
         * - Firefox and Opera won't fire callbacks or add nodes to the editor
         *   unless you use the page's document.
         * - Webkit works with either document object, but has an issue
         *   with the page's document where it will re-create the node when
         *   passing it to the editor's iframe, which will in turn cause its
         *   callbacks and handles to fire twice.
         * - IE needs to use the editor's document.
         *
         * @returns the best document object to create nodes on, depending on the browser.
         */
        getDoc: function() {
            var ed = tinymce.activeEditor;
            return ($.browser.mozilla || $.browser.opera) ? document : ed.getDoc();
        }
    };
}(AJS.$));
AJS.toInit(function ($) {
    var rangeAfterAutoFormat;

    if (AJS.Meta.get('remote-user') && AJS.Meta.get('confluence.prefs.editor.disable.autoformat')) {
        return; //autoformat enabled by default and for anon-users
    }

    function encodeDomNode(domNode){
        return $('<div>').append( domNode.cloneNode(true) ).html();
    }
    function getStartContainer(lengthOfMatch, currentTextNode, currentOffset) {
		if (!currentTextNode) {
			throw new Error("text node is null");
		}
		if (currentTextNode.nodeType != 3) {
			throw new Error("node passed in is not a text node");
		}

        for (var ps = currentTextNode, runningOffset = currentOffset; ps && ps.nodeType == 3; ps = ps.previousSibling) {
            if (runningOffset == -1) {
                runningOffset = ps.nodeValue.length;
            }

            if (runningOffset >= lengthOfMatch) {
				return {
					container: ps,
					offset: runningOffset - lengthOfMatch
				};
			} else {
				lengthOfMatch -= runningOffset;
                runningOffset = -1;
			}
        }

		return null;
	}

	function getTextFromAllPreviousSiblingTextNodes(currentTextNode, currentStartOffset) {
		if (!currentTextNode) {
			throw new Error("text node is null");
		}
		if (currentTextNode.nodeType != 3) {
			throw new Error("node passed in is not a text node");
		}

		var result = currentTextNode.nodeValue.substring(0, currentStartOffset);

		for (var ps = currentTextNode.previousSibling; ps && ps.nodeType == 3; ps = ps.previousSibling) {
			result = ps.nodeValue + result;
		}

		return result;
	}

    function createHandler(regex, fragmentCreator, swallowTriggerChar, containerFilter) {
        return {
            handles: function (ed) {
                var result = false,
                    range = ed.selection.getRng(true),
                    candidateTextNode = range.commonAncestorContainer || {}, textNode;

                // CONFDEV-4376: Autoformat should be disabled in preformatted text.
                if ($(candidateTextNode).closest("pre").length) {
                    return false;
                }

                if (containerFilter && $(candidateTextNode).closest(containerFilter).length) {
                    return false;
                }

                if (candidateTextNode.nodeType == 3) {
                    textNode = candidateTextNode;
                } else if (candidateTextNode.nodeName == "P" && candidateTextNode.nodeType == 1 && range.startOffset > 0) {
                    /**
                     * When text is pasted in Firefox and Chrome, the cursor is shown blinking after the inserted text.
                     * It LOOKS as if the cursor is at the end of the text node containing said text.
                     * However, the cursor is actually outside of the text node containing said text.
                     * More specifically it is positioned after the text node.
                     * The following code moves the cursor into the text node such that all of our wiki auto-format logic
                     * can continue to based on a text node.
                     * <p>
                     * Copy and pasting text is but one reproducible way to cause this situation.
                     */
                    candidateTextNode = candidateTextNode.childNodes[range.startOffset - 1];
                    if (candidateTextNode.nodeType == 3) {
                        range.setStart(candidateTextNode, candidateTextNode.nodeValue.length);
                        range.setEnd(candidateTextNode, candidateTextNode.nodeValue.length);
                        ed.selection.setRng(range);
                        textNode = candidateTextNode;
                    }
                }

                if (textNode) {
                    result = regex.test(getTextFromAllPreviousSiblingTextNodes(textNode, ed.selection.getRng(true).startOffset));
                }

                return result;
            },
            execute: function (ed, nativeEvent, jQueryEvent) {
                var range,
                    combinedText,
                    matchGroups,
                    matchGroupsOffset = 1,
                    startContainerData,
                    commonAncestor,
                    charCode = getCharCode(nativeEvent);

                if (charCode == 32) {
                    ed.execCommand('mceInsertContent', false, '&nbsp;'); //spaces don't stick around for undos (it's tragic, I know)
                } else {
                    ed.execCommand('mceInsertContent', false, String.fromCharCode(charCode));
                }

                ed.undoManager.beforeChange();
                ed.undoManager.add();

                range = ed.selection.getRng(true);
                combinedText = getTextFromAllPreviousSiblingTextNodes(range.commonAncestorContainer, range.startOffset);
                
                //CONFDEV-4771 Just for tables, add a space at the end and change the offset for matchGroups
                //This fixes and IE8 issue where the table autoformat didn't always work
                if(combinedText[combinedText.length-1] == '|') {
                    combinedText += ' ';
                    matchGroupsOffset = 0;
                }

                matchGroups = regex.exec(combinedText.substring(0, combinedText.length-1)); // regexes need to work for handles() where trigger character has not been appended
                startContainerData = getStartContainer(matchGroups[1].length+matchGroupsOffset, range.commonAncestorContainer, range.startOffset);
                range.setStart(startContainerData.container, startContainerData.offset);
                commonAncestor = $(range.commonAncestorContainer);
                ed.selection.setRng(range); //we have to set the editors selection now that we have modified the range to have text selected

                if (commonAncestor.closest(".wysiwyg-macro-body").length && range.toString() == commonAncestor.text()){
                    //if the entire text of container node is selected and we're in a macro, don't let let the macro get deleted
                    commonAncestor[0].innerHTML = "<br>";
                    ed.selection.select(commonAncestor[0].childNodes[0]);
                    ed.selection.collapse(true);//mceInsertContent throws an exception when only <br> is selected
                } else {
                    //else delete the selection since the area will be kept cursor targetable by whatever else is there.
                    ed.execCommand('delete', false, {}, {skip_undo: true});
                }

                fragmentCreator(matchGroups, ed.selection.getRng(true));
                rangeAfterAutoFormat = ed.selection.getRng(true);

                if (swallowTriggerChar) {
                    jQueryEvent.preventDefault();
                    jQueryEvent.stopPropagation();
                    tinymce.dom.Event.cancel(nativeEvent);
                    // CONFDEV-2503 - cancelling the event stops the browser from scrolling to the new content
                    // so manually scroll there.
                    AJS.Rte.showElement(rangeAfterAutoFormat.startContainer);
                    return false;
                }
            }
        };
    }

    function getCharCode (nativeEvent) {
        return AJS.$.browser.msie ? nativeEvent.keyCode : nativeEvent.which;
    }

    function HandlerManager () {
        this.handlers = {};
    }
    HandlerManager.prototype = {
        registerHandler: function (triggerCharCode, handler) {
            if (!this.handlers[triggerCharCode]) {
                this.handlers[triggerCharCode] = [];
            }

            this.handlers[triggerCharCode].push(handler);
        },
        executeHandlers: function (triggerCharCode, ed, nativeEvent, jQueryEvent) {
            var result = true;
            $.each(this.handlers[triggerCharCode] || [], function (i, handler) {
                if (handler.handles(ed)) {
                    result = handler.execute(ed, nativeEvent, jQueryEvent);
                    return false; // signal end of iteration
                }
            });

            return result;
        }
    };

    AJS.bind("init.rte", function (jQueryEvent, data) {
        var ed = data.editor,
            handlerManager = new HandlerManager();

        /* Emoticons */
        
        /** Create an Emoticon object and register it with the handlerManager */
        function Emoticon(triggerChar, handlerRegex, emoticon, title, image) {
            var triggerCharCode = triggerChar.charCodeAt(0),
                imagePath = AJS.params.editorPluginResourcePrefix + "/images/icons/emoticons/" + image,
                handler;

            handler = createHandler(handlerRegex, function() {
                var img = ed.dom.createHTML("img", {
                    "src" : imagePath,
                    "alt" : ed.getLang(title),
                    "title" : ed.getLang(title),
                    "border" : 0,
                    "class" : "emoticon emoticon-" + emoticon,
                    "data-emoticon-name" : emoticon
                });
                ed.execCommand('mceInsertContent', false, img, {skip_undo: true});
            }, true);
            this.imagePath = imagePath;
            handlerManager.registerHandler(triggerCharCode, handler);
        }
        
        var emoticons = [ 
              new Emoticon(")", /(:-?)$/, "smile", "emotions_dlg.smile", "smile.png"),
              new Emoticon("(", /(:-?)$/, "sad", "emotions_dlg.sad", "sad.png"),
              new Emoticon("P", /(:-?)$/, "cheeky", "emotions_dlg.tongue", "tongue.png"),
              new Emoticon("p", /(:-?)$/, "cheeky", "emotions_dlg.tongue", "tongue.png"),
              new Emoticon("D", /(:-?)$/, "laugh", "emotions_dlg.biggrin", "biggrin.png"),
              new Emoticon(")", /(;-?)$/, "wink", "emotions_dlg.wink", "wink.png"),
              new Emoticon(")", /(\(y)$/, "thumbs-up", "emotions_dlg.thumbs_up", "thumbs_up.png"),
              new Emoticon(")", /(\(n)$/, "thumbs-down", "emotions_dlg.thumbs_down", "thumbs_down.png"),
              new Emoticon(")", /(\(i)$/, "information", "emotions_dlg.information", "information.png"),
              new Emoticon(")", /(\(\/)$/, "tick", "emotions_dlg.check", "check.png"),
              new Emoticon(")", /(\(x)$/, "cross", "emotions_dlg.error", "error.png"),
              new Emoticon(")", /(\(!)$/, "warning", "emotions_dlg.warning", "warning.png"),
              new Emoticon(")", /(\(\+)$/, "plus", "emotions_dlg.add", "add.png"),
              new Emoticon(")", /(\(-)$/, "minus", "emotions_dlg.forbidden", "forbidden.png"),
              new Emoticon(")", /(\(\?)$/, "question", "emotions_dlg.help_16", "help_16.png"),
              new Emoticon(")", /(\(on)$/, "light-on", "emotions_dlg.lightbulb_on", "lightbulb_on.png"),
              new Emoticon(")", /(\(off)$/, "light-off", "emotions_dlg.lightbulb", "lightbulb.png"),
              new Emoticon(")", /(\(\*)$/, "yellow-star", "emotions_dlg.star_yellow", "star_yellow.png"),
              new Emoticon(")", /(\(\*y)$/, "yellow-star", "emotions_dlg.star_yellow", "star_yellow.png"),
              new Emoticon(")", /(\(\*r)$/, "red-star", "emotions_dlg.star_red", "star_red.png"),
              new Emoticon(")", /(\(\*g)$/, "green-star", "emotions_dlg.star_green", "star_green.png"),
              new Emoticon(")", /(\(\*b)$/, "blue-star", "emotions_dlg.star_blue", "star_blue.png")
        ];
        
        // Preload the emoticon images for faster auto-complete response
        var emoticonImages = new Array();
        for (var i=0; i < emoticons.length; i++) {
            emoticonImages[i] = new Image();
            emoticonImages[i].src = emoticons[i].imagePath;            
        }        
        
        /* End Emoticons */
        function formatInlineText (inlineFormat, content) {
            var format = ed.formatter.get(inlineFormat)[0],
                domNode = ed.dom.create(format.inline, {style: format.styles});
            domNode.appendChild(document.createTextNode(content));
            ed.execCommand('mceInsertContent', false, encodeDomNode(domNode), {skip_undo: true});
            ed.formatter.remove(inlineFormat); //disable the format so that typing after doesn't come out formatted
        }

        //add handlers for common inline wikimarkup styling 
        var handlerFormatMap = {'*':'bold', '_':'italic', '~':'subscript', '^':'superscript', '+':'underline', '-':'strikethrough'};
        $.each(handlerFormatMap, function(handler, format){
            var regex = new RegExp('(?:[\\s\\xA0\\u200b]+|^)(\\' + handler + '(?=[^\\s' + handler + '])([^' + handler + ']*?[^\\s]))$'); //\\xA0 is &nbsp; which is used in ie to create cursor targets
            handlerManager.registerHandler(handler.charCodeAt(0), createHandler(regex, function (matchGroups) { // require a space before asterisk to handle people using asterisk to denote footnotes
                formatInlineText(format, matchGroups[2]);
            }, true));
        });

        //register the code element with tiny's text formatter engine (move this into tinymce.init config)
        ed.formatter.register('code', {inline : 'code'});
        handlerManager.registerHandler("}".charCodeAt(0), createHandler(/(?:[\s\xA0\u200b]+|^)({{(?=[^\s])([^}]*?[^\s])})$/, function (matchGroups) {
            formatInlineText('code', matchGroups[2]);
        }, true));

        //headers h1-h6
        for (var i = 1; i <= 6; i++) {
            (function (count) {
                handlerManager.registerHandler(" ".charCodeAt(0), createHandler(new RegExp("^\\u200b?(h" + count + "\\.)$"), function () {
                    ed.execCommand('formatBlock', false, 'h' + count, {skip_undo: true});
                }, true));
            })(i);
        }

        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/^\u200b?(bq\.)$/, function () {
            ed.execCommand('formatBlock', false, 'blockquote', {skip_undo: true});
        }, true));

        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/^\u200b?(\*)$/, function () {
            ed.plugins.lists.applyList('ul', 'ol');
        }, true));

        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/^\u200b?(\#)$/, function () {
            ed.plugins.lists.applyList('ol', 'ul');
        }, true));

        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/^\u200b?(1\.)$/, function () {
            ed.plugins.lists.applyList('ol', 'ul');
        }, true));

        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/^\u200b?(\-)$/, function () {
            var dom = ed.dom,
                list;
            ed.plugins.lists.applyList('ul', 'ol');
            list = dom.getParent(ed.selection.getNode(), 'ol,ul');
            if (list) {
                dom.setStyles(list, {listStyleType:'square'});
				list.removeAttribute('data-mce-style');
            }
        }, true));

        /**
         * Handle em and en dashes where there are surrounding spaces
         */
        handlerManager.registerHandler(" ".charCodeAt(0), createHandler(/[^-]*[\s](\-\-\-?)$/, function (matchGroups) {
            var dash = matchGroups[1].length === 2 ? "\u2013" : "\u2014";
            ed.execCommand('mceInsertContent', false, dash, {skip_undo: true});
        }, false));
        /**
         * Handle em and en dashes with words on either side of the dashes. Delay the autoformat until a space or enter is
         * pressed after the last word has been typed. That is, for "foo--bar", only trigger when a space or enter is
         * pressed after "bar".
         */
        var dashHandler = createHandler(/(([^\s-]+)(\-\-\-?)([^\s-]+))$/, function (matchGroups) {
            var dash = matchGroups[3].length === 2 ? "\u2013" : "\u2014";
            ed.execCommand('mceInsertContent', false, matchGroups[2] + dash + matchGroups[4], {skip_undo: true});
        }, false);
        handlerManager.registerHandler(" ".charCodeAt(0), dashHandler);
        handlerManager.registerHandler(13, dashHandler);

        handlerManager.registerHandler(13, createHandler(/^\u200b?(\-\-\-\-)$/, function () {
            ed.execCommand('mceInsertContent', false, '<hr />', {skip_undo: true});
        }, true));

        handlerManager.registerHandler(13, createHandler(/(^\u200b?\|\|\s*(?:[^|]*\s?\|\|\s?)+$)/, function (matchGroups) {
            var tableMarkup = "<table class='confluenceTable'><tr>",
                tableRows = "",
                cellsEmpty = true,
                cellWords = $(matchGroups[1].slice(2,-2).split('||')).map(function(cellWord){
                    cellWord = $.trim(this);
                    cellsEmpty = cellsEmpty && cellWord == "";
                    return cellWord;
                });
            if (cellsEmpty) {
                cellWords[0] = "first cell";
            }

            for (var k = 0, l = cellWords.length; k < l; k++) {
                tableMarkup += "<th class='confluenceTh'>" + cellWords[k] + "</th>";
                tableRows += "<td class='confluenceTd'>" + AJS.Editor.Adapter.ZERO_WIDTH_WHITESPACE + "</td>";
            }
            tableMarkup += "</tr><tr>" + tableRows + "</tr></table>";
            ed.execCommand('mceInsertContent', false, tableMarkup, {skip_undo: true});
            ed.selection.select($(ed.selection.getRng(true).commonAncestorContainer).parents('table').find(cellsEmpty ? 'th' : 'td')[0].childNodes[0]);
        }, true));

        handlerManager.registerHandler(13, createHandler(/(^\u200b?\|\s?(?:[^|]*\s?\|\s?)+$)/, function (matchGroups) {
            var tableMarkup = "<table class='confluenceTable'><tr>",
                cellsEmpty = true,
                cellWords = $(matchGroups[1].slice(1,-1).split('|')).map(function(cellWord){
                    cellWord = $.trim(this);
                    cellsEmpty = cellsEmpty && cellWord == "";
                    return cellWord;
                });
            if (cellsEmpty) {
                cellWords[0] = "first cell";
            }
            for (var k = 0, l = cellWords.length; k < l; k++) {
                tableMarkup += "<td class='confluenceTd'>" + cellWords[k] + "</td>";
            }
            tableMarkup += "</tr></table>";
            ed.execCommand('mceInsertContent', false, tableMarkup, {skip_undo: true});
            cellsEmpty && ed.selection.select($(ed.selection.getRng(true).commonAncestorContainer).parents('table').find('td')[0].childNodes[0]);
        }, true));

        var urlHandler = createHandler(/\b(((https?|ftp):\/\/|(www\.))[\w\.\$\-_\+!\*'\(\),/\?:@=&%#~;\[\]]+)$/, function (matchGroups) {
            var url = matchGroups[1].indexOf("http://") > -1 ? matchGroups[1] : "http://" + matchGroups[1],
                domNode = ed.dom.create('a', {href: url});
            domNode.appendChild(document.createTextNode(matchGroups[1]));
            ed.execCommand('mceInsertContent', false, encodeDomNode(domNode), {skip_undo: true});
            ed.getDoc().execCommand('unlink', false, {});
        }, false, 'a');
        handlerManager.registerHandler(" ".charCodeAt(0), urlHandler);
        handlerManager.registerHandler(13, urlHandler);

        var emailHandler = createHandler(/\b((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)$/i, function (matchGroups) {
            var domNode = ed.dom.create('a', {href: 'mailto:' + matchGroups[1]});
            domNode.appendChild(document.createTextNode(matchGroups[1]));
            ed.execCommand('mceInsertContent', false, encodeDomNode(domNode), {skip_undo: true});
            ed.getDoc().execCommand('unlink', false, {});
        }, false, 'a');
        handlerManager.registerHandler(" ".charCodeAt(0), emailHandler);
        handlerManager.registerHandler(13, emailHandler);

        var jiraTextHandler = createHandler(/(?:\b|^)(jira)$/, function () {
            ed.execCommand('mceInsertContent', false, "JIRA", {skip_undo: true});
        }, false);
        handlerManager.registerHandler(" ".charCodeAt(0), jiraTextHandler);
        handlerManager.registerHandler(13, jiraTextHandler);

        ed.onKeyPress.addToTop(function (ed, nativeEvent) {
            return handlerManager.executeHandlers(getCharCode(nativeEvent), ed, nativeEvent, jQueryEvent);
        });
    });
});

AJS.$(document).bind("initContextToolbars.Toolbar", function(e, editor) {
    var handlingNodeChange,
        getLang = function(key) {
            var title = editor.getLang(key),
                shortcut = editor.getLang(key + "_shortcut", "");
            if (shortcut) {
                title += " (" + shortcut + ")";
            }
            return title;
        },
        isChildOfTable = function(node) {
            var table = AJS.$(node).closest("table:not(.wysiwyg-macro)");
            return table.length;
        },
        hasColOrRowSpan = function(node) {
            //not the most efficent of solutions
            //but the table plugin is rather, well basically i am not touching it.
            function parseSpan(node, type) {
               return parseInt(node.attr(type),10);
            }
            return node.filter(function(index) {
                var temp = AJS.$(this);
                return parseSpan(temp, "rowspan") > 1 || parseSpan(temp, "colspan") > 1;
            }).length >= 1;
        },
        updateSplitButton = function(node) {

            var clickedCell = AJS.$(node).closest("td,th"),
                selectedCells = AJS.$(".mceSelected",clickedCell.closest("table"));

            if(hasColOrRowSpan(clickedCell.add(selectedCells))) {
                editor.plugins.customtoolbar.enableToolbarButton("table-split-cells");
            } else {
                editor.plugins.customtoolbar.disableToobarButton("table-split-cells");
            }
        },
        storageManager = Confluence.storageManager("tables");

    // Can't follow the event delegation model, as NodeChange events don't supply an
    // event parameter (only an element), so the type of the event cannot be determined.
    editor.onNodeChange.add(function(ed, cm, e) {
        // required, as show/removing of toolbar seems to fire another nodeChangeEvent
        // CONFDEV-3419: Parent node null check to prevent table toolbar from being removed when
        // callbacks occur with the document or body nodes (this may happen when multiple cells are selected).
        if ((!handlingNodeChange) && (e.parentNode != null) && (e.nodeName != "BODY")) {
            handlingNodeChange = true;
            if(isChildOfTable(e)) {
                AJS.$(document).trigger("createContextToolbarRow.Toolbar", {buttons: Confluence.Editor.tableToolbar.Buttons, topToolbar: true})
                               .trigger("enableContextToolbarRow.Toolbar");

               updateSplitButton(e);

            }
            else {
                AJS.$(document).trigger("disableContextToolbarRow.Toolbar");
            }
            handlingNodeChange = false;
        }
    });

    editor.onUndo.add(function(ed,cm,e) {
        //seems the bookmark gets restored after the node change from setcontent is triggered. Odd.
        ed.nodeChanged();
    });

    var classPrefix =  "table-";
    Confluence.Editor.tableToolbar = {
          Buttons:[{
                text: getLang("table.row_before_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableInsertRowBefore", false,"");
                },
                iconClass: classPrefix + "insert-row-before"
            }, {
                 text: getLang("table.row_after_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableInsertRowAfter", false,"");
                },
                iconClass: classPrefix + "insert-row-after"
            }, {
                text: getLang("table.delete_row_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableDeleteRow", false,"");
                },
                iconClass: classPrefix + "delete-row"
            }, null, {
                text: getLang("table.cut_row_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableCutRow", false,"");
                    editor.plugins.customtoolbar.enableToolbarButton("table-paste");
                },
                iconClass: classPrefix + "cut"
            }, {
                text: getLang("table.copy_row_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableCopyRow", false,"");
                    editor.plugins.customtoolbar.enableToolbarButton("table-paste");
                },
                iconClass: classPrefix + "copy"
            }, {
                text: getLang("table.paste_row_before_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTablePasteRowBefore", false,"");
                },
                disabled: !storageManager.doesContain("copied"),
                iconClass: classPrefix + "paste"
            }, null, {
                text: getLang("table.col_before_desc"),
                 click: function() {
                    tinyMCE.execCommand("mceTableInsertColBefore", false,"");
                },
                iconClass: classPrefix + "insert-column-before"
            }, {
                text: getLang("table.col_after_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableInsertColAfter", false,"");
                },
                iconClass: classPrefix + "insert-column-after"
            },{
                iconClass: classPrefix +"delete-column",
                text: getLang("table.delete_col_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableDeleteCol", false,"");
                }
            }, null, {
                iconClass: classPrefix +"merge-cells",
                text: getLang("table.merge_cells_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableMergeCells", false,"");
                }
            },{
                iconClass: classPrefix +"split-cells",
                text: getLang("table.split_cells_desc"),
                click: function() {
                    tinyMCE.execCommand("mceTableSplitCells", false,"");
                }
            }, null, {
                text: getLang("table.row_highlight"),
                click: function(buttonLink,element) {
                  tinyMCE.execCommand("confTableRowToggleHighlight", false, buttonLink);
                },
                iconClass: classPrefix + "highlight-row"
            }, {
                text: getLang("table.col_highlight"),
                click: function(buttonLink,element) {
                  tinyMCE.execCommand("confTableColumnHighlight", false, buttonLink);
                },
                iconClass: classPrefix + "highlight-column"
            }, {
                text: getLang("table.cell_highlight"),
                click: function(buttonLink,element) {
                  tinyMCE.execCommand("confTableCellToggleHighlight", false, buttonLink);
                },
                iconClass: classPrefix + "highlight-cell"
            }, null, {
                iconClass: classPrefix + "delete-table",
                text: getLang("table.del"),
                click: function() {
                    tinyMCE.execCommand("mceTableDelete", false,"");
                }
            }
          ],

          // takes an array of {type: tinymceEvent, shouldTrigger: function, callback: function, missed: function }
          Events: [
          ]
    };
});

(function ($) {
    function sourceMode() {
        var sourceToggleButton = $("#rte-button-source-mode");

        sourceToggleButton.click(function () {
            if (sourceToggleButton.hasClass("active")) {
                Confluence.Editor.changeMode(Confluence.Editor.MODE_RICHTEXT);
            } else {
                Confluence.Editor.changeMode(Confluence.Editor.MODE_SOURCE);
            }
        });
    }

    function wireToolbarContext(delegator,editor) {
        //not happy about this. Will be finding a better way to wind everything up.
        //dont want the dependence on the tabletoolbar want to query for that some how.
        //the levels of async call backs and editor loading here made it hard.
        $(document).trigger("initContextToolbars.Toolbar", editor);
        $(document).bind("createContextToolbarRow.Toolbar", createContextToolbar)
                   .bind('removeContextToolbarRow.Toolbar', removeContextToolbar)
                   .bind('enableContextToolbarRow.Toolbar', enableContextToolbar)
                   .bind('disableContextToolbarRow.Toolbar', disableContextToolbar);
        delegator.addEventsForComponent("TableToolbar", Confluence.Editor.tableToolbar.Events);
    }

    /**
     *  Adds bindings to close the dropdown menu when typed or clicked in the RTE
     */
    function bindCloseDropDown(editor, dd) {
        var closeMenu = function() {
                dd && dd.hide();
            };

        $(document).bind("showLayer", function(e, type, data){
            if (type=="dropdown" && data == dd) {
                editor.onClick.add(closeMenu);
                editor.onKeyUp.add(closeMenu);
            }
        }).bind("hideLayer", function(e, type, data) {
            if (type=="dropdown" && data == dd) {
                editor.onClick.remove(closeMenu);
                editor.onKeyUp.remove(closeMenu);
            }
        });
    }

    //CONFDEV-1921 Formatting should not be allowed inside plain text macro bodies
    function disableDropDown(dropdown) {
        dropdown.addCallback('show', function() { 
            if(dropdown.$.parents('.disabled').length) {
                dropdown.hide();
            }
        });
    }

    /**
     * Binds to the color controls and displays picker when needed
     * @param editor
     */
    function colorPicker(editor) {
        var control = $('#color-picker-control'),
            menu = control.find('.aui-dd-parent'),
            changeColorButton = $('#rte-button-color'),
            changeColorIcon = changeColorButton.find('span.icon'),
            dropdown = menu.dropDown('Standard', { alignment: 'left' })[0],
            defaultColor = changeColorButton.attr('data-color');

        changeColorIcon.css('background-color', '#' + defaultColor);
        control.delegate('a[data-color]', 'click', function(e) {
            var $this = $(this),
                color = $this.attr('data-color');

            e.preventDefault();
            if ( $this.closest('.disabled').length ) { //if this part of the toolbar is disabled
                return;
            }

            tinyMCE.execCommand('ForeColor',false,'#' + color);
            changeColorButton.attr('data-color', color);
            changeColorIcon.css('background-color','#' + color);
        });

        disableDropDown(dropdown); 
        bindResizeDropDownOnShow(menu, dropdown, 160);
        bindCloseDropDown(editor, dropdown);
    }

    /**
     * Binds to the insert table control and displays the pciker when needed
     * @param editor
     */
    function tablePicker(editor) {
        AJS.Rte.TablePicker.bindToControl(editor, $('#insert-table-dropdown'));
    }

	/**
	 * Resizes the drop down for IE7, as it does not size the drop down correctly.
	 */
    function bindResizeDropDownOnShow(menu, dd, widthOverride) {
        var done = false;
        
        if ($.browser.msie && parseInt($.browser.version, 10) == 7) { // IE7 only
            dd.addCallback('show', function() {
                if(!done) { // only needs to be resized the first time.
                    var maxWidth = 0,
                        ddElement = $(menu).find('.aui-dropdown');
                    $(ddElement).width('auto');
                    if(widthOverride) {
                        $(ddElement).width(widthOverride);
                    } else {
                        $(ddElement).find(".item-link").each(function (i, items) {
                            maxWidth = Math.max(maxWidth, $(this).outerWidth(true));
                        });
                        $(ddElement).width(maxWidth + 20); /* Magic number for IE7 */
                    }
                    done = true;
                    // Force another show so that the shadow is resized.
                    // Only necessary as this callback is registered after the shadow resize callback in dropdown.js
                    dd.show();
                }
            });
        }
    }

    function executeToolbarItem(editor, item, event) {
        var macroName = item.attr("data-macro-name"),
            command = item.attr("data-command"),
            format = item.attr("data-format"),
            controlId = item.attr("data-control-id");

        if (macroName) {
            tinymce.confluence.macrobrowser.macroBrowserToolbarButtonClicked({presetMacroName: macroName});
        }
        if (command) {
            editor.execCommand(command, false);
        }
        if (format) {
            editor.execCommand("FormatBlock", false, format);
        }
        if(controlId) {
            var control = editor.controlManager.get(controlId);
            if (control) {
                var s = control.settings;
                s.onclick.call(s.scope, event);
            }
            else {
                AJS.log("ERROR: no tinymce control found for " + controlId);
            }
        }
    }

    function enableContextToolbar() {
        $(".toolbar-contextual").removeClass("disabled");
    }

    function disableContextToolbar() {
        $(".toolbar-contextual").addClass("disabled");
    }

    //could be extended to disable a command as well
    function disableToolbarButton(buttonClass) {
        toggleButton($("." + buttonClass.replace(".","")),true);
    }

    function enableToolbarButton(buttonClass) {
        toggleButton($("." + buttonClass.replace(".","")),false);
    }

    function toggleButton(button,state) {
        button.closest("a").add(button.closest("li")).toggleClass("disabled",state)
    }

    //added a timeouts to ensure that if we change between showing and hiding it will not flicker and slide in and out.
    //better ux
    var toolbarTimeout;

    function removeContextToolbar() {
        clearTimeout(toolbarTimeout);
        toolbarTimeout = setTimeout(function() {
            $(".toolbar-contextual").slideUp(400, function() {
                $(this).remove();
                $(document).trigger("resize.resizeplugin");
            });
        }, 100);
    }


    function createContextToolbar(e, options) {
        //for now we dont want to create more than 1
        if($(".toolbar-contextual").length) {
            return;
        }

        var row = $("<div></div>").addClass("toolbar-split toolbar-split-row toolbar-contextual"),
            span,
            a,
            group = $("<ul></ul>").addClass("toolbar-group");
        row.append(group);
        var toolbar = !options.topToolbar ? $("#savebar-container .aui-toolbar")  : $("#rte-toolbar.aui-toolbar");
        for(var i = 0, length = options.buttons.length; i < length; i++) {
            if(!options.buttons[i]) {
                group = $("<ul></ul>").addClass("toolbar-group");
                row.append(group);
                continue;
            }
            var li = $("<li></li>").addClass("toolbar-item");

            a = $("<a href='#'></a>");
            a.addClass("toolbar-trigger");
            a.attr({"title" : options.buttons[i].text });
            a.click(getClickToolbarItemFunction(options.buttons[i]));
            if(options.buttons[i].disabled) {
                li.addClass("disabled");
                a.addClass("disabled");
            }

            if(options.buttons[i].iconClass) {
                span = $("<span />");
                span.addClass("icon " + options.buttons[i].iconClass);
                span.text(options.buttons[i].text);
                a.append(span);
            }
            li.append(a);
            group.append(li);
        }
        row.css({"display": "none"});
        toolbar.append(row);

        clearTimeout(toolbarTimeout);
        toolbarTimeout = setTimeout(function() {
            row.slideDown(400, function() {
              $(document).trigger("resize.resizeplugin").trigger("shown.contextToolbar");
            });
        }, 100);
    }

    function getClickToolbarItemFunction(button) {
        return function(e) {
            button.click();
            e.preventDefault();
        };
    }

    function bindDropdownMenu(editor, id) {
        var menu = $(id),
            dropdown = menu.dropDown("Standard", { alignment: "left" })[0];

        menu.find(".dropdown-item").click(function(e) {
            var item = $(this);
            executeToolbarItem(editor, item, e);
            dropdown.hide();
            e.preventDefault();
        });

        disableDropDown(dropdown);
        bindResizeDropDownOnShow(menu, dropdown);
        bindCloseDropDown(editor, dropdown);
    }

    function keyboardShortcutsHelp() {
        if (Confluence.KeyboardShortcuts) {
            $("#rte-button-help").click(Confluence.KeyboardShortcuts.openDialog);
        }
    }

    function editorHints(ed) {
        var hints = [];
        for (var hint in TinyMCELang.hints) {
            hints.push("hints." + hint);
        }
        var hintManager = Confluence.hintManager(hints);
        var updateHint = function () {
            $("#rte-savebar .hints-text").html(ed.getLang(hintManager.getNextHint()));
        };
        updateHint();
        $("#rte-savebar a.hints-close").click(function (e) {
            $(this).closest(".toolbar-split").addClass("hidden");
            e.preventDefault();
        });
        $("#rte-savebar a.hints-next").click(function (e) {
            updateHint();
            e.preventDefault();
        });
    }

    tinymce.create('tinymce.plugins.customtoolbar', {
        init: function (ed) {
            bindDropdownMenu(ed, "#format-dropdown");
            bindDropdownMenu(ed, "#more-menu .aui-dd-parent");
            bindDropdownMenu(ed, "#insert-menu .aui-dd-parent");

            colorPicker(ed);
            tablePicker(ed);
            sourceMode();
            keyboardShortcutsHelp();
            editorHints(ed);

            // todo - move this into the new link browser plugin once it has been enabled
            $("#rte-button-attachments").click(function(e) {
                tinyMCE.activeEditor.execCommand("mceConfAttachments", false);
            });

            wireToolbarContext(AJS.Rte.EventDelegator(ed),ed);

            var formatDropDown = $("#format-dropdown"),
                dropDownTextHolder = formatDropDown.find("span.dropdown-text");

            ed.onBeforeExecCommand.add(function(ed, cmd, ui, val, context) {
                // CONFDEV-3535: Reset to left alignment if in style that does not support alignment.
                $.each(['pre','blockquote'], function() {
                    if (cmd == "FormatBlock" && val == this) {
                        ed.formatter.remove('alignleft');
                        ed.formatter.remove('aligncenter');
                        ed.formatter.remove('alignright');
                    }
                });

                // CONFDEV-767/CONFDEV-1408: Prevent styles, links and macros from being used within a pre block.
                if ($(ed.selection.getNode()).closest("pre").length) {
                    var toDisable = ["Bold", "Italic", "Underline", "Strikethrough", "InsertUnorderedList",
                        "InsertOrderedList", "superscript", "subscript", "mceConfMacroBrowser", "mceConfimage",
                        "mceConfAttachments", "mceEmotion", "InsertWikiMarkup", "mceConflink", "mceInsertTable",
                        "mceConfAutocompleteLink"];
                    var toDisableLen = toDisable.length;

                    for (var i = 0; i < toDisableLen; i++) {
                        if (cmd == toDisable[i]) {
                            context.terminate = true;
                        }
                    }
                }

                // Remove existing formatting before applying a pre style.
                if ((cmd == "FormatBlock") && (val == "pre")) {
                    ed.undoManager.add();
                    ed.execCommand("removeFormat");
                }

                // Block
                // if going from blockquote to paragraph format, we need to execute the
                // blockquote command again to remove formatting - that's just how tinymce works
                if (cmd == "FormatBlock" && val == "p") {
                    var blockquote = formatDropDown.find(".dropdown-item[data-format='blockquote']").text();
                    if (dropDownTextHolder.text() == blockquote) {
                        context.terminate = true;
                        ed.execCommand("FormatBlock", false, "blockquote");
                    }
                }
                // only remove format if text is selected
                else if (cmd == "FormatBlock" && val == "removeformat" && ed.selection.isCollapsed()) {
                    context.terminate = true;
                    AJS.log("Not removing format for no selected text");
                }
            });

            // Update format dropdown display text
            ed.onExecCommand.add(function(ed, cmd, ui, val, context) {
                if (cmd == "FormatBlock") {
                    var format = formatDropDown.find(".dropdown-item[data-format='" + val + "']");
                    if (!format.length) { // default to paragraph format
                        format = formatDropDown.find(".dropdown-item[data-format='p']");
                    }
                    dropDownTextHolder.text(format.text());
                }
            });

            ed.onNodeChange.add(function(ed, cm, e) {
                // Text alignment not possible in certain styles.
                var format = $(e).closest('PRE,BLOCKQUOTE').length;
                cm.setDisabled('justifyleft', !!format);
                cm.setDisabled('justifycenter', !!format);
                cm.setDisabled('justifyright', !!format);

                // CONFDEV-767/CONFDEV-1408 Disables styling buttons when in a pre block.
                if ($(e).closest("PRE").length) {
                    var toDisable = ['bold', 'italic', 'underline', 'bullist', 'numlist', 'indent', 'outdent', 'linkbrowserButton'];
                    var toDisableLen = toDisable.length;
                    for (var i = 0; i < toDisableLen; i++) {
                        cm.setDisabled(toDisable[i], true);
                    }

                    AJS.Rte.TablePicker.disable();
                    $("#insert-menu, #more-menu").addClass("disabled");
                    $("#insert-menu, #color-picker-control").addClass("disabled");
                    
                    if($(e).parents('table[data-macro-body-type=PLAIN_TEXT]').length) { 
                        $('#format-dropdown').addClass('disabled');
                    }
                    // Adds a disabled class to the toolbar-triggers so that button looks disabled on :hover and :active
                    $("#toolbar").find("li.disabled").find(".toolbar-trigger:first").addClass("disabled");
                } else {
                    $('#format-dropdown, #insert-menu, #more-menu').removeClass('disabled');
                    $('#format-dropdown, #insert-menu, #color-picker-control').removeClass('disabled');
                    AJS.Rte.TablePicker.enable();
                    $("#toolbar").find("li:not(.disabled)").find(".toolbar-trigger:first").removeClass("disabled");
                }
            });

            ed.onInit.add(function(ed) {
                $(".aui-toolbar .toolbar-trigger[data-control-id]").each(function(i, trigger) {
                    var trigger = $(trigger),
                        toolbarItem = trigger.closest(".toolbar-item"),
                        id = trigger.attr("data-control-id"),
                        control = ed.controlManager.get(id);
                    if (control) {
                        // bindings for format drop down
                        if (id == "formatselect") {
                            // override control select methods to update our toolbar's format display
                            control.selectByIndex = function (idx) {
                                if (idx < 0) idx = 0;
                                dropDownTextHolder.text(formatDropDown.find(".dropdown-item:eq(" + idx + ")").text());
                            };
                            control.select = function(name) {
                                if (name == "p") {
                                    var n = ed.selection.getNode();
                                    // fix bug in advanced theme where it always shows paragraph format when in a blockquote
                                    if($(n).closest("p").parent().is("blockquote")) {
                                        name = "blockquote";
                                    }
                                }
                                var text = formatDropDown.find(".dropdown-item[data-format='" + name + "']").text();
                                if (text) {
                                    dropDownTextHolder.text(text);
                                }
                            };
                            control.selectByIndex(0);
                        }
                        // bindings for the other control buttons
                        else {
                            //Controls are already created by the Advanced Theme by this point. The point of this
                            // is to simply wrap the existing controls to update our custom toolbar state.
                            $.aop.before({target: control, method: "setActive"}, function(args){
                                toolbarItem.toggleClass("active", !!args[0]);
                            });
                            $.aop.before({target: control, method: "setDisabled"}, function(args){
                                toolbarItem.toggleClass("disabled", !!args[0]);
                            });
                            var s = control.settings;
                            toolbarItem.click(function (e) {
                                if (!$(this).hasClass('disabled')) {
                                    s.onclick.call(s.scope, e);
                                    e.preventDefault();
                                }
                            });
                        }
                    }
                });
                $(".aui-toolbar .more-menu-trigger[data-control-id]").each(function(i, trigger) {
                    var trigger = $(trigger),
                        id = trigger.attr("data-control-id"),
                        control = ed.controlManager.get(id),
                        icon = trigger.find(".icon-check");

                    if (control) {
                        $.aop.before({target: control, method: "setActive"}, function(args){
                            icon.toggleClass("hidden", !args[0]);
                        });
                        $.aop.before({target: control, method: "setDisabled"}, function(args){
                            icon.toggleClass("hidden", !args[0]);
                        });
                    }
                });
            });
        },

        disableToobarButton: disableToolbarButton,
        enableToolbarButton: enableToolbarButton,

        getInfo: function () {
            return {
                longname: 'customtoolbar',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com/',
                version: "1.0"
            };
        }
    });

    // Register plugin
    //noinspection JSUnresolvedVariable
    tinymce.PluginManager.add('customtoolbar', tinymce.plugins.customtoolbar);

})(AJS.$);

/*
 This plugin sanitises content when it is pasted into the editor.
*/
(function($) {
    tinymce.create('tinymce.plugins.ConfluencePastePlugin', {
        init: function(ed, url) {
            ed.onInit.add(function() {

                function filterRelativePath(element, attribute) {
                    var items = $(element).find('[' + attribute + ']'),
                        item = '',
                        attr = '', 
                        mceAttr = '';
                    
                    for(var i=0, ii=items.length; i<ii; i++) {
                        item = $(items[i]);
                        attr = item && ['/', item.attr(attribute)].join('');
                        mceAttr = item && ['/', item.attr('data-mce-' + attribute)].join('');
                
                        if(ed.settings.context_path && attr.indexOf(ed.settings.context_path) === 0) {
                            items[i] = item.attr(attribute, attr);
                        }

                        if(ed.settings.context_path && mceAttr.indexOf(ed.settings.context_path) === 0) {
                            items[i] = item.attr('data-mce-' + attribute, mceAttr);
                        }
                    }
                
                    return items;
                }

                function createCssFromWhiteList(item, whiteList) {
                    var newCss = [],
                        regex = '',
                        matchedValue,
                        styleAttr = item.attr('style'),
                        mceStyleAttr = item.attr('data-mce-style');

                    for(var i=0, ii=whiteList.length; i < ii; i++) {
                        if(styleAttr && styleAttr.toLowerCase().indexOf(whiteList[i]) > -1) {
                            newCss.push(whiteList[i] + ': ' + item.css(whiteList[i]) + ';');
                        } else if(mceStyleAttr && mceStyleAttr.toLowerCase().indexOf(whiteList[i]) > -1) {
                            regex = new RegExp(whiteList[i] + ':.+?(?:;|$)');
                            matchedValue = mceStyleAttr.match(regex);
                            newCss.push(matchedValue[0].indexOf(';') > -1 ? matchedValue : matchedValue + ';');
                        }
                    }
                    return newCss.join(' ');

                }

                function createFromWhiteList(item, attribute, whiteList) {
                    var newList = [];
                    
                    if(whiteList) {
                        for(var i=0, ii=whiteList.length; i < ii; i++) {
                            if(item.is('[' + attribute + '~=' + whiteList[i] + ']')) {
                                newList.push(whiteList[i]);
                            }
                        }
                    }
                    return newList.join(' ');
                }

                function filterAttribute(element, attribute, whiteList) {
                    var items = $(element).find('[' + attribute + ']'),
                        list = '',
                        item,
                        whiteListForItem;

                    if(items) {
                        for(var i=0, ii=items.length; i<ii; i++) {
                            item = $(items[i]);

                            whiteListForItem = whiteList.standard;
                            for (var selector in whiteList) {
                                if(whiteList.hasOwnProperty(selector) && item.is(selector)) {
                                    whiteListForItem = whiteList[selector];
                                }
                            }

                            if(attribute.indexOf('style') > -1) {
                                list = createCssFromWhiteList(item, whiteListForItem);
                            } else {
                                list = createFromWhiteList(item, attribute, whiteListForItem);
                            }

                            item.removeAttr(attribute);
                            if(list.length) {
                               item.attr(attribute, list);
                            }
                        }
                    }
                }

                function filterAttributes(element, attributes) {
                    var cssWhiteList = {};

                    cssWhiteList.standard = ['text-decoration', 'text-align', 'margin-left'];
                    cssWhiteList['.wysiwyg-macro'] = ['background-image'];
                    cssWhiteList['p'] = ['margin-left', 'text-align'];
                    cssWhiteList['span'] = ['color', 'text-decoration'];
                    cssWhiteList['pre'] = ['margin-left'];

                    if(attributes.indexOf('style') > -1) {
                        filterAttribute(element, 'style', cssWhiteList);
                        filterAttribute(element, 'data-mce-style', cssWhiteList);
                    }
                    
                    //Confusion with pathnames when pasting between pages within
                    //the same domain. Add / to relative path in order to make absolute.
                    //Gecko browsers only
                    if(tinymce.isGecko) {
                        if(attributes.indexOf('src') > -1) {
                            filterRelativePath(element, 'src');
                        }

                        if(attributes.indexOf('href') > -1) {
                            filterRelativePath(element, 'href');
                        }
                    }
                    
                    filterAttribute(element, 'face', { standard: []});
                    filterAttribute(element, 'id', { standard: []});
                    return element;
                }

                function removeNode(node, selector) {
                    $(selector, node).contents().unwrap();
                }

                //Remove <br> tags from plain text and replace with paragraphs
                function processPlainText(element) {
                    var node, newHtml;
                    node = $(element);
                    newHtml = "<p>" + node.html().replace(/<br>/gi, "</p><p>") + "</p>";
                    node.html(newHtml);
                }

                //If the only tags are <br> tags then we assume that we are pasting plain text
                function isPlainText(element) {
                    var node = $(element);
                    return node.children("br").length && !node.find(":not(br)").length;
                }

                //Search for things that should always be removed but keep the content that these
                //elements may contain
                function filterNodes(node) {
                    var toRemove = [".panelContent", ".panel", ".panelHeader", ".Apple-converted-space", "font", ".diff-html-removed", ".diff-html-added"];
                    $.each(toRemove, function(i, selector){
                        removeNode(node, selector);
                    });

                    //CONFDEV-4487 Pasting empty <img> tag breaks the editor
                    $('img', node).map(function() {
                        !$(this).attr('src') && $(this).remove();
                    });
                }

                function makeLinkProcess(regex, node, isEmail) {
                    var prefix,
                        newNode,
                        match = regex.exec(node.data);

                    if (match) {
                        if(isEmail) {
                            prefix = 'mailto:';
                        } else {
                            prefix = match && match[0].indexOf('://') == -1 ? 'http://' : '';
                        }

                        if (match.index) {
                            node = node.splitText(match.index);
                        }

                        newNode = node.splitText(match[0].length);
                        $(node).wrap("<a href='" + prefix + node.data + "'></a>");
                        return newNode;
                    }
                
                    return '';       
                }

                function makeLinks(node) {
                    var urlRegEx = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/,
                        emailRegEx = /((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\b/,
                        nodesToTest = $.makeArray(node.childNodes),
                        currentNode;

                    while (nodesToTest.length) {
                        currentNode = nodesToTest.pop();
                        if (!$(currentNode).is("a")) {
                            if (currentNode.nodeType == 3) {
                                nodesToTest = nodesToTest.concat(makeLinkProcess(urlRegEx, currentNode, false)) || nodesToTest;
                                nodesToTest = nodesToTest.concat(makeLinkProcess(emailRegEx, currentNode, true)) || nodesToTest;
                            } else if (currentNode.nodeType == 1 && currentNode.childNodes && currentNode.nodeName.toLowerCase() !== "pre") {
                                nodesToTest = nodesToTest.concat($.makeArray(currentNode.childNodes));
                            }
                        }
                    }
                }

                AJS.$(document).bind('postPaste', function(e, pl, o) {
                    filterAttributes(o.node, 'style,src,href');
                    filterNodes(o.node);

                    if (!tinymce.isIE) { //IE does this for us automatically
                        makeLinks(o.node);
                    }

                    //If we are in firefox and the only tags in the paste are <br> tags then lets fix up the
                    //formatting so we are not pasting in one giant paragraph
                    if (tinymce.isGecko && isPlainText(o.node) ) {
                        processPlainText(o.node);
                    }
                });
            });
        },

        getInfo: function() {
            return {
                longname: 'ConfluencePastePlugin',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com',
                version: '1.0'
            };
        } 
    });

    tinymce.PluginManager.add('confluencepasteplugin', tinymce.plugins.ConfluencePastePlugin);
})(AJS.$);

/*
 This plugin alters the behaviour of lists that are pasted into the editor.
 CONFDEV-4087 Copy pasting bullets within a list erroneously indents the bullets

 NOTE: This is designed to work on simple lists only, A simple list is one that only contains text.
*/

(function($) {
    tinymce.create('tinymce.plugins.ConfluencePasteListPlugin', {
        init: function(ed) {
            
            //Verifies that the element is a list and just a list
            function isList(element) {
                var itterator = element[0] || element,
                    isList = true;

                while(itterator) {
                    if(itterator.tagName != 'LI') {
                        isList = false;
                        break;
                    }
                    itterator = itterator.nextSibling || false;
                }

                return isList;
            }
            
            //Make all list content the same. Essentially we're pairing back to just the list items
            function normalizeClipboardContent(content, browser) {
                var element = '';

                if(browser == 'WebKit') {
                    if(content && content.childNodes.length > 1 && content.firstChild.tagName.toUpperCase() == 'META') {
                        content.removeChild(content.firstChild);
                    }
                } 

                if(browser == 'WebKit' || browser== 'Gecko') {
                    if(content && content.childNodes.length == 1 && content.firstChild.tagName && 
                    (content.firstChild.tagName.toUpperCase() == 'UL' || content.firstChild.tagName.toUpperCase() == 'OL')) {
                        element = content.ownerDocument.createElement('div');
                        element.innerHTML = content.firstChild.innerHTML;
                        content = element;
                    }
                }
                
                return content;
            }
    
            //Merge the secondary list into the primary. The merge index is the position at which the secondary will be mergred.
            //If the split position is 0 then the secondary will be merged before, if it is the length of the content of the merge
            //index then it will be after. Otherwise the merge index will be split into two items with the secondary placed inbetween
            function mergeLists(primary, secondary, mergeIndex, splitPosition) {
                var mergedList,
                    textBefore,
                    textAfter,
                    listItem;

                if(splitPosition && splitPosition < primary[mergeIndex].innerHTML.length) {
                    textBefore = primary[mergeIndex].innerHTML.substring(0, splitPosition);
                    textAfter = primary[mergeIndex].innerHTML.substring(splitPosition);
                    primary[mergeIndex].innerHTML = textBefore;
                    mergeIndex++;
                } else if(splitPosition && splitPosition >= primary[mergeIndex].innerHTML.length) {
                    mergeIndex++; 
                }

                mergedList = primary.slice(0, mergeIndex);
                mergedList = mergedList.concat(secondary);

                if(textAfter) {
                    listItem = window.document.createElement('li');
                    listItem.innerHTML = textAfter;
                    mergedList[mergedList.length] = listItem;
                }
                
                mergedList = mergedList.concat(primary.slice(mergeIndex, primary.length));
                return mergedList;
            }

            //Find the index of the list item that contains the cursor
            function getPosition(referenceNode) {
                referenceNode.setAttribute('data-position-marker', 1);
                
                for(var i=0, ii=referenceNode.parentNode.childNodes.length; i<ii; i++) {
                    if(referenceNode.parentNode.childNodes[i].getAttribute('data-position-marker')) {
                        referenceNode.parentNode.childNodes[i].removeAttribute('data-position-marker');
                        return i;
                    }
                }
            }

            //Does this list item contain no data.
            function isEmptyListItem(element) {
                return (element.tagName == 'BR' && element.parentNode.siblings.length == 1)
            }
            
            //These are used for unit testing purposes
            $.extend(this, {
                _isEmptyList: isEmptyListItem,
                _mergeLists: mergeLists,
                _normalizeClipboardContent: normalizeClipboardContent,
                _isList: isList,
                _getPosition: getPosition
            });

            ed.onInit.add(function() {
                $(document).bind('postPaste', function(e, pl, o) {
                    var content = o.node.cloneNode(true),
                        range = tinymce.activeEditor.selection.getRng(true),
                        referenceNode = tinymce.activeEditor.selection.getStart(),
                        rng = tinymce.activeEditor.dom.createRng(),
                        context = referenceNode.parentNode,
                        browser,
                        mergedList,
                        beforeSelection = referenceNode.previousSibling || referenceNode.parentNode;
                    
                    if(!isList(referenceNode)) {
                        return;
                    }
                    
                    browser = tinymce.isWebKit ? 'WebKit' : tinymce.isGecko ? 'Gecko' : tinymce.isIE ? 'IE' : '';
                    content = normalizeClipboardContent(content, browser);

                    //This handles the you've just hit enter and are pasting into an empty listItem
                    //situation.
                    if(isEmptyListItem(referenceNode)) {
                        referenceNode = referenceNode.parentNode;
                        context = context.parentNode;
                    }
                  
                     
                    //Merge lists.
                    if(isList(content.childNodes)) {
                        if(!tinymce.activeEditor.selection.isCollapsed()) {
                            tinymce.activeEditor.selection.setContent('');
                            rng.setStartBefore(beforeSelection);
                            rng.setEndAfter(beforeSelection);
                        }

                        mergedList = mergeLists($(context.childNodes).toArray(), $(content.childNodes).toArray(), getPosition(referenceNode), range.startOffset); 
                        
                        //Update content with the merged list 
                        for(var i=0, ii=mergedList.length; i<ii; i++) {
                            if(content.childNodes[i]) {
                                content.replaceChild(mergedList[i], content.childNodes[i]);
                            } else {
                                content.appendChild(mergedList[i]);
                            }
                        }                       
                        
                        o.node = content; 

                        //Set the range to be the existing list. This will be overwritten
                        //with the new list when the content is serialized.                        
                        rng.setStartBefore(context);
                        rng.setEndAfter(context);
                        tinymce.activeEditor.selection.setRng(rng);
                    }
                });
            });
        },

        getInfo: function() {
            return {
                longname: 'ConfluencePasteListPlugin',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com',
                version: '1.0'
            };
        } 
    });

    tinymce.PluginManager.add('confluencepastelistplugin', tinymce.plugins.ConfluencePasteListPlugin);
})(AJS.$);

/*
 This plugin sanitises content when it is pasted into the editor.
 CONFDEV-2219 Cannot highlight a table row if the table was pasted from another source.
 
 The bugger of this is that we have to iterate though the whole paste, might need to do
 some performance enhancements...
*/
(function($) {
    tinymce.create('tinymce.plugins.ConfluencePasteTablePlugin', {
        init: function(ed) {
            ed.onInit.add(function() {
                
                /* This is a table paste plugin so we only want to format table elements.
                   If the element is not a table element then we don't want to do anything with it */
                function isTable(element) {
                    return !element.tagName ? false :
                    element.tagName == 'TABLE' ? true :
                    element.tagName == 'TR' ? true : 
                    element.tagName == 'TH' ? true : 
                    element.tagName == 'TD' ? true :
                    element.tagName == 'TBODY' ? true :
                    element.tagName == 'THEAD' ? true :
                    element.tagName == 'TFOOT' ? true : 
                    element.tagName == 'COL' ? true :
                    element.tagName == 'COLGROUP' ? true :
                    element.tagName == 'CAPTION';
                }
                
                /* Confluence tables have an annoying class structure which we will need
                   to replicate with any table being pasted into the editor */
                function getTableClass(element) {
                    return !element.tagName ? '' :
                    element.tagName == 'TABLE' ? 'confluenceTable' :
                    element.tagName == 'TR' ? 'confluenceTr' : 
                    element.tagName == 'TH' ? 'confluenceTh' : 
                    element.tagName == 'TD' ? 'confluenceTd' : '';
                }

                function isMacroPlaceHolder(element) {
                    var className = '';

                    if(element && element.getAttribute)  {
                        className = element.getAttribute('class');
                    }

                    return !element.tagName ? false :
                    element.tagName != 'TABLE' ? false :
                    !className ? false : className.indexOf('wysiwyg-macro') !== -1;
                }
                
                // Is this a table wrapping div which we generating when rendering tables (see ViewTableWrappingFragmentTransformer)
                // This wrapping div should not exist in the editor so remove it.
                function isTableWrap(element) {
                    var className;
                    if (element && element.getAttribute) {
                        className = element.getAttribute('class');
                    }
                    
                    return className && className.indexOf('table-wrap') >= 0;
                }

                /*
                   if it's a table then we'll remove anything nasty and add in
                   the appropriate classes to make it look like a confluence plugin.
                */ 
                function formatTable(element) {
                    var attributeWhiteList = {
                        'id':true, 
                        'class':true, 
                        'style':true,
                        'rowspan':true,
                        'colspan':true,
                        'data-mce-style':true, 
                        'data-mce-bogus':true, 
                        'data-macro-name':true,
                        'data-macro-parameters':true,
                        'data-macro-body-type':true
                        },
                        attribute,
                        i,
                        className;

                    //Not an element or a macro-placeholder? do nothing.
                    if(!element || isMacroPlaceHolder(element)) {
                         return element;
                    }
                    
                    if (isTableWrap(element)) {
                        var tableElement = $("table", element);
                        if (tableElement.length) {
                            $(tableElement).unwrap();
                            element = tableElement[0];
                        } else {
                            return null;
                        }
                    }
                    
                    //Remove attributes not in the white list
                    if(isTable(element) && element.attributes) {
                        i=0;
                        while(i <= element.attributes.length) { 
                            attribute = element.attributes[i];
                            if (attribute && attribute.specified === true && !attributeWhiteList[attribute.name]) {
                                element.removeAttribute(attribute.name);
                            } else {
                                i++;
                            }
                        }
                        
                        //overwrite existing classes, this is a conscious decision to prevent users
                        //from getting styling into their tables that they can't create with the editor
                        className = getTableClass(element);
                        element.setAttribute('class', className);
                    }
                    return element;
                }
                
                //is the first element a parent of the second element.
                function isParent(firstElement, secondElement) {
                    return secondElement.parentNode === firstElement;
                }

                /* We're not using recursion here to avoid 'Too much recursion errors'
                   this iterative approach should be faster as well.*/
                function parseHtml(element, process) {
                    var current = element,
                        previous = '';

                    while(true) {
                        if(!current) {
                            break;
                        }

                        if(typeof process == 'function') {
                            current = process(current);
                        }

                        // if process has removed a node which had no child nodes then current will now be null
                        if (!current) {
                            break;
                        } else if(current.firstChild && !isParent(current, previous) && !isMacroPlaceHolder(current)) {
                            previous = current;
                            current = current.firstChild;
                        } else if(current.nextSibling) {
                            previous = current;
                            current = current.nextSibling
                        } else {
                            previous = current;
                            current = current.parentNode;
                        }
                    }
                    
                    return element;
                }

                $(document).bind('postPaste', function(e, pl, o) {
                    o.node = parseHtml(o.node, formatTable);
                });
            });
        },

        getInfo: function() {
            return {
                longname: 'ConfluencePasteTablePlugin',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com',
                version: '1.0'
            };
        } 
    });

    tinymce.PluginManager.add('confluencepastetableplugin', tinymce.plugins.ConfluencePasteTablePlugin);
})(AJS.$);

/*
 This plugin removes all the html for the clipboard leaving only the text. 
 CONFDEV-4233 Only text from the clipboard should be pasted into the body of plain text macro
 CONFDEV-4451 Code macro strips whitespace
 */
(function($) {
    tinymce.create('tinymce.plugins.ConfluencePasteMacroPlugin', {
        init: function(ed) {
            var that = this;
            ed.onInit.add(function() {
                $(document).bind('postPaste', function(e, pl, o) {
                    var pastingMacro = $(o.node).find('.wysiwyg-macro-body p, .wysiwyg-macro-body pre');

                    if ($(ed.selection.getStart()).closest("[data-macro-body-type='PLAIN_TEXT']").length) {
                        o.node = that.parseHtml(ed, o.node);
                    }
                });
            });
        },

        /* We're not using recursion here to avoid a stack overflow */
        parseHtml: function(ed, element) {
            var stack = $.makeArray(element.childNodes),
                current,
                insertElem,
                document = ed.getDoc(),
                content = document.createElement('div');

            while (stack.length) {
                current = stack.shift();

                if (current.childNodes.length) {
                    tinymce.html.Schema.blockElementsMap[current.nodeName] && stack.unshift(document.createElement('br'));
                    stack = $.makeArray(current.childNodes).concat(stack);
                }
                else {
                    insertElem = this.formatMacroContent(current, document);
                    insertElem && content.appendChild(insertElem);
                }
            }

            return content;
        },

        //Strip out html when pasting into a plain text macro
        formatMacroContent: function(element, document) {
            var content,
                textNode = 3,
                nonBreakingSpace = /\xA0/g;
            document = document || window.document;

            if (element.nodeType == textNode) {
                content = document.createTextNode($(element).text().replace(nonBreakingSpace, ' '));
            } else if (element.nodeName == 'BR') {
                content = document.createElement('br');
            }

            return content;
        },

        getInfo: function() {
            return {
                longname: 'ConfluencePasteMacroPlugin',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com',
                version: '1.0'
            };
        }
    });

    tinymce.PluginManager.add('confluencepastemacroplugin', tinymce.plugins.ConfluencePasteMacroPlugin);
})(AJS.$);

/**
 * A plugin which ensures that there is always somewhere for a user to place their cursor when editing.
 * A typical problem is when there are two macro placeholders following each other then a user is unable to 
 * insert the cursor between them.
 * 
 * (This functionality originally lived in tinymce-cursor.js but that isn't a TinyMCE plugin and therefore
 * doesn't bind to onSetContent early enough to do the necessary processing.)
 *
 * Copyright 2011, Atlassian
 */

(function($) {

	tinymce.create('tinymce.plugins.CursorTargetPlugin', {

	    /**
		 * Initialises the plugin - executed after the plugin has been created.
		 * This call is done before the editor instance has finished it's initialisation so use the onInit event
		 * of the editor instance to intercept that event.
		 *
		 * @param {tinymce.Editor} ed Editor instance that the plugin is initialised in.
		 * @param {string} url Absolute URL to where the plugin is located.
		 */
		init : function(ed, url) {

            /** The jQuery selector for the blocks we are interested in fixing. */ 
            var blocksToFixSelector = ".wysiwyg-macro, table";
            
            /**
             * Passing true to tinymce.Editor.selection.getRng() returns a W3C range (even in IE - returns tinyMCE's implementation of w3c range over ie's textRange).
             */
            var W3C_RANGE = true;

            var BLOCK_ELEMENT_SELECTOR = "ol, ul, p, pre, table, blockquote, div, h1, h2, h3, h4, h5, h6";

            /**
             * Go through the entire editor content and ensure that 'cursor target' paragraphs are added where needed.
             */
            function addCursorTargetParagraphsToContent(dom) {
                $(blocksToFixSelector, dom).each(function(index, element) {
                    addSurroundingCursorTargetParagraph(element);
                });
            }

            /**
             * CONFDEV-4041: Go through the entire editor content and ensure that empty paragraphs are correctly
             * formatted for the user's browser. Note that our storage format stores empty paragraphs
             * as <p>&nbsp;</p>, which becomes irritating to the user as there is an extra space.
             * CONFDEV-4285: Fix spaces in empty table cells as well.
             */
            function convertEmptyParagraphs(dom) {
                // IE8 does not support the Gecko/Webkit compatible <p><br/></p>
                // IE8 should be able to handle empty paragraphs like <p/> but they appear collapsed
                // Temporary workaround is to keep empty paragraphs in IE as <p>&nbsp;</p>
                var emptyParagraph = (tinyMCE.isIE) ? "&nbsp;" : "<br/>";

                function containsSingleNbsp(element) {
                    return element && element.childNodes && element.childNodes.length === 1 && element.childNodes[0].nodeType == 3 && element.childNodes[0].nodeValue && element.childNodes[0].nodeValue.length === 1 && element.childNodes[0].nodeValue.charCodeAt(0) == 160;
                }

                $("p, td.confluenceTd, th.confluenceTh", dom).each(function(index, element) {
                    if (containsSingleNbsp(element)) {
                        $(element).html(emptyParagraph);
                    }
                });
            }
            
            /**
             * For the supplied DOM element check if it requires surrounding cursor target paragraphs added.
             */
            function addSurroundingCursorTargetParagraph(element) {
                var $element = $(element);

                /**
                 * Empty paragraphs in FF, webkit and IE 9 can look like this <p><br/></p>.
                 * Empty paragraphs in IE 8 must look like this: <p>&nbsp;</p> (without the NBSP, IE 8 won't render the paragraph.
                 *
                 * Implementing empty paragraphs with the BR will avoid the use of an NBSP which takes up space
                 * and needs to be deleted manually (which may be bothersome to the user). The BR allows the cursor to be placed inside the paragraph.
                 */
                function padEmptyParagraph($paragraphElement) {
                    if ($paragraphElement.length === 0) {
                        throw new Error("no paragraph element specified");
                    }

                    var paragraphElement = $paragraphElement[0];
                    if (paragraphElement.childNodes.length === 0) {
                        if (tinymce.isIE && !tinymce.isIE9) {
                            paragraphElement.innerHTML = "&nbsp;";
                        } else {
                            paragraphElement.innerHTML = "<br />";
                        }
                    }
                    return paragraphElement;
                }

                function isInline(element) {
                    return !$(element).is(BLOCK_ELEMENT_SELECTOR);
                }

                function createCursorTargetFor(element, siblingMethod) {
                    var $cursorTarget = $("<p/>");

                    if ($(element.parentNode).is("li, td, th")) { /* CONFDEV-5521 */
                        while (element[siblingMethod] && isInline(element[siblingMethod])) {
                            $cursorTarget[siblingMethod == "previousSibling" ? "prepend" : "append"](element[siblingMethod]);
                        }
                    }

                    return padEmptyParagraph($cursorTarget);
                }

                if (!$element.prev().length
                        || $element.prev(blocksToFixSelector).length
                        || ($(element.parentNode).is("li, td, th") && isInline(element.previousSibling)) /* CONFDEV-5521 */) {
                    $element.before(createCursorTargetFor(element, "previousSibling"));
                }
                
                if (!$element.next().length
                        || $element.next(blocksToFixSelector).length
                        || ($(element.parentNode).is("li, td, th") && isInline(element.nextSibling)) /* CONFDEV-5521 */) {
                    $element.after(createCursorTargetFor(element, "nextSibling"));
                }
                
                /**
                 * Cut or keyboard deleted non-collapsed selections can leave unusual structures in the browser such as 
                 * a macro place holder with just an empty <br> before or after it. This function deals with the cases 
                 * we have encountered and converts these cases into a "proper" cursor target paragraph.
                 * 
                 * Other structures encountered are various forms of empty <p> or just multiple <br/> elements.
                 * 
                 * @param $blockEl the jQuery wrapped block element to ensure is properly surrounded with cursor-target spacers
                 */
                function checkForEmptiedParagraphs($blockEl) {
                    
                    /**
                     * If the supplied $structure is of the required type then convert it (in place in the DOM) to
                     * being a cursor target.
                     * 
                     * @param $structure
                     */
                    function convertCutStructureToCursorTarget($structure) {
                        // On Firefox you can be left with a <BR> after the cut of a "trailing" paragraph
                        if ($structure[0].nodeName == "BR" || isTextContainerEmpty($structure[0])) {
                            // replace with the correct 'empty' structure (it may already be the correct structure - no matter).
                            $structure.replaceWith(padEmptyParagraph($("<p/>")));
                        }
                    }
                    
                    /**
                     * "is An After Cursor Target" - should the supplied node be a 'cursor target' for after a block element.
                     * 
                     * If the node parameter has no next sibling OR the next sibling is a block then the node should be 
                     * considered a cursor target.
                     * 
                     * If the node has next siblings that are all BRs or Ps then it should also be considered a cursor target.
                     * This unusual case can arise from cut operations on non-collapsed selections in the browser.
                     * 
                     * @param $node a jQuery wrapped node
                     * @return true if this node should be considered a cursor target after a block
                     */                    
                    function isAnAfterCursorTarget($node) {
                        var $next = $node.next();
                        while ($next.length && !$next.is(blocksToFixSelector)) {
                            var next = $next[0];
                            if (next.nodeName != "BR" && next.nodeName != "P") {
                                return false;
                            }
                            
                            $next = $next.next();
                        }
                        
                        return true;                        
                    }

                    /**
                     * "is A Before Cursor Target" - should the supplied node be a 'cursor target' for before a block element.
                     * 
                     * If the node parameter has no previous sibling OR the previous sibling is a block then the node should be 
                     * considered a cursor target.
                     * 
                     * If the node has previous siblings that are all BRs or Ps then it should also be considered a cursor target.
                     * This unusual case can arise from cut operations on non-collapsed selections in the browser.
                     * 
                     * @param $node a jQuery wrapped node
                     * @return true if this node should be considered a cursor target before a block
                     */                           
                    function isABeforeCursorTarget($node) {
                        var $prev = $node.prev();
                        while ($prev.length && !$prev.is(blocksToFixSelector)) {
                            var prev = $prev[0];
                            if (prev.nodeName != "BR" && prev.nodeName != "P") {
                                return false;
                            }
                            
                            $prev = $prev.prev();
                        }
                        
                        return true;                        
                    }
                    
                    var $prev = $blockEl.prev();
                    if ($prev.length && isABeforeCursorTarget($prev)) {
                        convertCutStructureToCursorTarget($prev);
                    }
                    
                    var $next = $blockEl.next();
                    if ($next.length && isAnAfterCursorTarget($next)) {
                        convertCutStructureToCursorTarget($next);                        
                    }
                }
                
                checkForEmptiedParagraphs($element);
            }
            
            /**
             * Go through the entire editor content and ensure that any empty paragraphs which are single
             * spacers before or after block content are removed.
             * 
             * In addition any 'cursor targets' found that are no longer empty will have their marker class removed. 
             * 
             * @param dom the dom to be modified
             * @return the modified dom
             */
            function processCursorTargets(dom) {
                // now find all paragraphs and remove those that are empty and associated with a block
                // (i.e. are cursor targets only)
                // do this in 2 steps (mark then delete, otherwise you will end up removing all empty paragraphs on the page :))
                $("p", dom).each(function(index, element) {
                    if (isTextContainerEmpty(element) && isCursorTarget(element)) {
                        $(element).attr("data-tag", "marked-for-deletion");
                    }
                });
                $("p[data-tag=marked-for-deletion]", dom).remove();

                return dom;
            }
            
            /**
             * Check the various different forms of 'empty' that can arise in TinyMCE.
             * These 'forms of empty' have all been observed in testing. So don't just
             * go assuming I'm mental and remove checks from here :-)
             * 
             * Some cases that cause a "non-standard" empty paragraph -
             * 1) Removing a selection that ranges from part way into a place holder to the end of a document will result in 
             * an 'empty' paragraph which actually contains a &nbsp; (Chrome and Firefox)
             * 2) Removing a selection that ranges from the last paragraph on a page back to the end of a placeholder body will
             * result in <P>&nbsp;</p> (IE8 & 9 - this is non-standard for IE).
             * 3) The steps aren't clear, but on Chrome you can get a <p></p> which is non-standard for non IE browsers.
             * 4) There are others I haven't pinned down yet.
             * 
             * @param target the dom object to check.
             * @return true if empty.
             */
            function isTextContainerEmpty(target) {
                /**
                 * Handles:
                 * <p> </p>
                 * <p> <br/></p>
                 * <p><br/> </p>
                 * <p> ... with multiple text nodes that are empty with at least one containing a space ... </p>
                 * <p> ... with multiple text nodes that are empty ... </p>
                 */
                function _containsOptionalWhiteSpaceOrOptionalBr(container) {
                    var brCount = whiteSpaceCount = 0;

                    for (var i = 0, l = container.childNodes.length; i < l; i++) {
                        var childNode = container.childNodes[i];
                        if (childNode.nodeName == "BR") {
                            if (++brCount > 1) {
                                return false;
                            }
                        } else if (/^[\s|\u00A0]$/.test(childNode.nodeValue)) {
                            if (++whiteSpaceCount > 1) {
                                return false;
                            }
                        } else if (childNode.nodeValue !== "") {
                            return false;
                        }
                    }

                    return true;
                }

                return target.nodeType == 1 && target.childNodes.length == 0 || _containsOptionalWhiteSpaceOrOptionalBr(target);
            }

            /**
             * Checks if the current node is a cursor target.
             *
             * If the node is nested inside a cursor target, this function will traverse up to find
             * @param node the node to check
             * @return true if the node is a 'cursor target' node
             */
            function isCursorTarget(node) {
                if (!node || node.nodeType != 1) {
                    return false;
                }

                var prev = _getSiblingWithinBlockDisregardingNesting(node, false);
                var next = _getSiblingWithinBlockDisregardingNesting(node, true);
                
                return (prev == null || $(prev).is(blocksToFixSelector)) && (next == null || $(next).is(blocksToFixSelector)) && (!(prev == null && next == null));
            }                
            
            /**
             * Get the next/previous sibling to the one supplied within the current block but coping with nested structures.
             * 
             * For instance, in the case of next checking, if the current node was an <li> within a <ul> and there is no 
             * next sibling to the <li> then the parent <ul> will be checked for a next sibling instead. If there was another 
             * bullet item just after the <li> then it would be the sibling.
             * 
             * The checking will stop once either a <body>, <td> or <div> tag is reached.
             * 
             * @param node the DOM node who's next/previous sibling you are interested in
             * @param next if true then you want to check for the next sibling. If false then check the previous sibling.
             * @return the next/previous sibling DOM node or null if there is none.
             */                            
            function _getSiblingWithinBlockDisregardingNesting(node, next) {

                var containerSelector = ".mceContentBody, td, li";
                var $currentNode = $(node);
                
                var _siblingOperation;
                if (next) {
                    _siblingOperation = function(node) {
                        return node.nextSibling;
                    } 
                } else {
                    _siblingOperation = function(node) {
                        return node.previousSibling;
                    }
                }

                // Check for siblings, traversing up the tree from this node
                do {
                    var sibling = _siblingOperation($currentNode[0]);
                    if (sibling) return sibling;
                    
                    $currentNode = $currentNode.parent();
                } while ($currentNode.length > 0 && !$currentNode.is(containerSelector))
               
                return null;
            }

            /**
             * Returns true if the cursor is at the start of the given collapsed selection.
             * 
             * @param ed the editor
             * @return true if the cursor is at the start of the selection.
             */
            function isCursorAtStartOfContainer(range) {
                var startContainer = range.startContainer;
                if (startContainer.nodeType == 3) {
                    // deal with un-normalized situations where there could be multiple text nodes.
                    // this can occur when for instance a paragraph and a bullet list are merged (via a delete)
                    var hasPreviousSiblingContent = startContainer.previousSibling && 
                         (startContainer.previousSibling.nodeType == 3 || $(startContainer.previousSibling).is("br"));
                    return !hasPreviousSiblingContent && range.startOffset === 0;                    
                } else if (startContainer.nodeType == 1) {
                    return range.startOffset == 0;
                }
                
                return false;
            }
            
            /**
             * Returns true if the cursor is at the end of a given collapsed selection.
             * 
             * @return true if the cursor is at the end of the container.
             */
            function isCursorAtEndOfContainer(range) {
                var startContainer = range.startContainer;
                if (startContainer.nodeType == 3) {
                    // deal with un-normalized situations where there could be multiple text nodes.
                    // this can occur when for instance a paragraph and a bullet list are merged (via a delete)
                    var hasNextSiblingTextNode = startContainer.nextSibling && 
                        (startContainer.nextSibling.nodeType == 3 || $(startContainer.nextSibling).is("br"));
                    return !hasNextSiblingTextNode && range.endOffset == startContainer.nodeValue.length;
                } else if (startContainer.nodeType == 1) {
                    var nodeAfterCursor = startContainer.childNodes[range.startOffset];
                    // TinyMCE uses <p><br><p> structures for empty paragraphs in some browsers
                    // In such a scenario the cursor will be just before the <br> even though that
                    // is effectively the final position in the <p>
                    if ($(nodeAfterCursor).is("br") && nodeAfterCursor.nextSibling == null) {
                        return true;
                    } else {
                        return range.endOffset == startContainer.childNodes.length;                        
                    }
                }
                
                return false;
            }
            
            /**
             * Returns true if the supplied Element node has non-empty text nodes, not as immediate children
             * but as a deeper descendant.
             */
            function doesContainerHaveNestedContent(container) {
                var children = $(container).children();
                var i;
                for (i = 0; i < children.length; i++) {
                    if (isTextContainerEmpty(children[i])) {
                        continue;
                    }
                    
                    if ($(children[i]).contents().filter(function() {
                        return this.nodeType == 3;
                    }).length || doesContainerHaveNestedContent(children[i])) {
                        return true;
                    } 
                }

                return false;
            }

            /**
             * Check if the current cursor position (range) is in one of the positions unique to tables
             * of immediately before or immediately after the table (on the same 'line' as the table).
             * 
             * @param before set true if you want to check if the cursor is before the table. If false then check
             * if the cursor is after the table.
             * @param range w3c range that provides the current cursor position
             */
            function isCursorBeforeAfterTable(before, range) {
                var childNodeIndex = range.startOffset;
                
                function _isCursorAtTable(before, range) {
                    var currentNodeIndex = range.startOffset;
                    if (!before) {
                        currentNodeIndex--;
                    }
                    
                    return currentNodeIndex >= 0 && currentNodeIndex < range.startContainer.childNodes.length && range.startContainer.childNodes[currentNodeIndex].nodeName == "TABLE";
                }
                
                return range.startContainer == range.endContainer 
                    && range.startContainer.nodeType == 1 
                    && _isCursorAtTable(before, range);
            }

            function isCursorAtEndOfBottomRightCellInTable(range) {
                if (!range.collapsed) {
                    return false;
                }

                var node = range.startContainer, nextSibling;

                if (node.nodeType == 3) {
                    nextSibling = _getSiblingWithinBlockDisregardingNesting(node, true);
                    return range.startOffset == node.nodeValue.length && (nextSibling == null || $(nextSibling).is("br"));
                } else if (node.nodeType === 1) {
                    if ($(node).is("td, th") && range.startOffset === 0 && node.childNodes.length > 0) {
                        return $(node.childNodes[0]).is("br");
                    } else {
                        var index = (range.startOffset === 0) ? 0 : range.startOffset - 1;
                        nextSibling = _getSiblingWithinBlockDisregardingNesting(node.parentNode.childNodes[index], true);
                        return nextSibling == null || $(nextSibling).is("br");
                    }
                } else {
                    return false;
                }
            }

            function containsSoloBrElement(element) {
                return element && element.childNodes.length === 1 && $(element.childNodes[0]).is("br");
            }

            function shouldCancelDelete(selectedNode, range) {
                if (tinymce.isWebKit && isCursorBeforeAfterTable(false, range)) {
                    return true;
                } else if (tinymce.isGecko) {
                    if (isCursorAtEndOfBottomRightCellInTable(range)
                            /* firefox 3.6 will remove <p><br/></p> and <li><br/></li> on delete */
                            || (isCursorTarget(selectedNode) && range.startOffset === 0 && containsSoloBrElement(selectedNode))) {
                        return true;
                    }
                }

                return false;
            }

            function shouldCancelBackspace(selectedNode, range) {
                /**
                 * In WebKit, when you have <table></table> (or any block content), followed by say <p>foo</p>, if the cursor
                 * is inside and at the _start_ of the P element, hitting backspace will remove the table.
                 *
                 * The desired behaviour is to prevent this. Both MS Word and IE implement this.
                 */
                if (tinymce.isWebKit && (isCursorBeforeAfterTable(true, range) || (isCursorAtStartOfContainer(range) && isCursorTarget(selectedNode)))) {
                    return true;
                } else if (tinymce.isGecko && isCursorAtStartOfContainer(range) && isCursorTarget(selectedNode) && containsSoloBrElement(selectedNode)) {
                    return true;
                }

                return false;
            }

            /**
             * Backspace and delete suppression involves processing a few different key related events (depending on browsers)
             * so this method centralising the processing of the events.
             * 
             * Disallow backspace in a block if -
             * 1) the cursor is at the start of the block AND
             * 2) the preceding element is a  pre, div, blockquote or table AND
             * 3) there is no following element or the following element is a pre, div, blockquote or table
             * 
             * Disallow delete in a paragraph, pre, blockquote and div if -
             * 1) the cursor is at the end of the paragraph (empty is a degenerative case) AND
             * 2) the following element is a 'blocksToFixSelector'
             * 
             * @param ed the editor firing the event being handled
             * @param e the event itself.
             * @return true if the event should be allowed or false if it should be supressed
             */
            function deleteAndBackspaceKeyHandling(ed, e) {
                var keyCode = e.keyCode;
                if (keyCode != 8 && keyCode != 46) {
                    return true;
                }

                if (!ed.selection.isCollapsed()) {
                    // On the key down event the 'removal' hasn't yet happened. 
                    // We must allow that event to complete before cleaning up the content
                    setTimeout(function() {
                        addCursorTargetParagraphsToContent(ed.getBody());
                    }, 100);                    
                } else if (keyCode == 8 && shouldCancelBackspace(ed.selection.getNode(), ed.selection.getRng(W3C_RANGE))) {
                    return tinymce.dom.Event.cancel(e);
                } else if (keyCode == 46 && shouldCancelDelete(ed.selection.getNode(), ed.selection.getRng(W3C_RANGE))) {
                    return tinymce.dom.Event.cancel(e);
                }

                return true;  
            }
            
            ed.onSetContent.add(function(ed, o) {
                addCursorTargetParagraphsToContent(ed.getBody());
                convertEmptyParagraphs(ed.getBody());
            });

            ed.onGetContent.add(function(ed, o) {
                var startRootNodeString = "<div class='root-node'>";
                var endRootNodeString = "</div>";
                var wrappedContent = startRootNodeString + o.content + endRootNodeString;
                var modifiedDom = processCursorTargets($(wrappedContent));
                var serialised = AJS.Rte.getEditor().serializer.serialize(modifiedDom[0]);
                // remove the root-node wrapper
                o.content = serialised.substring(startRootNodeString.length, serialised.length - endRootNodeString.length);
            });
            
            // The editor only has a selection after it has initialised so hook onto selection events on editor init.
            ed.onInit.add(function() {
                // Insert macro, table or wiki markup all end up calling selection.setContent.
                // We should ensure that all necessary cursor targets exist after these operations.
                ed.selection.onSetContent.add(function(selection, o) {
                    addCursorTargetParagraphsToContent(ed.getBody());        
                });
                
                ed.dom.bind(ed.getBody(), "cut", function(e) {
                	// we need to do this 'later' to allow the event to actually manipulate the DOM first.
                    setTimeout(function() {
                        addCursorTargetParagraphsToContent(ed.getBody());
                    }, 100);
                });                
            });

            // ATLASSIAN - CONFDEV-5541 - Fix cursor position if enter / return / delete / backspace is pressed.
            if(tinymce.isGecko) {
                ed.onKeyDown.add(function(ed, e) {
                    if(e.keyCode == 13 || e.keyCode == 8 || e.keyCode == 46) {
                        // Enter may be broken if the cursor is in the wrong position - fix first
                        ed.selection.normalize();
                    }
                });
            }


            // Webkit will fire keyDown and keyUp for backspace and delete (even if one is supressed).
            ed.onKeyDown.add(deleteAndBackspaceKeyHandling);
            ed.onKeyUp.add(deleteAndBackspaceKeyHandling);
            
            // FF will fire all three key events for backspace and delete. If you register this on IE and Chrome
            // then you get CONFDEV-4062 which is fun.
            tinymce.isGecko && ed.onKeyPress.add(deleteAndBackspaceKeyHandling);             
            
            ed.onPaste.add(function(ed, e) {
                addCursorTargetParagraphsToContent(ed.getBody());
            });

            /**
             * Expose functions for unit testing
             */
            $.extend(this, {
                _isTextContainerEmpty: isTextContainerEmpty,
                _processCursorTargets: processCursorTargets,
                _isCursorAtStartOfContainer: isCursorAtStartOfContainer,
                _isCursorAtEndOfContainer: isCursorAtEndOfContainer,
                _getSiblingWithinBlockDisregardingNesting : _getSiblingWithinBlockDisregardingNesting,   
                _doesContainerHaveNestedContent : doesContainerHaveNestedContent,
                isCursorBeforeAfterTable : isCursorBeforeAfterTable,
                isCursorTarget: isCursorTarget,
                _addCursorTargetParagraphsToContent: addCursorTargetParagraphsToContent,
                _isCursorAtEndOfBottomRightCellInTable: isCursorAtEndOfBottomRightCellInTable,
                _shouldCancelDelete: shouldCancelDelete,
                _shouldCancelBackspace: shouldCancelBackspace
            });

            // This would deal with the cases -
            // - deletion with the keyboard of a non-collapsed selection
            // - pasting content from the clipboard
            // - cut
            // However, it is a bit of blunt knife approach. Hitting enter in the editor causes this event, as does pasting content, or adding macros, tables, etc
//            ed.onChange.add(function(ed, e) {
//                addCursorTargetParagraphsToContent(ed.getBody());
//            })

            AJS.bind('created.macro', function() {
                addCursorTargetParagraphsToContent(ed.getBody());
            });
            AJS.bind('udpated.macro', function() {
                addCursorTargetParagraphsToContent(ed.getBody());
            });

            // TODO cannot detect menu delete in any decent way across different browsers.
            // I either need to run a repeating fix up operation or fallback to keyboard navigation capturing.
		},

		createControl : function(n, cm) {
			return null;
		},

		getInfo : function() {
			return {
				longname : 'Cursor Target plugin',
				author : 'Atlassian',
				authorurl : 'http://www.atlassian.com',
				version : "1.0"
			};
		}
	});

	tinymce.PluginManager.add('cursorTarget', tinymce.plugins.CursorTargetPlugin);
})(AJS.$);

(function($) {
    tinymce.create('tinymce.plugins.ConfluenceCleanupPlugin', {
        init: function(ed) {
            //CONFDEV-2853/CONFDEV-3594 - When you delete in webkit the there is possibility that a Apple-style-span could be added. Make sure that it is removed.
            if(tinymce.isWebKit) {
                var colorMap = {},
                    getOrCreateMap = function (map, key) {
                        var value = map[key];
                        if (!value) {
                            value = {};
                            map[key] = value;
                        }
                        return value;
                    },
                    getFormat = function (span) {
                        var weightMap = colorMap[span.css("color")],
                            sizeMap;
                        if (weightMap) {
                            sizeMap = weightMap[span.css("font-weight")];
                            if (sizeMap) {
                                return sizeMap[span.css("font-size")];
                            }
                        }

                        return null;
                    };

                // CONFDEV-4061 - Map the heading formats to their corresponding styles.
                $("#format-dropdown").find("ul.aui-dropdown li").each(function () {
                    var link = $("a", this),
                        size = link.css("font-size"),
                        weight = link.css("font-weight"),
                        color = link.css("color"),
                        weightMap = getOrCreateMap(colorMap, color),
                        sizeMap = getOrCreateMap(weightMap, weight);

                    if (!sizeMap[size]) {
                        sizeMap[size] = $(this).attr("data-format");
                    }
                });

                ed.onNodeChange.add(function(ed, e) {
                    var appleStyleSpans = ed.dom.select('span.Apple-style-span', ed.dom.doc.body),
                        appleStyleFonts = ed.dom.select('font.Apple-style-span', ed.dom.doc.body),
                        appleStyle= appleStyleSpans.concat(appleStyleFonts);
                    
                    for(var i=0, ii=appleStyle.length; i<ii; i++) {
                        if (ed.dom.is(appleStyle[i], '[face="mceinline"]')) {
                            break; //don't remove if it's mceinline as tiny places these in and later formats them correctly
                        }

                        var format = getFormat($(appleStyle[i]));
                        //if we find a format to use, replace the span with the format, otherwise, leave the apple-style-span
                        if (format) {
                            var bm = ed.selection.getBookmark();
                            format && ed.dom.remove(appleStyle[i], 1);
                            ed.selection.moveToBookmark(bm);
                            ed.execCommand("FormatBlock", false, format)
                        }
                    }
                });
            }

            // CONFDEV-3864 - Firefox 3.6 creates extra img tags as part of its drag and drop behaviour when dragging
            // an image into the editor, with the src of these tags pointing to the local filesystem. This cleans these img tags up.
            ed.onNodeChange.add(function(ed) {
                var localPrefix = "file:///",
                    images = ed.dom.select('img',  ed.dom.doc.body),
                    numImages = images.length;

                for (var i = 0; i < numImages; i++) {
                    var image = $(images[i]),
                        local = image.attr('src');

                    if (local.substr(0, localPrefix.length) === localPrefix) {
                        ed.dom.remove(images[i]);
                    }
                }
            });
        },

        getInfo: function() {
            return {
                longname: 'ConfluenceCleanupPlugin',
                author: 'Atlassian',
                authorurl: 'http://www.atlassian.com',
                infourl: 'http://www.atlassian.com',
                version: '1.0'
            };
        }
    }); 
    tinymce.PluginManager.add('confluencecleanupplugin', tinymce.plugins.ConfluenceCleanupPlugin);
   
})(AJS.$);


(function() {

    var DIALOG_HEADER_SIZE = 43;       // as specified in dialog.css for the h2 element
    var DIALOG_BUTTON_PANEL_SIZE = 43; // as in the dialog.js
    var DIALOG_CONTENT_PADDING = 10;

    var Dispatcher = tinymce.util.Dispatcher, each = tinymce.each, isIE = tinymce.isIE, isOpera = tinymce.isOpera;

    tinymce.create('tinymce.plugins.AUIWindowManager', {
        init : function(ed, url) {
            // Replace window manager
            ed.onBeforeRenderUI.add(function() {
                ed.windowManager = new tinymce.AUIWindowManager(ed);
                // DOM.loadCSS(url + '/skins/' + (ed.settings.inlinepopups_skin || 'clearlooks2') + "/window.css");
            });
        },

        getInfo : function() {
            return {
                longname : 'AUIWindowManager',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    tinymce.create('tinymce.AUIWindowManager:tinymce.WindowManager', {

        /**
         * Create the AUIWindowManager plugin.
         * @constructor
         */
        AUIWindowManager : function(ed) {
            var t = this;

            t.editor = ed;
            t.onOpen = new Dispatcher(t);
            t.onClose = new Dispatcher(t);
            t.params = {};
            t.features = {};
            t.modalDialogsStack = new Array();
            t.dialogs = {};
            t.count = 0;
        },

        /**
         * Open and return a window as specified by the settings parameter.
         *
         * The created window is returned and should be supplied to the close function to dismiss it later.
         *
         * Note that regardless of the setting, all windows in this Window Manager are modal. Likewise, they are
         * not resizeable and their position is centred regardless of the position requested.
         *
         * The following settings are supported -
         *     name (string) - Window title resource key - will be localized.
         *     file (string) - URL of the file to open in the window.
         *     content (string) - HTML Content to be used. This is only applicable for non-popup windows and will be used
         *     in preference to the file setting
         *     width (int) - Width in pixels. (default 400)
         *     height (int) - Height in pixels. (default 250)
         *     popup (boolean) - defaults to true. Indicates that the window contents should be rendered in an
         *                       iframe, for compatibility with standard TinyMCE plugins.
         *
         * The following custom params are supported -
         *     cssClass (string) - a CSS class to put on the opened dialog (default is tinymce-auidialog)
         *     helpLink (string) - the property name of a help link you want added to the dialog.
         *     buttons (array[Object]) - an array of buttons to be added in order.
         *                               Each item in the array should have a label String and an action function.
         *     cancelLink (boolean) - specified whether a cancel button should be automatically added - defaults to true.
         *
         * @returns the id of the dialog opened.
         */
        open: function (settings, params) {
            var t = this, f = '', x, y, sw, sh, vp = tinymce.DOM.getViewPort(), u;

            settings = settings || {};
            t.initialiseSettings(settings);

            params = params || {};
            sw = isOpera ? vp.w : screen.width; // Opera uses windows inside the Opera window
            sh = isOpera ? vp.h : screen.height;

            params = this.setDefaultParameters(params);
            params.mce_width = settings.width;
            params.mce_height = settings.height;
            params.mce_auto_focus = settings.auto_focus;

            if (isIE) {
                settings.center = true;
                settings.help = false;
                settings.dialogWidth = settings.width + 'px';
                settings.dialogHeight = settings.height + 'px';
            }

            t.features = settings;
            t.params = params;
            t.onOpen.dispatch(t, settings, params);

            var height = settings.height;
            if (settings.name) {
                height += DIALOG_HEADER_SIZE;
            }

            if (params.cancelLink || params.buttons) {
                height = height + DIALOG_BUTTON_PANEL_SIZE;
            }

            // include the padding
            height = height + (2 * DIALOG_CONTENT_PADDING);

            // add an arbitrary amount to cope with the user agent border that is often applied to iframes
            if (settings.popup) {
                height = height + 8;
            }

            var id = settings.id || tinymce.DOM.uniqueId();
            var dialog = new AJS.Dialog(settings.width, height, id);
            if (settings.name) {
                dialog.addHeader(this.editor.getLang(settings.name));
            }

            this.createDialogButtons(dialog, params, id);

            settings.file = settings.file || settings.url;
            settings.file = tinymce._addVer(settings.file);

            this.createDialogContent(dialog, settings, params, id);
            params.helpLink && this.createHelpLink(dialog, params.helpLink, params.helpName, id);
            params.hintText && this.createHintText(dialog, params.hintText, id);

            // Add window so it can be managed elsewhere if necessary
            var dialogDetails = {
                    id : id,
                    settings : settings,
                    params : params,
                    dialog : dialog
            };

            this.modalDialogsStack.push(dialogDetails);
            this.dialogs[id] = dialogDetails;
            this.count++;

            AJS.Rte.BookmarkManager.storeBookmark();
            dialog.show();

            if (dialog.takeFocus != undefined) {
                dialog.takeFocus();
            }

            // IE 8 is getting confused with the panel size layout and always including scrollbars.
            // This fixes it.
            if (isIE) {
                AJS.$("#" + id).css("overflow", "hidden");
                AJS.$("#" + id + " ." + params.cssClass).css("overflow", "hidden");
            }

            return id;
        },

        initialiseSettings : function (settings) {
            settings.width = parseInt(settings.width || 400,10);
            settings.height = parseInt(settings.height || 250,10);
            settings.resizable = false;
            if (settings.popup == undefined) {
                settings.popup = true;
            }

            if (settings.popup && settings.content != undefined) {
                settings.popup = false;
            }

            if (settings.scrollbars) {
                settings.scrollbars = "yes";
            } else {
                settings.scrollbars = "no";
            }
        },

        /**
         * Create any specified buttons on the dialog, as well as adding a help link if the parameter
         * is supplied
         */
        createDialogButtons : function (dialog, parameters, dialogId) {
            if (parameters.buttons) {
                var i = 0;
                for (i=0; i < parameters.buttons.length; i++) {
                    var buttonDef = parameters.buttons[i];
                    dialog.addButton(this.editor.getLang(buttonDef.label), buttonDef.action);
                }
            }

            if(parameters.cancelLink) {
                dialog.addCancel(this.editor.getLang("auiwindowmanager.cancel"), function () { return tinyMCE.activeEditor.windowManager.close(dialogId); });
            }
        },


        /**
         * Create the content of the dialog taking into account whether an iframe is required or
         * dynamic loading into the DOM.
         */
        createDialogContent : function (dialog, settings, params, id) {

            var contentId = this.createContentId(id);
            var panelId = "panel_" + contentId;
            if (settings.popup) {
                dialog.addPanel(panelId, AJS.$("<iframe id='" + contentId + "' width='100%' height='" + settings.height + "px' frameborder='0' name='auidialogiframe' src='" + settings.file + "' scrolling='" + settings.scrollbars + "'></iframe>"), params.cssClass);
                dialog.takeFocus = function() {
                    AJS.$("#" + contentId).focus();
                };
            } else {
                if (settings.content == undefined) {
                    // Dynamically loading the panel content from the settings.file
                    dialog.addPanel(panelId, AJS.$("<div id='" + contentId + "'/>"), params.cssClass);
                    // Load the content of the window
                    AJS.$("#" + contentId).load(settings.file);
                    // TODOXHTML Add some error handling for when the content doesn't load
                } else {
                    // Statically provided panel content
                    dialog.addPanel(panelId, AJS.$("<div id='" + contentId + "'>" + settings.content + "</div>"), params.cssClass);
                }

                dialog.takeFocus = function() {
                    // set the focus to the first input element found
                    var inputs = AJS.$("#" + contentId + " :input");
                    if (inputs.length) {
                        AJS.$(inputs[0]).focus();
                    } else {
                        // set focus to the first button found on the dialog
                        var buttons = AJS.$("#" + contentId + " .button-panel button");
                        if (buttons.length) {
                            AJS.$(buttons[0]).focus();
                        }
                    }
                };
            }

            dialog.getPanel(0).setPadding(DIALOG_CONTENT_PADDING);
            dialog.gotoPanel(0,0);
        },

        //can be used to render help text directly instead of making a request for a link
        createHintText : function(dialog, hintText, id) {
            var tip = AJS.$("<div></div>").addClass("dialog-tip");
            tip.text(tinyMCE.activeEditor.getLang(hintText));
            AJS.$("#" + id + " .dialog-button-panel").append(tip);
        },

        /**
         * If specified in the settings then add a help link to the button bar of the dialog.
         */
        createHelpLink : function (dialog, linkKey, linkName, id) {
            var helpLink = AJS.$("<div></div>").addClass("dialog-help-link");
            helpLink.load(AJS.params.contextPath + "/plugins/tinymce/helplink.action", { linkUrlKey : linkKey, linkNameKey: (linkName || "") });
            AJS.$("#" + id + " .dialog-components .dialog-title").append(helpLink); // TODO: I believe this should be added *after* the title, but this is how the macro form is doing it.
        },

        /**
         * Close the specified window
         */
        close : function(win, id) {
            var dialogId;
            if (typeof(win) == 'string') {
                dialogId = win;
            } else if (id) {
                dialogId = id;
            } else {
                // a window was supplied, without an id so we just assume the last opened dialog is to
                // be closed
                var dialog = this.modalDialogsStack.pop();
                if (dialog) {
                    dialogId = dialog.id;
                }
            }

            if (!this.dialogs[dialogId]) {
                AJS.log("Couldn't find id " + dialogId + " in dialogs array so dialog is not closed.");
            } else {
                this.count--;
                var dialog = this.dialogs[dialogId];
                dialog.dialog.remove();
                this.dialogs[dialogId] = null;

                // set focus back to the editor
                tinyMCE.activeEditor.focus();

                AJS.Rte.BookmarkManager.restoreBookmark();
            }
            return false;
        },

        alert : function(title, callback, scope, w) {
            var settings = {
                    content : "<p>" + title + "</p>",
                    width : 500,
                    height : 160,
                    popup : false
            };

            var alertDialogId;

            var okCallback = function() {
                tinyMCE.activeEditor.windowManager.toggleOtherDialogs(alertDialogId, true);
                tinyMCE.activeEditor.windowManager.close(alertDialogId);
                if (callback) {
                    callback.call(scope || this);
                }
            };

            var params = {
                    buttons : [{
                        label : "auiwindowmanager.ok",
                        action : okCallback
                    }],
                    cancelLink: false,
                    cssClass : "tinymce-auidialog-alert"
            };

            alertDialogId = this.open(settings, params);

            // Disable the buttons on all the other dialogs while the alert is shown.
            this.toggleOtherDialogs(alertDialogId, false);
        },

        createInstance : function(cl, a, b, c, d, e) {
            var f = tinymce.resolve(cl);

            return new f(a, b, c, d, e);
        },

        // Hide the button panel on all dialogs except the alert one that was just opened.
        // The button panel may contain a cancel link so disabling is not enough. A hide is required.
        toggleOtherDialogs: function(alertDialogId, enable) {
            var i = 0;
            for (i = 0; i < this.modalDialogsStack.length; i++) {
                var dialog = this.modalDialogsStack[i];
                if (dialog.id != alertDialogId) {
                    AJS.$("#" + dialog.id + " .dialog-button-panel").toggle(enable);
                }
            }
        },

        confirm : function(t, cb, s, w) {
            w = w || window;

            cb.call(s || this, w.confirm(this._decode(this.editor.getLang(t, t))));
        },

        /**
         * Change the busy status of the dialog. If true then the dialog will indicate it is busy and not accept
         * any input. If false then the dialog should revert to it's normal state.
         *
         *  @param dialogId The id of the dialog to change the status of
         *  @param status true or false.
         */
        setBusy : function (dialogId, busy) {
            var dialog = this.dialogs[dialogId];
            if (!dialog) {
                return;
            }

            if (busy) {
                // hide the content panel for this dialog
                var contentId = this.createContentId(dialogId);
                var content = AJS.$("#" + contentId);

                content.hide();
                var contentHolder = content.parent();
                var busyId = this.createBusyContentId(dialogId);
                var busyPanel = AJS.$("#" + busyId, contentHolder);
                if (!busyPanel.length) {
                    contentHolder.append(AJS.$("<div id='" + busyId + "' class='spinner'></div>"));
                    busyPanel = AJS.$("#" + busyId, contentHolder);
                    Raphael.spinner(busyPanel[0], 50, "#666");
                }

                AJS.$(".button-panel button" ,AJS.$("#" + dialogId)).each(function(index,element) {
                    AJS.$(element).attr("disabled","disabled");
                });

                AJS.$(busyPanel[0]).show();
            } else {
                // hide the busy panel and show the content again
                var busyId = this.createBusyContentId(dialogId);
                var busyPanel = AJS.$("#" + busyId);
                if (busyPanel.length) {
                    AJS.$(busyPanel[0]).hide();
                }

                var contentId = this.createContentId(dialogId);
                var content = AJS.$("#" + contentId);

                AJS.$(".button-panel button" ,AJS.$("#" + dialogId)).each(function(index,element) {
                    AJS.$(element).attr("disabled","enabled");
                });

                AJS.$(content[0]).show();
            }
        },

        logMCESelection : function (title) {
            var s = tinyMCE.activeEditor.selection;
            AJS.log("******************************");
            AJS.log("Logging TinyMCE selection title:    " + title);
            AJS.log("Bookmark:");
            AJS.log(s.getBookmark());
            var rangeNodeText = AJS.$(s.getRng().startContainer).text() || $(s.getRng().startContainer.parentNode).text();
            AJS.log("Range: " + rangeNodeText);
            AJS.log(s.getRng());
        },

        //
        // ---- Internal functions --------------------------------------------
        //
        setDefaultParameters: function (params) {
            params.inline = false;

            if (!params.cssClass) {
                params.cssClass = "tinymce-auidialog";
            }

            if (params.cancelLink != false)
                params.cancelLink = true;

            return params;
        },

        /**
         * @param id the id to create a content id for.
         * @return a contentId for the supplied id
         */
        createContentId : function (id) {
            return "content_" + id;
        },

        createBusyContentId : function (id) {
            return "content_busy_" + id;
        }

    });

    // Register plugin
    tinymce.PluginManager.add('auiwindowmanager', tinymce.plugins.AUIWindowManager);
})();

tinymce.confluence.macrobrowser = (function($) { return {
    /**
     * The current selection range in the editor
     */
    storedRange : null,
    /**
     * The current bookmark location in the editor
     */
    bookmark : null,

    getCurrentNode : function () {
        return $(tinyMCE.activeEditor.selection.getNode());
    },
    isMacroDiv : function(node) {
        return $(node).hasClass("wysiwyg-macro");
    },
    isMacroTag : function(node) {
        var $node = $(node);
        return $node.hasClass("wysiwyg-macro") || $node.hasClass("editor-inline-macro");
    },
    isBodylessMacro : function (node) { /** FIXME: This method's name is misleading. */
        return $(node).hasClass("editor-inline-macro");
    },
    isMacroWithBody : function (node) {
        return $(node).hasClass("wysiwyg-macro");
    },
    isMacroStartTag : function(node) {
        return $(node).hasClass("wysiwyg-macro-starttag");
    },
    isMacroEndTag : function(node) {
        return $(node).hasClass("wysiwyg-macro-endtag");
    },
    isMacroBody : function(node) {
        return $(node).hasClass("wysiwyg-macro-body");
    },
    hasMacroBody : function(node) {
        return $(node).attr("macrohasbody") == "true";
    },
    /**
     * Returns an array of macro names for macro divs enclosing the current node.
     */
    getNestingMacros : function(node) {
        var $node = $(node || this.getCurrentNode());
        var nestingMacros = [];
        $node.parents(".wysiwyg-macro").each(function() {
            nestingMacros.push($(this).attr("data-macro-name"));
        });
        return nestingMacros;
    },

    logMCESelection : function (title) {
        var s = tinyMCE.activeEditor.selection;
        AJS.log("******************************");
        AJS.log("Logging TinyMCE selection title:    " + title);
        AJS.log("Bookmark:");
        AJS.log(s.getBookmark());
        var rangeNodeText = $(s.getRng().startContainer).text() || $(s.getRng().startContainer.parentNode).text();
        AJS.log("Range: " + rangeNodeText);
        AJS.log(s.getRng());
    },

    getSelectedMacro : function(editor) {
        var t = tinymce.confluence.macrobrowser,
            $selectionNode = t.getCurrentNode();
        AJS.log("getSelectedMacro: $selectionNode=" + $selectionNode[0]);
        return t.isMacroDiv($selectionNode) ? $selectionNode : $selectionNode.closest(".wysiwyg-macro");
    },

    openMacro : function(macroDefinition) {
        AJS.MacroBrowser.open({
            selectedMacro : macroDefinition,
            onComplete : tinymce.confluence.macrobrowser.macroBrowserComplete,
            onCancel : tinymce.confluence.macrobrowser.macroBrowserCancel
        });
    },

    editMacro: function (macroNode) {
        var t = tinymce.confluence.macrobrowser;
        var $macroDiv = $(macroNode);
        t.editedMacroDiv = $macroDiv[0];

        /**
         * serialize() to ensure that any bogus tinymce tags + dirty browser markup are cleaned up.
         * macro node should be cloned before serialization, as we don't want serialization tampering with the actual DOM element.
         */
        var macroHtml = AJS.Rte.getEditor().serializer.serialize($macroDiv.clone()[0], {
            forced_root_block: false // Prevent serialize from wrapping in a <p></p>
        });

        AJS.Rte.getEditor().selection.select($macroDiv[0]);
        AJS.Rte.BookmarkManager.storeBookmark();

        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: AJS.params.contextPath + "/rest/tinymce/1/macro/definition",
            data: AJS.$.toJSON({
                contentId: Confluence.Editor.getContentId(),
                macroHtml: macroHtml
            }),
            dataType: "text",
            success: function (macro) {
                t.openMacro(AJS.$.secureEvalJSON(macro));
            }
        });

    },

    /**
     * Called to insert a new macro.
     * If user has not selected text, just open the Macro Browser.
     * If user has selected text, it will convert it to wiki markup for the body of the macro
     */
    macroBrowserToolbarButtonClicked : function(options) {

        var t = tinymce.confluence.macrobrowser,
            editor = tinyMCE.activeEditor,
                $node = t.getCurrentNode();
        AJS.Rte.BookmarkManager.storeBookmark();

        // Inserting new macro
        var settings = $.extend({
            presetMacroName : null,
            nestingMacros : t.getNestingMacros($node),
            onComplete : t.macroBrowserComplete,
            onCancel : t.macroBrowserCancel,
            mode : "insert",
            selectedHtml: "",
            selectedText: ""
        }, options);

        if (!options.ignoreEditorSelection) {
            settings.selectedHtml = editor.selection.getContent();
            settings.selectedText = editor.selection.getContent({ format: "text" });
        }
        AJS.MacroBrowser.open(settings);
    },
    /**
     * Takes macro markup (usually generated by the Macro Browser) and inserts/updates the relevant Macro macroHeader
     * in the RTE.
     * @param macro macro object describing inserted/edited macro
     */
    macroBrowserComplete : function(macro) {
        var t = tinymce.confluence.macrobrowser,
            macroRenderRequest = {
            contentId: AJS.Meta.get('content-id') || "0",
            macro: {
                name: macro.name,
                params: macro.params,
                defaultParameterValue: macro.defaultParameterValue,
                body : macro.bodyHtml
            }
        };

        if (t.editedMacroDiv) {
            tinymce.confluence.MacroUtils.updateMacro(macroRenderRequest, t.editedMacroDiv);
            delete t.editedMacroDiv;
        } else {
            tinymce.confluence.MacroUtils.insertMacro(macroRenderRequest);
        }
    },

    // Called when the macro browser is closed to clean up and reset data.
    macroBrowserCancel : function() {
        var t = tinymce.confluence.macrobrowser;
        AJS.Rte.BookmarkManager.restoreBookmark();
        t.editedMacroDiv = null;
        t.editedMacro = null;
    }
};})(AJS.$);

(function($) {
        /**
         *  Keys of properties that should be stored as attributes of the image element.
         */
        var imageElementAttributeKeys =  ["imagetext", "src", "align", "border", "width", "height"],

        /**
         * Regex matches if an image is attached to Confluence content
         */
        contentImageRegex = /\/download\/(thumbnails|attachments)\/([0-9]+)\//,

        /**
         * The pageId for the content owning the image attachment is not
         * stored against the image element directly, so we extract it from the
         * image element's src attribute.
         *
         * e.g. /confluence/download/attachments/2129935/ca5.gif extracts "2129935"
         *      /confluence/download/thumbnails/2129935/ca5.gif ALSO extracts "2129935"
         */
        getPageIdFromImageElement = function (imgEl) {
            var src = $(imgEl).attr("src"),
                matches = src.match(contentImageRegex);
            if (matches && matches.length == 3) {
                return matches[2];
            }
            AJS.log("ERROR: could not parse page id from image url " + src);
            return "0";
        },

        /**
         * Creates the string to store in the src attribute of the img element.
         *
         * e.g. /confluence/download/attachments/884738/hedgehog.jpg
         */
        getSrc = function (imgProps) {
            var destination = imgProps.destination;
            if (imageUtils.isRemoteImg(destination)) {
                return destination;
            }
            var thumbnailDir = '/thumbnails/';
            var attachmentsDir = '/attachments/';
            return (imgProps.thumbnail) ? destination.replace(attachmentsDir, thumbnailDir) :
                destination.replace(thumbnailDir, attachmentsDir);
        },

        /**
         * Returns true iff this is a Confluence image tag with a wiki-markup "imagetext" attribute.
         */
        isEmbeddedImage = function(imgEl) {
            return imgEl && $.nodeName(imgEl, 'img') && $(imgEl).hasClass("confluence-embedded-image");
        },

        newImageCounter = 0,

        // Local ref to tinymce.confluence.ImageUtils 
        imageUtils;

    /**
     * ImageProperties class represents all of the attributes of an image in the
     * RTE:
     *      imageFileName
     *      thumbnail
     *      border
     *      width
     *      height
     *      url (optional) the full url of the image for an external image
     *      pageId (optional if url exists) the id of the page owning the image attachment
     *      destination (optional) the full "wiki" path to the image, eg "TST:Foo^bar.jpg"
     *
     *  It can return calculated values, such as:
     *      src
     *      imagetext
     */
    tinymce.confluence.ImageProperties = function (props) {
        if (isEmbeddedImage(props)) {
            // props is an img element
            var imgEl = $(props);

            props = {
                destination: imgEl.attr("src"),
                url: imgEl.attr("src"),
                border: (imgEl.attr("class") && imgEl.attr("class").indexOf("confluence-content-image-border") != -1) ? 1 : 0,
                width: imgEl.attr("width"),
                height: imgEl.attr("height")
            };

            if (!imageUtils.isRemoteImg(props.destination)  && (!AJS.$(imgEl).hasClass("confluence-external-resource")))
                props.pageId = getPageIdFromImageElement(imgEl);

            return $.extend({}, props);

        } else if (props.destination) {
            // props is a map of ImageProperties values
            props.imageFileName = props.imageFileName || props.destination;

            return $.extend({}, props);
        }
        // not an image element and not a valid properties object - return nothing
        return null;
    };

    imageUtils = tinymce.confluence.ImageUtils = {

        /**
         * Loads the image properties into the passed img element.
         * @param img an HtmlImageElement or a jQuery wrapping one.
         * @param imgProps the image properties to update the element with.
         */
        updateImageElement: function (img, imgProps) {

            var $img = $(img);

            imgProps.src = getSrc(imgProps);

            // Renderer & TinyMCE both add style tags to show borders - these both need updating, if present, or they'll
            // override any border attribute set.
            $img.toggleClass("confluence-content-image-border", !!imgProps.border);
            $img.toggleClass("confluence-thumbnail", !!imgProps.thumbnail);

            for (var i = 0, ii = imageElementAttributeKeys.length; i < ii; i++) {
                var key = imageElementAttributeKeys[i], val = imgProps[key];
                if (val !== false && val != null) {
                    $img.attr(key, val);
                } else {
                    $img.removeAttr(key);
                }
            }
            tinyMCE.activeEditor.undoManager.add();
        },

        /**
         * Insert an external image or an image placeholder with the specified properties.
         * @param properties {Object} Required fields: filename, contentId, url (if external). optional: thumbnail, alignment, border
         */
        insertFromProperties: function (properties) {
            AJS.$.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: AJS.params.contextPath + "/rest/tinymce/1/embed/placeholder/image",
                data: AJS.$.toJSON(properties),
                dataType: "text",
                success: function(imagePlaceholderHtml) {
                    // there is no simple way to get back at the inserted image, so we put
                    // a temporary id on it before inserting
                    var ed = tinyMCE.activeEditor,
                        id = "_" + (+ new Date),
                        div = $("<div></div>").append($(imagePlaceholderHtml).attr("id", id));

                    ed.selection.setContent(div.html());

                    var imgElem = ed.dom.get(id);
                    ed.dom.setAttrib(imgElem, "id", "");
                    $(imgElem).load(function() {
                        // ATLASSIAN - CONFDEV-3749 - make sure new element is visible
                        AJS.Rte.showSelection(function() {
                            AJS.trigger("trigger.property-panel", {elem: imgElem});
                            ed.undoManager.add();
                        });
                    });
                }
            });
        },

        /**
         * Checks if an image has a url as its destination instead of a wiki link.
         * @return true if image text starts with http or https protocol
         */
        isRemoteImg: function (destination) {
            var currentbaseurl = Confluence.Editor.Adapter.getCurrentBaseUrl();
            return destination.match('(https?://)') && destination.indexOf(currentbaseurl) == -1;
        }
    };
})(AJS.$);

(function ($) {
    var that;

    /**
     * Creates a temporary macro node from the passed macro object,
     * without requesting any HTML from the server.
     * @param macro the data for the macro to be created.
     */
    function makeMacroNode(macro, placeholderId) {
        var ed = AJS.Rte.getEditor(),
            macroNode = ed.dom.create("span"); // For now.
        return fillMacroNode(macroNode, placeholderId, macro);
    }

    /**
     * Fill the node with some attributes for a macro.
     * This is intended to be a temporary placeholder until the server responds with the full one.
     * @note This is quite similar to the link browser's Link#fillNode method.
     * Consider refactoring the approach to be more like the link-object + link-adapter appraoch.
     */
    function fillMacroNode(node, placeholderId, macro) {
        if (!node) return null;
        node = (node.jquery) ? node[0] : node;

        node.setAttribute('data-macro-name', macro.name);
        node.id = placeholderId;
        return node;
    }

    /**
     * Fixes the DOM up when we insert a block macro placeholder
     * inside another block-level element.
     */
    function fixHtmlAroundMacroNode(macroNode) {
        var mb = tinymce.confluence.macrobrowser,
            ed = tinymce.activeEditor;
        macroNode = (macroNode.jquery) ? macroNode[0] : macroNode;

        if (mb.isMacroWithBody(macroNode)) {
            if (/P|H\d/.test(macroNode.parentNode.nodeName)) {
                ed.dom.split(macroNode.parentNode, macroNode);
            }
        }
    }

    /**
     * Places the editor cursor inside a bodied macro placeholder.
     */
    function focusMacroNode(macroNode) {
        AJS.debug("focusMacroNode: start method");
        var mb = tinymce.confluence.macrobrowser,
            ed = tinymce.activeEditor,
            doc = ed.getDoc(),
            nodeToFocus,
            range;
        macroNode = (macroNode.jquery) ? macroNode[0] : macroNode;

        if (mb.isMacroWithBody(macroNode)) {
            // add a BR to allow the cursor to be positioned in the placeholder
            !tinymce.isIE && $("p, pre", macroNode).each(function () {
                if (this.childNodes.length === 0) {
                    this.appendChild(doc.createElement("br"));
                }
            });

            nodeToFocus = ed.dom.select('p, pre', macroNode);
            if (nodeToFocus.length > 0) {
                range = ed.selection.getRng(true);
                range.setStart(nodeToFocus[0], 0);
                range.setEnd(nodeToFocus[0], 0);
                ed.selection.setRng(range);
            }
        }
        AJS.debug("focusMacroNode: end method.");
        return macroNode;
    }

    tinymce.confluence.MacroUtils = that = {

        insertMacro: function (macroRenderRequest, postInsertion) {
            var tempMacroId = "macro-" + (new Date()).getTime(),
                tempNode,
                request = {
                    url: Confluence.getContextPath() + "/rest/tinymce/1/macro/placeholder",
                    contentType: "application/json; charset=utf-8",
                    data: $.toJSON(macroRenderRequest),
                    dataType: "text"
                },
                nodeInsertionTask;

            AJS.Rte.BookmarkManager.restoreBookmark(); // this is set by tinymce.confluence.macrobrowser#editMacro

            // Create a temporary node that will be used to hold the fort while we wait for the server
            tempNode = tinymce.confluence.NodeUtils.replaceSelection(makeMacroNode(macroRenderRequest.macro, tempMacroId));

            nodeInsertionTask = tinymce.confluence.NodeUtils.updateNode(tempNode, request);
            nodeInsertionTask.done(function(newNode) {
                fixHtmlAroundMacroNode(newNode);
                focusMacroNode(newNode);

                // ATLASSIAN - CONFDEV-3749 - make sure new element is visible
                AJS.Rte.showSelection();

                // Trigger callbacks
                typeof postInsertion == "function" && postInsertion(newNode);
                AJS.trigger('created.macro', newNode);
            });

            // After all other callbacks are made, create an undo step.
            nodeInsertionTask.done(tinymce.activeEditor.undoManager.add);
        },

        updateMacro: function (macroRenderRequest, macroNode, postInsertion) {
            var request = {
                url: Confluence.getContextPath() + "/rest/tinymce/1/macro/placeholder",
                contentType: "application/json; charset=utf-8",
                data: $.toJSON(macroRenderRequest)
            }, nodeUpdateTask;

            tinymce.activeEditor.undoManager.add();

            AJS.Rte.BookmarkManager.restoreBookmark(); // this is set by tinymce.confluence.macrobrowser#editMacro

            nodeUpdateTask = tinymce.confluence.NodeUtils.updateNode(macroNode, request);
            nodeUpdateTask.done(function(node) {
                focusMacroNode(node);

                // Trigger callbacks
                typeof postInsertion == "function" && postInsertion(node);
                AJS.trigger('updated.macro', node);
            });
            // After all other callbacks are made, create an undo step.
            nodeUpdateTask.done(tinymce.activeEditor.undoManager.add);
        }
    };
})(AJS.$);

/**
 * Confluence TinyMCE plugin. Creates controls and commands for:
 * - inserting images
 * - macro browser
 * - unlinking
 *
 * TODOXHTML: clean this up and split into separate TinyMCE plugins
 */
(function($) {

    // Register commands and onclicks
    tinymce.create('tinymce.plugins.ConfluencePlugin', {
        init : function(ed, url) {

            ed.addCommand("mceConfimage", Confluence.Editor.defaultInsertImageDialog);

            ed.addCommand("mceConfMacroBrowser", tinymce.confluence.macrobrowser.macroBrowserToolbarButtonClicked);

            ed.addCommand("mceConfUnlink", function (ui, val) {
                var ed = AJS.Rte.getEditor(),
                    s = ed.selection,
                    n = val || s.getNode(),
                    $n = $(n);

                if (n.nodeName != 'A') {
                    var parent = $n.closest("a[href]");
                    if (!parent.length) {
                        return;
                    }
                    n = parent[0];
                    $n = parent;
                }
                // unlinking external links requires wrapping it a span with a class
                // so we don't automatically convert them to links on the server
                var removeTrailingSlash = function (text) {
                    var trailingSlashRegexResult = /(.*)[\/]$/.exec(text);
                    return trailingSlashRegexResult != null ? trailingSlashRegexResult[1] : text;
                };
                if (!$n.attr("data-linked-resource-id") && AJS.Editor.Adapter.isExternalLink($n.attr("href")) &&
                        $n.text() == removeTrailingSlash($n.attr("href"))) {
                    var span = ed.dom.create("span", {"class": "nolink"}, $n.attr("href"));
                    ed.dom.replace(span, n, false);
                }
                else {
                    s.select(n);
                    // Firefox 3.6 doesn't remove link classes when it
                    // unlinks so we remove them manually
                    $n.removeClass("createlink");
                    $n.removeClass("unresolved");
                    ed.execCommand("UnLink");
                }
            });

            // Register buttons
            ed.addButton("confimage", {title : "confluence.confimage_desc", cmd : "mceConfimage"});
            ed.addButton("conf_macro_browser", {title : "confluence.conf_macro_browser_desc", cmd : "mceConfMacroBrowser"});
        },

        getInfo : function () {
            return {
                longname : "Confluence TinyMCE Plugin",
                author : "Atlassian",
                authorurl : "http://www.atlassian.com",
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    // Register plugin
    tinymce.PluginManager.add("confluence", tinymce.plugins.ConfluencePlugin);
})(AJS.$);

/**
 * Macro placeholders.
 *
 * Copyright 2010, Atlassian
 */

(function($) {
    var VK = tinymce.VK;

	tinymce.create('tinymce.plugins.MacroPlaceHolderPlugin', {
		/**
		 * Initializes the plugin, this will be executed after the plugin has been created.
		 * This call is done before the editor instance has finished it's initialization so use the onInit event
		 * of the editor instance to intercept that event.
		 *
		 * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
		 * @param {string} url Absolute URL to where the plugin is located.
		 */
		init : function(ed) {

            var t = this;

            /**
             * Ensure that placeholders inserted inside P's are moved out as P's should only contain inline content.
             */
            ed.onInit.add(function (ed) {
                /**
                 * ed.selection is initialized until after the editor has completed initialization.
                 * Hence the registering of this listener be added to the 'onInit' event.
                 */
                ed.selection.onSetContent.add(function (ed) {
                    tinymce.each(ed.dom.select('p > .wysiwyg-macro'), function (macroPlaceholderNode) {
                        var parentParagraphNode = ed.dom.getParent(macroPlaceholderNode.parentNode, 'p');
                        try {
                            ed.dom.split(parentParagraphNode, macroPlaceholderNode);
                        } catch (ex) {
                            // IE can sometimes fire an unknown runtime error so we just ignore it
                        }
                    });
                });
            });
            
            /**
             * TinyMCE will strip empty <PRE> blocks (which causes problems for plain text
             * macros - CONFDEV-3998). Ensure that each <pre> inside a macro
             * has a <br>
             */
            ed.onSetContent.add(function (ed) {
                tinymce.each(ed.dom.select('.wysiwyg-macro[data-macro-body-type="PLAIN_TEXT"] .wysiwyg-macro-body'), function (macroBody) {
                    var pre = ed.dom.select('pre', macroBody);
                    if (!pre.length) {
                        if (tinymce.isIE) {
                            ed.dom.setHTML(macroBody, "<pre></pre>");
                        } else {
                            ed.dom.setHTML(macroBody, "<pre><br /></pre>");
                        }
                    }
                });
            });

            /**
             * Fix to ensure that hitting ENTER inside a PRE in a macro placeholder in IE and webkit produces a BR rather than creating another PRE
             */
            if (tinymce.isWebKit || tinymce.isIE) {
                ed.onKeyPress.addToTop(function(ed, e) {
                    if (e.shiftKey || e.keyCode != VK.ENTER) {
                        return true;
                    }

                    var selection = ed.selection,
                        selectedNode = AJS.$(selection.getNode());
                    if (selectedNode.is("pre") && selectedNode.closest(".wysiwyg-macro").length) {
                        selection.setContent('<br id="__" /> ', {format : 'raw'});
                        var n = ed.dom.get('__');
                        n.removeAttribute('id');
                        selection.select(n);
                        selection.collapse();
                        return tinymce.dom.Event.cancel(e);
                    }
                });
            }

            /**
             * CONFDEV-4357
             * If the user selects a bodied macro in its entirety (by clicking its title)
             * and starts typing, the text should go inside its body.
             * Does not preserve the macro when content is pasted over it.
             */
            ed.onKeyDown.add(function(ed, e) {
                var selection = ed.selection,
                    selectedNode,
                    body;

                /**
                 * Figure out if the combination of keys pressed would result in
                 * a content-modifying operation or not.
                 * FIXME: Extraction of abstraction is the key to happiness.
                 * Find all this key-wrangling detection behaviour a nicer home.
                 */
                var ignoreKeys = [VK.BACKSPACE, VK.DELETE, VK.ENTER, VK.ESCAPE];

                function isModifierKey(e) {
                    var isModifier = false;
                    if (e.charCode === 0) {
                        isModifier = (jQuery.inArray(e.keyCode, [VK.SHIFT, VK.CTRL, VK.ALT, VK.META]) != -1);
                    }
                    return isModifier;
                }

                // Don't save the macro if a deletion action is about to occur.
                // We only need to care if there's an active selection.
                // NOTE: Pressing enter to replace the macro is also a valid approach to deleting it.
                if (jQuery.inArray(e.keyCode, ignoreKeys) != -1 || isModifierKey(e) || selection.isCollapsed()) {
                    return;
                }

                if (e.ctrlKey || e.metaKey || (!tinymce.isMac && e.altKey)) {
                    // If one of these keys is held down,
                    // it's likely to not be a content-related operation.
                    return;
                }
                /* @end key-wrangling detection behaviour */

                selectedNode = selection.getNode();

                if (tinymce.confluence.macrobrowser.isMacroWithBody(selectedNode)) {
                    var $macroBody = $(".wysiwyg-macro-body", selectedNode);

                    if ($macroBody.length > 0) {
                        AJS.debug("MacroPlaceholderPlugin: Adjusting text selection to only include macro's body.");
                        selection.select($macroBody[0], true);
                    }
                }
            });

            /**
             * Allow the user to double-click the title area of a macro placeholder to quickly open
             * the dialog to edit it.
             */
            ed.onDblClick.add(function(ed, e) {
                var $target = $(e.target),
                    macroPlaceholderEl = function ($element) {
                        return $element.closest("[data-macro-name]");
                    },
                    $macroNode = macroPlaceholderEl($target);

                if ($macroNode.length) {
                    AJS.debug("Double-click triggered inside a macro.");

                    // This prevents people double-clicking arbitrary content inside macros and getting the dialog.
                    // Only proceed if we're dealing with the container element of the macro itself.
                    // inline macros have no macro body and no macro-body-type.
                    if ($macroNode.attr("data-macro-body-type")) {
                        // Allow clicking inside the table cell in IE8 to give it a chance of allowing double-click.
                        if (tinymce.isIE) {
                            if ($target.hasClass("wysiwyg-macro-body") || $target.hasClass("wysiwyg-macro")) {
                                AJS.debug("Double-click inside macro body, but its IE so its OK I guess...");
                            } else {
                                AJS.debug("Double-click was inside bodied macro content, even in IE. Skipping.");
                                return;
                            }
                        }
                        // For every normal browser that allows padding inside tables, ignore all clicks inside the content area.
                        else if ($target.closest(".wysiwyg-macro-body").length) {
                            AJS.debug("Double-click was inside bodied macro content. Skipping.");
                            return;
                        }
                    }

                    tinymce.confluence.macrobrowser.editMacro($macroNode);
                    // Because the user has essentially bypassed the property-panel, we should close it.
                    if (AJS.Confluence.PropertyPanel && AJS.Confluence.PropertyPanel.current) {
                        AJS.Confluence.PropertyPanel.destroy();
                    }
                }
            });

            /**
             * When the user is inside a plain text macro body placeholder, we do not want to expose any of the
             * rich text controls in the browser.
             */
            ed.onNodeChange.addToTop(function(ed) {
                var selectedNode = ed.selection.getNode();
                if (selectedNode.nodeName == 'PRE' && AJS.$(selectedNode).closest('.wysiwyg-macro').length) {
                    t._setTinymceControlsState(ed, 0);
                } else {
                    t._setTinymceControlsState(ed, 1);
                }
            });

            /**
             * Override default behaviour in firefox and opera to insert a tab instead of moving focus.
             */
            if (tinymce.isGecko || tinymce.isOpera || tinymce.isIE) {
                ed.onKeyDown.add(function (ed, e) {
                    if (e.keyCode == VK.TAB) {
                        if (ed.dom.getParent(ed.selection.getNode(), '.wysiwyg-macro pre')) {
                            ed.selection.setContent('\t', {format: 'raw'});
                            return tinymce.dom.Event.cancel(e);
                        }
                    }
                });
            }

            ed.addCommand("mceConfRemoveMacro", function(macroNode) {
                $(macroNode).remove();
                if (tinymce.isGecko) {
                    // Repaint to clear the image handles from their old positions.
                    AJS.Rte.getEditor().execCommand('mceRepaint', false);
                }
            });

            /**
             * Registers a key listener in a such a way as to ensure that when a user presses and holds the key,
             * that the default action can be effectively cancelled across webkit and firefox.
             *
             * Webkit is actually not fussy: registering against keydown is sufficient.
             *
             * On Firefox, one needs to register against keypress and keyup in addition to keydown.
             *
             * @param listener a key listener
             */
            function registerKeyListener(listener) {
                ed.onKeyDown.add(listener);

                if (tinymce.isGecko) {
                    ed.onKeyPress.add(listener);
                    ed.onKeyUp.add(listener);
                }
            }

            /**
             * Determines if the node is an empty node (in the sense that it contains only a single child BR node.
             *
             * @param node the node to test
             * @param selector the specified node must pass this css selector
             */
            function nodeContainsSoloBrElementAndMatchesSelector(node, selector) {
                return node && node.childNodes.length === 1 && $(node).is(selector) && $(node.firstChild).is("br");
            }

            /**
             * Checks to see if the cursor is inside a node that is the only child of its parent (and the parent is the only
             * child of it's parent and so and so on until the placeholder body element (marked with class="wysiwyg-macro-body" is encountered).
             * If we can traverse successfully to the placeholder body element return true, otherwise return false.
             *
             * For example, for the following nested structure:
             *
             * <td class="wysiwyg-macro-body>
             *     <p>
             *         <strong>
             *             <u>
             *                 <em>CURSOR HERE text<br/></em>
             *             </u>
             *         </strong>
             *     </p>
             * </td>
             *
             * Will return true if the cursor is there.
             *
             * Note that:
             * <ul>
             *     <li>the cursor is inside an element that is the only child of its parent, and the parent is also the only child of it's parent, and so and so forth
             *     <li>the EM element that we are testing has to be the only child of it's parent. However, the EM element itself is permitted to have multiple children.
             * </ul>
             *
             * @param range
             */
            function isCursorInMostNestedElement(range) {
                if (!range.collapsed) {
                    return false;
                }

                var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                if (!node || !node.parentNode) {
                    return false;
                }

                do {
                    if (node.previousSibling || node.nextSibling) { // if node is only child (don't use :only-child as it does not respect text nodes)
                        return false;
                    }
                    node = node.parentNode;
                } while (node && !$(node).is(".wysiwyg-macro-body"));

                return true;
            }
            t._isCursorInMostNestedElement = isCursorInMostNestedElement;

            /**
             * Prevent the last empty P or PRE in a placeholder from being deleted (emulating MS word behaviour)
             */
            (function () {
                (tinymce.isWebKit || tinymce.isGecko) && registerKeyListener(function (ed, e) {
                    if (e.keyCode == 46 && isCursorInLastParagraphOrPre(ed.selection.getRng(true))) {
                        return tinymce.dom.Event.cancel(e);
                    }
                });

                function isCursorInLastParagraphOrPre(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                    return node.previousSibling && !node.nextSibling && nodeContainsSoloBrElementAndMatchesSelector(node, "p, pre");
                }
                t._isCursorInLastParagraphOrPre = isCursorInLastParagraphOrPre;
            })();

            /**
             * This will prevent a delete or backspace key event from removing the whole placeholder when the cursor is inside
             * a nested element. For example:
             *
             * <table ...>
             *     ...
             *     <td class="wysiwyg-macro-body>
             *         <p>
             *             <strong><em><u>CURSOR HERE<br/></u></em></strong>
             *         </p>
             *     </td>
             *     ...
             * </table>
             *
             * This does does not prevent backspace when the cursor is inside an a LI element intentionally
             * (as we would prefer to allow the user to outdent in this scenario)
             */
            (function () {
                tinymce.isWebKit && registerKeyListener(function (ed, e) {
                    if (e.keyCode != 8 && e.keyCode != 46) {
                        return true;
                    }

                    var range = ed.selection.getRng(true),
                        node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                    if (isCursorInNestedElementContainingSoloBr(range)
                            && (e.keyCode == 8 ? !$(node).is("li") : true) /* LI and keyCode 8 is handled by another handler */) {
                        return tinymce.dom.Event.cancel(e);
                    }

                    return true;
                });

                function isCursorInNestedElementContainingSoloBr(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                    return isCursorInMostNestedElement(range) && node.childNodes.length === 1 && $(node.firstChild).is("br");
                }
            })();

            /**
             * Pressing delete when inside a bullet item inside a table cell should remove the bullet item.
             *
             * Currently, if you are in a placeholder, hitting delete removes the whole placeholder including the bullet.
             *
             * Also, if you are inside a table cell in a normal table, hitting delete will not remove the bullet but just
             * move the cursor to the previous cell.
             *
             * <table ...>
             *     ...
             *     <td>
             *         <p>
             *             <ul><li>CURSOR HERE<br/></li></ul>
             *         </p>
             *     </td>
             *     ...
             * </table>
             *
             * This does not handle nested LI elements intentionally (as we would prefer to allow the user to unindent in this scenario)
             */
            (function () {
                tinymce.isWebKit && registerKeyListener(function (ed, e) {
                    if (e.keyCode != 8) {
                        return true;
                    }

                    if (isCursorInsideMacroPlacholderInsideListItemContainingSoloBr(ed.selection.getRng(true))) {
                        ed.execCommand("Outdent");
                        return tinymce.dom.Event.cancel(e);
                    }

                    return true;
                });

                function isCursorInsideMacroPlacholderInsideListItemContainingSoloBr(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                    return range.startOffset === 0 && $(node).is(".wysiwyg-macro-body > ul > li") && node.childNodes.length === 1 && $(node.firstChild).is("br");
                }
                t._isCursorInsideMacroPlacholderInsideListItemContainingSoloBr = isCursorInsideMacroPlacholderInsideListItemContainingSoloBr;
            })();

            /**
             * Webkit removes the whole table-based placeholder when its body only contains the following:
             *
             * <p>CURSOR HERE<br/></p>
             * <p><br/></p>
             *
             * And the user hits delete
             */
            (function () {
                tinymce.isWebKit && registerKeyListener(function(ed, e) {
                    if (e.keyCode != 46) {
                        return true;
                    }

                    var range = ed.selection.getRng(true),
                        node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    if (isCursorInFirstParagraphWhenThereAreTwoEmptyParagraphs(range)) {
                        var nextSibling = node.nextSibling;
                        $node.remove();
                        ed.selection.select(nextSibling, true);

                        return tinymce.dom.Event.cancel(e);
                    }
                });

                function isCursorInFirstParagraphWhenThereAreTwoEmptyParagraphs(range) {
                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    return range.startOffset === 0
                            && nodeContainsSoloBrElementAndMatchesSelector(node, "p, pre")
                            && $node.parent().is("td.wysiwyg-macro-body")
                            && !node.previousSibling
                            && node.nextSibling
                            && nodeContainsSoloBrElementAndMatchesSelector(node.nextSibling, "p, pre");
                }
                t._isCursorInFirstParagraphWhenThereAreTwoEmptyParagraphs = isCursorInFirstParagraphWhenThereAreTwoEmptyParagraphs
            })();

            /**
             * Webkit removes the whole table-based placeholder when its body only contains the following:
             *
             * <p><br/></p>
             * <p>CURSOR HERE<br/></p>
             *
             * And the user hits backspace.
             */
            (function () {
                tinymce.isWebKit && registerKeyListener(function(ed, e) {
                    if (e.keyCode != 8) {
                        return true;
                    }

                    var range = ed.selection.getRng(true),
                        node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    if (isCursorInSecondParagraphWhenThereAreTwoEmptyParagraphs(range)) {
                        var previousSibling = node.previousSibling;
                        $node.remove();
                        ed.selection.select(previousSibling, true);

                        return tinymce.dom.Event.cancel(e);
                    }
                });

                function isCursorInSecondParagraphWhenThereAreTwoEmptyParagraphs(range) {
                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    return range.startOffset === 0 && nodeContainsSoloBrElementAndMatchesSelector(node, "p, pre")
                        && $node.parent().is("td.wysiwyg-macro-body")
                        && !node.nextSibling // is this the last child
                        && node.previousSibling
                        && !node.previousSibling.previousSibling // the previousSibling has not previousSibling, meaning this will determine whether the previousSibling is in fact the first child
                        && nodeContainsSoloBrElementAndMatchesSelector(node.previousSibling, "p, pre");
                }
                t._isCursorInSecondParagraphWhenThereAreTwoEmptyParagraphs = isCursorInSecondParagraphWhenThereAreTwoEmptyParagraphs
            })();

            /**
             * Do not allow a single <p><br/></p> or <pre><br/></pre> inside a macro placeholder body to be deleted by backspace or delete
             */
            (function () {
                (tinymce.isWebKit || tinymce.isGecko) && registerKeyListener(function (ed, e) {
                    if (e.keyCode != 8 && e.keyCode != 46) {
                        return true;
                    }

                    if (isCursorInSoloParagraphOrPreInsidePlaceholder(ed.selection.getRng(true))) {
                        return tinymce.dom.Event.cancel(e);
                    }
                });

                function isCursorInSoloParagraphOrPreInsidePlaceholder(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    if (!node || range.startOffset > 0) {
                        return false;
                    }

                    /**
                     * this will remove any empty text nodes that proceed the BR element (I've personally encountered when I outdented a single bullet item inside a placeholder)
                     * Empty text nodes interfere with the :only-child check that we will perform in the rest of the logic in this function
                     */
                    node.normalize();

                    return node && range.startOffset === 0
                            && nodeContainsSoloBrElementAndMatchesSelector(node, "p, pre")
                            && $node.parent().is("td.wysiwyg-macro-body")
                            && !node.previousSibling && !node.nextSibling; // p or pre is the only child
                }
                t._isCursorInSoloParagraphOrPreInsidePlaceholder = isCursorInSoloParagraphOrPreInsidePlaceholder;
            })();

            /**
             * Webkit deletes the whole table-based placeholder when the last character or element (excluding the BR) is removed via
             * backspace or delete.
             *
             * Firefox inserts a <BR/> element outside of the P when the last character is deleted. That is you get:
             *
             * <td>
             *     <p><br/></p>
             *     <br/>
             * </td>
             *
             * This is problematic markup and its confusing behaviour.
             *
             * The following code checks for the condition where this is only one text node or one element left
             * and manually performs the removal of the node and cancels the backspace or delete key event (hence bypassing the
             * default browser handling which causes the above mentioned problems).
             */
            (function () {
                (tinymce.isWebKit || tinymce.isGecko) && registerKeyListener(function(ed, e) {
                    if (e.keyCode != 8 && e.keyCode != 46) {
                        return true;
                    }

                    var range = ed.selection.getRng(true),
                        node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer;

                    node.normalize();

                    if ((e.keyCode == 46 && (isCursorBehindOnlyCharacterInNestedElement(range) || isCursorBehindOnlyChildInNestedElement(range)))
                            || (e.keyCode == 8 && (isCursorAfterOnlyCharacterInNestedElement(range) || isCursorAfterOnlyChildInNestedElement(range)))) {

                        if (node.lastChild && !$(node.lastChild).is("br")) {
                            $(node).append("<br/>"); // this is required so we have a place to put the cursor
                        }

                        $(node.firstChild).remove();
                        ed.selection.select(node, 1);

                        return tinymce.dom.Event.cancel(e);
                    }

                    return true;
                });

               function isCursorBehindOnlyCharacterInNestedElement(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer;

                    return node && range.startOffset === 0
                            && node.nodeType == 3
                            && isCursorInMostNestedElement(range)
                            && node.nodeValue.length === 1
                            && !node.previousSibling && !node.nextSibling;
                }

                function isCursorAfterOnlyCharacterInNestedElement(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer;

                    return node && range.startOffset === 1
                            && node.nodeType == 3
                            && isCursorInMostNestedElement(range)
                            && node.nodeValue.length === 1
                            && !node.previousSibling && !node.nextSibling;
                }

                function isCursorBehindOnlyChildInNestedElement(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer;

                    return node && range.startOffset === 0
                            && node.nodeType == 1
                            && isCursorInMostNestedElement(range)
                            && !node.previousSibling && !node.nextSibling;
                }

                function isCursorAfterOnlyChildInNestedElement(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer;

                    return node && range.startOffset === 1
                            && node.nodeType == 1
                            && isCursorInMostNestedElement(range)
                            && !node.previousSibling && !node.nextSibling;
                }

                t._isCursorBehindOnlyCharacterInNestedElement = isCursorBehindOnlyCharacterInNestedElement;
                t._isCursorAfterOnlyCharacterInNestedElement = isCursorAfterOnlyCharacterInNestedElement;
                t._isCursorBehindOnlyChildInNestedElement = isCursorBehindOnlyChildInNestedElement;
                t._isCursorAfterOnlyChildInNestedElement = isCursorAfterOnlyChildInNestedElement;
            })();

            (function () {
                tinymce.isWebKit && registerKeyListener(function(ed, e) {
                    if (e.keyCode != 8 && e.keyCode != 46) {
                        return true;
                    }

                    var range = ed.selection.getRng(true);
                    if (isMacroBodySelected(range)) {
                        var $macroBody = $(range.startContainer).closest(".wysiwyg-macro-body");

                        if ($macroBody.length > 0) {
                            var isPlainTextMacro = $($macroBody[0].firstChild).is("pre"); //plain text macros will always have one or more PRE elements

                            ed.undoManager.add();
                            $macroBody.empty();

                            var $container;
                            if (isPlainTextMacro) {
                                $container = $("<pre><br/></pre>").appendTo($macroBody);
                            } else {
                                $container = $("<p><br/></p>").appendTo($macroBody);
                            }

                            ed.selection.select($container[0], true);

                            return tinymce.dom.Event.cancel(e);                            
                        }
                    }

                    return true;
                });

                function isMacroBodySelected(range) {
                    if (range.collapsed) {
                        return false;
                    }

                    var isFirstChild = function (n) {
                        return !n.previousSibling;
                    };
                    var isLastChild = function (n) {
                        return !n.nextSibling;
                    };

                    return range.startOffset === 0
                            && isMacroBodyElementReachableViaSelector(range.startContainer, isFirstChild)
                            && isOffsetAtEndOfNode(range.endContainer, range.endOffset)
                            && isMacroBodyElementReachableViaSelector(range.endContainer, isLastChild);
                }
                t._isMacroBodySelected = isMacroBodySelected;

                /**
                 * Returns true if the macro body element can be reached from the specified node.
                 *
                 * SPECIAL CASE:
                 *
                 * Return true if the specified node "is" the macro body element.
                 *
                 * @param node the node
                 * @param f function used to test parent nodes on the way to the macro body element
                 */
                function isMacroBodyElementReachableViaSelector(node, f) {
                    if (!node) {
                        return false;
                    }

                    if ($(node).is(".wysiwyg-macro-body")) {
                        return true;
                    }

                    do {
                        if (!f(node)) {
                            return false;
                        }
                        node = node.parentNode;
                    } while (node && !$(node).is(".wysiwyg-macro-body"));

                    return true;
                }

                function isOffsetAtEndOfNode(node, offset) {
                    if (!node) {
                        return false;
                    }

                    if (node.nodeType == 3) {
                        return offset === node.nodeValue.length;
                    } else if (node.nodeType === 1) {
                        if (node.childNodes.length === 1 && $(node.firstChild).is("br")) {
                            return offset === 0; // SPECIAL CASE: Return true if the endContainer is a node containing a BR like <XXX><br/></XXX>, where the endOffset is 0.
                        } else {
                            return offset === node.childNodes.length;
                        }
                    } else {
                        return false;
                    }
                }

            })();

            /**
             * Prevent the delete key when the cursor is:
             * ... miscellaneous content ...
             * <p>CURSOR HERE<br/></p>
             * ... placeholder markup ...
             *
             * If we don't, webkit will "select" the placeholder for a split second (you will see light blue cover
             * face of the placeholder, and then the selection will go away and the cursor is no where to be seen.
             * Focus is lost from the editor.
             */
            (function () {
                tinymce.isWebKit && registerKeyListener(function (ed, e) {
                    if (e.keyCode == 46 && isCursorInEmptyParagraphPrecedingPlaceholder(ed.selection.getRng(true))) {
                        return tinymce.dom.Event.cancel(e);
                    }
                });

                function isCursorInEmptyParagraphPrecedingPlaceholder(range) {
                    if (!range.collapsed) {
                        return false;
                    }

                    var node = range.startContainer.nodeType == 3 ? range.startContainer.parentNode : range.startContainer,
                        $node = $(node);

                    return range.startOffset === 0 && nodeContainsSoloBrElementAndMatchesSelector(node, "p") && $node.next().is("table");
                }
                t._isCursorInEmptyParagraphPrecedingPlaceholder = isCursorInEmptyParagraphPrecedingPlaceholder;
            })();

            // CONFDEV-4248
            (function() {
                tinymce.isWebKit && registerKeyListener(function(ed, e) {
                    var currentRange = ed.selection.getRng();

                    if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.keyCode == VK.LEFT) {
                        var range = _selectTillStart(currentRange);
                        if (range !== currentRange) {
                            ed.selection.setRng(range);
                            return tinymce.dom.Event.cancel(e);
                        }
                    }
                });

                function _findTextContainer(node) {
                    if (!node || $(node.parentNode).is(".wysiwyg-macro td")) {
                        return null;
                    }
                    if ($(node.parentNode).is("p")) {
                        return node.parentNode;
                    }

                    return _findTextContainer(node.parentNode);
                }

                function _findFirstTextNode(node) {
                    if (!node) {
                        return null;
                    }
                    if (node.nodeType == 3) {
                        return node;
                    }

                    return _findFirstTextNode(node.firstChild) || _findFirstTextNode(node.nextSibling);
                }

                function _findNearestPrecedingBr(node) {
                    if (!node || node.nodeName == "PRE" || node.nodeName == "TD") {
                        return null;
                    }
                    if (node.nodeName == "BR") {
                        return node;
                    }

                    return _findNearestPrecedingBr(node.previousSibling) || _findNearestPrecedingBr(node.parentNode);
                }

                /**
                 * webkit, at the time of writing, selects too much when a user hits CMD+SHIFT+LEFT inside the macro body.
                 * Instead of selecting just text, webkit will also include the placeholder in the selection.
                 * If the user deletes, the whole placeholder is deleted instead.
                 *
                 * To resolve this, we modify the start of the range on the fly.
                 * If there are any BR tags preceding the cursor, we want to fall back to the browser's default handling.
                 */
                function _selectTillStart(range) {
                    var firstTextNode, $pre;

                    if ($(range.commonAncestorContainer).closest(".wysiwyg-macro td").length > 0) {
                        if (!_findNearestPrecedingBr(range.startContainer)) {
                            firstTextNode = _findFirstTextNode(_findTextContainer(range.startContainer));
                        }
                    }

                    if (($pre = $(range.commonAncestorContainer).closest(".wysiwyg-macro td > pre")).length > 0) {
                        if (!_findNearestPrecedingBr(range.startContainer)) {
                            firstTextNode = _findFirstTextNode($pre[0]);
                        }
                    }

                    if (firstTextNode) {
                        range = range.cloneRange();
                        range.setStart(firstTextNode, 0);
                    }

                    return range;
                }

                t._selectTillStart = _selectTillStart; // expose for testing
            })();

		},

        _setTinymceControlsState: function (ed, state) {
            tinymce.each(ed.controlManager.controls, function(c) {
                c.setDisabled(!state);
            });
        },

		getInfo : function() {
			return {
				longname : 'Macro Place Holder plugin',
				author : 'Atlassian',
				authorurl : 'http://www.atlassian.com',
				version : "1.0"
			};
		}
	});

	tinymce.PluginManager.add('macroplaceholder', tinymce.plugins.MacroPlaceHolderPlugin);
})(AJS.$);

(function(tinymce) {
	tinymce.create('tinymce.plugins.InsertWikiMarkupPlugin', {
		init : function(ed, url) {
			// Register commands
			ed.addCommand('InsertWikiMarkup', function() {
			    var settings = {
                        id: "insert-wiki-markup-dialog",
			            content : "<form action='#' method='post'><textarea class='monospaceInput' id='insertwikitextarea' name='wikitext'/></form>",
			            width : 800,
			            height : 486,
			            popup : false,
			            name : "confluence.conf_wikimarkup"
			    };

			    var dialogId = "";

			    var successfulConversion = function(data, textStatus, XMLHttpRequest) {
			        ed.windowManager.setBusy(dialogId,false);
			        
		            tinymce.EditorManager.activeEditor.windowManager.close(dialogId);
		            ed.execCommand('mceInsertContent', false, data);
                    splitParas(ed);
			    };

                var erroredConversion = function(XMLHttpRequest, textStatus, errorThrown) {
                    ed.windowManager.setBusy(dialogId, false);
                    
                    var alertStr = "<p class='warning'>" + tinymce.EditorManager.activeEditor.getLang("confluence.conf_wikimarkup_errors") + "</p>";
                    alertStr += "<p class='exception-report'><span class='exceptionMessage'>";
                    if ("timeout" == textStatus) {
                        alertStr += tinymce.EditorManager.activeEditor.getLang("confluence.conf_wikimarkup_timeout");
                    }
                    else {
                        alertStr = alertStr + AJS.escapeHtml(textStatus) + " : " + AJS.escapeHtml(errorThrown);
                    }
                        
                    alertStr += "</span></p>";
                    
                    tinymce.EditorManager.activeEditor.windowManager.alert(alertStr);
                };

	             var convert = function() {
	                    ed.windowManager.setBusy(dialogId, true);
	                    var conversionData = {
	                            wiki : AJS.$("#insertwikitextarea").val(),
	                            entityId :  AJS.Meta.get("content-id"),
	                            spaceKey : AJS.Meta.get("space-key")
	                    };

	                    AJS.$.ajax( {
	                        type : "POST",
	                        contentType : "application/json; charset=utf-8",
	                        url : AJS.params.contextPath + "/rest/tinymce/1/wikixhtmlconverter",
	                        data : AJS.$.toJSON(conversionData),
	                        dataType : "text", // if this is switched back to json be sure to use "text json". See CONFDEV-4799 for details.
	                        success : successfulConversion,
	                        error : erroredConversion,
	                        timeout: 45000
	                    });

	                };

                var params = {
                        buttons : [{
                            label : "confluence.conf_insert_button_title",
                            action : convert
                        }],
                        plugin_url : url,
                        helpLink : "help.insert.wiki.markup",
                        hintText : "confluence.conf_wikimarkup_hint",
                        cssClass : "insertwikimarkuppanel"
                };

                dialogId = ed.windowManager.open(settings, params);
            });

            ed.addButton('insertwikimarkup', {title : 'confluence.conf_wikimarkup', cmd : 'InsertWikiMarkup'});
            
            var splitParas = function(editor, params) {
                tinymce.each(editor.dom.select('p > p'), function (p) {
                    try {
                        editor.dom.split(p.parentNode, p);
                    } catch (ex) {
                        // IE can sometimes fire an unknown runtime error so we just ignore it
                    }
                });                
            };
		},

		getInfo : function() {
			return {
				longname : 'InsertWikiMarkip',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                infourl : 'http://www.atlassian.com/',
				version : "1.0"
			};
		}
	});

	// Register plugin
	tinymce.PluginManager.add('insertwikimarkup', tinymce.plugins.InsertWikiMarkupPlugin);
})(tinymce);

/**
 * Confluence property panels for images and links.
 */
(function($) {
    var handlers = [];
    AJS.bind("add-handler.property-panel", function (e, data) {
        data && handlers.push(data);
    });
    tinymce.create('tinymce.plugins.PropertyPanel', {
        init : function(ed) {
            /*
              A single selection event might have several contexts:
              - No current property panel
              -- Selected element that DOESN'T fire a panel
              -- Selected element that DOES fire a panel
              - Current property panel
              -- Selected element that DOESN'T fire a panel
              -- Selected element that DOES fire a panel
              --- Selected the anchor of the CURRENT property panel
              --- Selected a different element that DOES fire a panel
              ---- Selected a different element that DOES fire a panel of the SAME type
              ---- Selected a different element that DOES fire a panel of a DIFFERENT type

              To begin with it will be the responsibility of the element-specific code (img/a/etc) to determine the context;
              if enough overlap between IMG and A is found it can be centralized here.
             */
            AJS.log("init property panel plugin");
            var boundElement,
            isToolBarShowing = function() {
                return !!$(".toolbar-contextual").length;
            },
            isElementInTable = function(el) {
              return !!el.closest("table:not(.wysiwyg-macro,.editor-inline-macro)").length;
            },
            isRightClick = function (e) {
                if (e.type != "click") return false;
                if (e.which)  return (e.which == 3);
                if (e.button) return (e.button == 2);
            },
            getClosestAnchor = function($element) {
                if ($element.is("img")) {
                    return $element[0];
                }

                var closest = $element.closest("a,table,.wysiwyg-macro,.editor-inline-macro");
                if (closest.length) {
                    return closest[0];
                }

                return "";
            },
            handler = function (ed, e, focusedEl) {
                var data = {
                        focusedEl: focusedEl,
                        focusedNodeName: focusedEl.nodeName && focusedEl.nodeName.toLowerCase(),
                        ed: ed,
                        e: $.extend({},e) //lookds odd, but is needed for what appears to be a GC bug in ie8/9
                    };
                var containerEl = getClosestAnchor($(focusedEl)),
                isInTable = isElementInTable($(focusedEl)),
                toolbarActive = isToolBarShowing();
                data.containerEl = containerEl;

                AJS.trigger("user-blurred-rte-element", data);
                if (containerEl && !isRightClick(e) && !isPanelShowing() &&  AJS.Confluence.PropertyPanel.shouldCreate) {
                    for (var i = 0, len = handlers.length; i < len; i++) {
                        if (handlers[i].canHandleElement($(containerEl))) {
                            AJS.log("PropertyPanel.create: Creating a panel for: " + handlers[i].name);
                            if(toolbarActive || !isInTable) {
                               return handlers[i].handle(data);
                            }
                            //this now means that you get no return.
                            //if this becomes a problem we could use a promise
                            (function(handler) {
                                AJS.$(document).bind("shown.contextToolbar",function(e) {
                                    handler.handle(data);
                                });
                            })(handlers[i]);
                        }
                    }
                }
            },
            isPanelShowing = function () {
                return !!AJS.Confluence.PropertyPanel.current;
            };

            /* All versions of IE apply drag/resize handles
             * (those white dots along the edges of elements)
             * to select elements in contentEditable regions that prevent the normal click
             * event from propagating.
             * Here, we're going to work around that fact.
             */
            if(tinymce.isIE) {
                ed.onMouseDown.add(function(ed, e) {
                    // Avoid memory leaks; kill any previous bound events.
                    // Bound to a custom namespace to avoid clobbering other click events.
                    boundElement && boundElement.unbind('mouseup.IEDragHandlesWorkaround');
                    boundElement = AJS.$(e.target);
                    // IE's drag/resize handles are applied to tables and images.
                    boundElement.filter("img, table").bind('mouseup.IEDragHandlesWorkaround', function() {
                        handler(ed, e, e.target);
                    });
                });
            }

            AJS.bind("trigger.property-panel", function(e, data) {
                handler(ed, e, data.elem);
            });

            ed.onClick.add(function(ed, e) {
                handler(ed, e, e.target);
            });
            ed.onKeyUp.addToTop(function (ed,e,o) {
                if (e.keyCode === 27) return; // ignore esc, it's handled by keydown listener
                handler(ed, e, ed.selection.getNode());
            });
            ed.onContextMenu.add(function() { // destroy property panel when a context menu is shown
                AJS.Confluence.PropertyPanel.destroy();
            });

            AJS.bind("user-blurred-rte-element", function (e, data) {
                if (isPanelShowing()) {
                    if (!data.containerEl || AJS.Confluence.PropertyPanel.current.hasAnchorChanged(data.containerEl)) {
                        AJS.Confluence.PropertyPanel.destroy();
                    } else {
                        AJS.trigger("same-anchor.property-panel");
                    }
                }
            });
            AJS.bind("created.property-panel", function (e, data) {               
                AJS.Editor.Adapter.bindScroll("property-panel", function (e) {
                    AJS.Confluence.PropertyPanel.destroy();
                });
                AJS.$(ed.getDoc()).bind("keydown.property-panel.escape", function (e) {
                    if (e.keyCode === 27 && isPanelShowing()) { // esc key
                        AJS.Confluence.PropertyPanel.destroy();
                    }
                });
            });
            AJS.bind("destroyed.property-panel", function (e, data) {
                AJS.Editor.Adapter.unbindScroll("property-panel");
                AJS.$(ed.getDoc()).unbind("keydown.property-panel.escape");
            });
        },

        getInfo : function() {
            return {
                longname : 'Image, Link and Macro Property Panels',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    // Register plugin
    tinymce.PluginManager.add('propertypanel', tinymce.plugins.PropertyPanel);
})(AJS.$);

/*
 * Monospace formatting in the Editor "More Menu"
 */

(function($) {
	tinymce.create('tinymce.plugins.ConfMonospacePlugin', {
        init : function(ed, url) {
			// Register commands
			ed.addCommand('confMonospace', function() {
				ed.formatter.toggle("monospace", undefined);
			});

            // Handle node change updates
            ed.onNodeChange.add(function(ed, cm, n) {
                cm.setActive('monospace', ed.formatter.match("monospace"));    
            });

            tinymce.activeEditor.onInit.add(function(ed) {
                ed.formatter.register({monospace : {inline : 'code'}});
            });

			// Register button
			ed.addButton('monospace', {title : 'monospace', cmd : 'confMonospace'});
		},

        getInfo : function() {
            return {
                longname : 'Monospace Formatting',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    // Register plugin
    tinymce.PluginManager.add("confmonospace", tinymce.plugins.ConfMonospacePlugin);
})();
/*
 * Extracted out our version of the charmap (from advanced theme).
 * It is easier to manage and upgrade tinymce this way.
 */
(function($) {
	tinymce.create('tinymce.plugins.ConfCharmapPlugin', {
        init : function(ed, url) {
			// Register commands
			ed.addCommand('confCharmap', function() {

                ed.windowManager.open({
                    id: "insert-char-dialog",
                    url : tinyMCE.settings.plugin_action_base_path + '/charmap.action',
                    width : 610 + parseInt(ed.getLang('advanced.charmap_delta_width', 0)),
                    height : 300 + parseInt(ed.getLang('advanced.charmap_delta_height', 0)),
                    inline : true,
                    name: "advanced.charmap_desc"
                });

			});
		},

        getInfo : function() {
            return {
                longname : 'Confluence Charmap Plugin',
                author : 'Atlassian',
                authorurl : 'http://www.atlassian.com',
                version : tinymce.majorVersion + "." + tinymce.minorVersion
            };
        }
    });

    // Register plugin
    tinymce.PluginManager.add("confcharmap", tinymce.plugins.ConfCharmapPlugin);
})();
(function($){
/**
 * Implement the methods required by the com.atlassian.confluence.plugin.editor.Editor interface, adapting to a
 * TinyMCE editor implementation.
 *
 * Note that tinyMCE should not be used as it gets confused with the tinymce object. Use tinymce.EditorManager if that's
 * what you need.
 */
AJS.Editor.Adapter = {

    /**
     * This is still here cause i'm paranoid that this library is still being used somwhere and we are building 4.0 soon.
     *
     * @deprecated since 4.0. Please use tinymce range apis instead
     */
    IERange: AJS.Rte.Range.IERange,

    // Used to ensure that a TextNode exists under the search-text span when the ranges are set.
    HIDDEN_CHAR: "\ufeff",
    ZERO_WIDTH_WHITESPACE: "&#x200b;",

    /**
     * Stores the currently selected range and scroll position of the editor so we can get back to to it later.
     * @deprecated use AJS.Rte.BookmarkManager.storeBookmark();
     */
    storeCurrentSelectionState : AJS.Rte.BookmarkManager.storeBookmark,

    /**
     * Moves the scroll position and selected range in the editor back to where it used to be.
     * @deprecated use AJS.Rte.BookmarkManager.restoreBookmark();
     */
    restoreSelectionState : AJS.Rte.BookmarkManager.restoreBookmark,

    // put the text in newValue into the editor. This is called when the editor needs new
    // content -- it is *not* called to set the initial content. That should be done either by providing the
    // editor with the content as part of the initial HTML, or by calling javascript from editorOnLoad()
    setEditorValue : function (newValue) {
        AJS.log("AJS.Editor.Adapter.setEditorValue called with :" + newValue);
        if (newValue) {
            AJS.Rte.getEditor().setContent(newValue);
        }
    },

    /*
     *  If TinyMCE has not finished loading the page content, and the user switches tabs (from rich text to markup),
     *  block the switch, otherwise their content will be lost. (CONF-4824)
     */
    // return true if the editor is in a state where changes from rich text to markup and vice versa are allowed
    allowModeChange : function () {
        return this._tinyMceHasInit;
    },

    // return the current HTML contents of the editor. This *must* return a JavaScript string,
    // not a JavaObject wrapping a java.lang.String
    getEditorHTML : function () {
        return "" + AJS.Rte.getEditor.getContent();
    },

    // return true if the contents of the editor has been modified by the user since
    // the last time editorResetContentChanged()
    editorHasContentChanged : function () {
        return AJS.Rte.getEditor().isDirty();
    },

    // called to reset the contents change indicator
    editorResetContentChanged : function () {
        AJS.Rte.getEditor().setDirty(false);
    },

    /**
     * Adds a callback that will be executed after this editor instance has been initialised.
     * @param callback
     */
    addOnInitCallback: function(callback) {
        if ($.isFunction(callback)) {
            if (this._tinyMceHasInit) {
                callback();
            }
            else {
                AJS.bind("init.rte", callback);
            }
        } else {
            throw new Error('Attempt made to register an oninit callback that is not a function. Received: ' + callback);
        }
    },

    /**
     * Binds a namespaced callback to the editor scroll event.
     * @param namespace used for unbinding e.g. property-panel
     * @param callback the function to run when the event occurs
     */
    bindScroll: function(namespace, callback) {
        AJS.log("bind scroll for ed");
        var ed = tinyMCE.activeEditor;
        $(document).add(ed.getDoc()).add(ed.getWin()).bind("scroll." + namespace, callback);
    },

    /**
     * Unbinds a namespace bound to the RTE window scroll event
     * @param namespace
     */
    unbindScroll: function(namespace) {
        AJS.log("unbind scroll for ed");
        var ed = tinyMCE.activeEditor;
        $(document).add(ed.getDoc()).add(ed.getWin()).unbind("scroll." + namespace);
    },

    /**
     * Non interface methods & variables
     */
    _tinyMceHasInit : false,
    _tinymcePluginInits : [],

    /**
     * Returns a reference to the main editor instance.
     */
    getMainEditor: function () {
        var e = tinymce.EditorManager.editors[AJS.Editor.Adapter.settings.editor_id];
        if (!e) {
            throw new Error("Main editor has not been initialised yet and is therefore not accessible via tinymce.EditorManager.editors");
        }
        return e;
    },

    getTinyMceHasInit: function () {
        return this._tinyMceHasInit;
    },
    /**
     * Returns the current active editor
     * @deprecated use AJS.Rte.getEditor()
     */
    getEditor : AJS.Rte.getEditor,
    /**
     * Returns the element atlassian draws the editor into
     */
    getEditorContainer : function () {
        return $("#wysiwyg");
    },

    /**
     * Returns the iframe containing the current active editor
     */
    getEditorFrame: function() {
        return $("#" + AJS.Rte.getEditor().id + "_ifr")[0];
    },
    /**
     * Returns a jquery object containing a table that houses the current active editor iframe
     */
    getEditorTable: function() {
        return $("#" + AJS.Rte.getEditor().id + "_tbl");
    },

    tinyMceOnInit : function() {
        AJS.log("Adapter:tinyMceOnInit oninit callback");
        this._tinyMceHasInit = true;
        var ed = AJS.Rte.getEditor();
        var ie7detected = $.browser.msie && (parseInt($.browser.version) == 7);
        var editingComment = AJS.params.contentType == "comment" && AJS.params.editorMode == "richtext";

        AJS.trigger("init.rte",{editor: ed});

        // Initial IE range -> W3C DOM Range handler
        this.IERange && this.IERange.setupSelection(ed.getDoc());

        // For some reason we need to set the focus after the rest of the stack has executed, or the cursor
        // will not be rendered in Firefox. Note: We tried setting the focus to the title for create page,
        // however this caused problems in Safari, see CONFDEV-868.
        if (!(ie7detected && editingComment)) {
            setTimeout(function() {
               ed.focus();
            }, 0);
        }

        // Ensures that the object resizing opts are disabled from the beginning.
        if (tinymce.isGecko) {
            var doc = ed.getDoc(), b = ed.getBody(), settings = ed.settings;

            if (!settings.readonly) {

                // Setting the contentEditable off/on seems to force caret mode in the editor and enabled auto focus (FF is now using contenteditable)
                b.contentEditable = false;
                b.contentEditable = true;

                // Doing the contentEditor hack about causes the content to be selected, so deselect it
                var sel = AJS.Rte.getEditor().selection;
                var rng = sel.getRng();
                rng.setEnd(rng.startContainer, 0);
                sel.setRng(rng);

                try {
                    // Try new Gecko method
                    doc.execCommand("styleWithCSS", 0, false);
                } catch (ex) {
                    // Use old method
                    if (!ed._isHidden())
                        try {doc.execCommand("useCSS", 0, true);} catch (ex) {}
                }

                if (!settings.table_inline_editing)
                    try {doc.execCommand('enableInlineTableEditing', false, false);} catch (ex) {}

                if (!settings.object_resizing)
                    try {doc.execCommand('enableObjectResizing', false, false);} catch (ex) {}
            }
        }
    },

    /**
     * Returns the text currently selected in the RTE
     */
    getSelectedText : function(){
        var selection = tinyMCE.activeEditor.selection;
        return selection.getRng().text || (selection.getSel() && selection.getSel().toString && selection.getSel().toString()) || "";
    },

    /**
     * Returns the offset of the element in relation to the parent frame.
     * @param element
     */
    offset: function (element) {
        var $node = $(element),
            // offset of the element in relation to the frame
            offset = $node.offset(),
            // offset of the editor
            frame = $(this.getEditorFrame()),
            frameOffset = frame.offset(),
            // Needed because IE doesn't like $body.scrollTop/Left()
            frameDoc = frame[0].contentWindow.document,
            $body = $(tinyMCE.activeEditor.getBody()),
            scrollOffset = {
                top : (jQuery.support.boxModel && frameDoc.documentElement.scrollTop  || $body.scrollTop()),
                left : (jQuery.support.boxModel && frameDoc.documentElement.scrollLeft  || $body.scrollLeft())
            };
        return {
            top: offset.top - scrollOffset.top + frameOffset.top,
            left: offset.left - scrollOffset.left + frameOffset.left
        };
    },

    /**
     * @deprecated since 4.0 tinymce now provides better range api. use tinymce.dom.Selection.getRng(true) instead
     * as it returns a  w3c range
     */
    getRange : function () {
        if (this.IERange)
            return this.IERange.getSelection().getRangeAt(0);

        return tinyMCE.activeEditor.selection.getRng();
    },

    /**
     * @deprecated since 4.0 tinymce now provides better range api. use tinymce.DOM.createRng()
     */
    createRange : function () {
        var doc = tinyMCE.activeEditor.getDoc();

        if (this.IERange && this.IERange.createRange)
            return this.IERange.createRange(doc);

        return doc.createRange();
    },

    /**
     * Replaces the element with the given text, which may be empty.
     * If the collapse parameter is true, the range will be collapsed at the end of the text.
     * If the given text IS empty, it will always be collapsed.
     * @param element  jQuery-wrapped element to replace
     * @param text  string to replace autocomplete with, if undefined a blank string is used
     * @param collapseToEnd if true, collapse range to end of text, else select text
     */
    replaceWithTextAndGetRange: function(element, text, collapseToEnd) {
        if (!element.length) {
            AJS.log("replaceWithTextAndGetRange Error: attempting to replace a non-existent element");
            return;
        }
        text = text || "";
        collapseToEnd = collapseToEnd || !text;

        var ed = AJS.Rte.getEditor(), rng;
        var parent = element[0].parentNode,
                cursorPosition = this.getChildIndex(parent, element[0]),
                offset = collapseToEnd ? 1 : 0;

        rng = ed.selection.getRng(true);
        rng.setStart(parent, cursorPosition + offset);

        rng.setEnd(parent, cursorPosition + 1);
        element.before(text || this.HIDDEN_CHAR).remove();
        ed.selection.setRng(rng);

        return rng;
    },

    tinyMceEventHandler : function(e) {
        // handle tabbing in tables and lists
        if (e.keyCode == 9) {
            var ed = AJS.Rte.getEditor(),
                selectionNode = ed.selection.getNode();

            if ($(selectionNode).closest("table").is(".confluenceTable")) {
                // lists have precedence over tables
                var inList = ed.dom.getParent(selectionNode, 'UL') || ed.dom.getParent(selectionNode, 'OL');
                if (inList) {
                    return true;
                }

                // stop firefox's default tab behaviour
                if (tinymce.isGecko && e.type == "keypress") {
                    return AJS.stopEvent(e);
                }

                if (e.type == "keydown") {
                    var command = e.shiftKey ? "mceTableMoveCaretToPrevCell" : "mceTableMoveCaretToNextCell";
                    ed.execCommand(command);
                    return false;
                }
            }
        }
        return true; // otherwise continue with default handling
    },

    addTinyMcePluginInit: function(func) {
        this._tinymcePluginInits.push(func);
    },

    initialiseTinyMce : function() {
        var t = AJS.Editor.Adapter;
        tinymce.EditorManager.preInit.apply(tinymce.EditorManager);
        tinymce.EditorManager.init(t.settings);
    },

    webResourcePath : "/download/resources/com.atlassian.confluence.tinymceplugin%3Atinymceeditor/",

    /**
     * gets the base url from the current location.
     * @deprecated use AJS.Rte.getCurrentBaseUrl()
     */
    //
    getCurrentBaseUrl : AJS.Rte.getCurrentBaseUrl,

    // gets the static resource url prefix, which will include the caching headers
    getResourceUrlPrefix : function() {
        if (!this.resourceUrlPrefix) {
            this.resourceUrlPrefix = this.getCurrentBaseUrl() + AJS.Meta.get('editor-plugin-resource-prefix');
        }
        return this.resourceUrlPrefix;
    },

    // gets the absolute url path to the tinymce web resources, which will include the caching headers
    getTinyMceBaseUrl : function() {
        if (!this.absoluteUrl) {
            this.absoluteUrl =  this.getResourceUrlPrefix() + this.webResourcePath + "tinymcesource/";
        }
        return this.absoluteUrl;
    },

    getMinEditorHeight: function() {
        return + AJS.Meta.get('min-editor-height');
    },

    /**
     * Used for selenium tests apparently.
     * @deprecated since 4.0 this doesn't work properly across all the browsers
     */
    putCursorAtPostionInElement : function (selector, position, node) {
        var ed = tinyMCE.activeEditor, doc = ed.getDoc();

        // need the #text node inside the selected element, so filter the child nodes of the selector
        var el = $(selector, node || doc);
        el = el.contents().filter(function(){ return this.nodeType == 3; })[0];
        var range = AJS.Editor.Adapter.createRange(ed.getDoc());
        range.setStart(el, position);
        range.setEnd(el, position);
        ed.selection.setRng(range);
    },

    /**
     * Finds the index of the supplied childNode in the parentNode
     */
    getChildIndex: function(parentNode, childNode) {
        var children = parentNode.childNodes;
        for (var i = 0, len = children.length; i < len; i++) {
            if (children[i] == childNode)
                return i;
        }
        return -1;
    },

    /**
     * @deprecated Since 4.0. Call Confluence.Editor.LinkUtils.insertLink directly.
     */
    insertLink: function (linkObj, existingLinkNode) {
        // Note: we call the method instead of assigning it directly because
        // Confluence.Editor.LinkUtils might not exist when Confluence.Editor.Adapter
        // is created.
        if (existingLinkNode) {
            AJS.Rte.getEditor().selection.select(existingLinkNode);
        }
        Confluence.Editor.LinkAdapter.setLink(linkObj);
    },

    isExternalLink: function(destination) { //Same check as ConfluenceLinkResolver
        return destination.match(/^(\/\/|mailto:|file:|http:|https:)/) || destination.indexOf("\\") === 0;
    },

    isInMacroPlaceholder: function (node) {
        return $(node).closest(".wysiwyg-macro").length > 0;
    },

    getTinyMceEditorMinHeight: function(extraHeight) {
        extraHeight = extraHeight || 0;
        var height = AJS.Editor.Adapter.getMinEditorHeight(), chromeHeight = 0;
        if (height) return height;

        $("#editor-precursor,#header-precursor,#header").each(function() {
          //AJS.log(this.id + " " + $(this).outerHeight());
          chromeHeight += $(this).outerHeight(true);
        });
        //these are very helpful with debugging, so should stay for now.
        //AJS.log("window " + $(window).height());
        //AJS.log("extra height " + extraHeight);
        //AJS.log("editor height " + ($(window).height() - chromeHeight -  extraHeight));
        return ($(window).height() - chromeHeight -  extraHeight);
    },

    resizeOnMessageClose: function() {
      $(".aui-message").bind("messageClose",function() {
           $(document).trigger("resize.resizeplugin", { height: $(this).outerHeight(true) });
      });
    },

    // Initialises the TinyMCE editor. This can be run without waiting for DOM ready if the language pack variable
    // TinyMCELang is available before calling this function.
    initTinyMce: function() {
        AJS.log("Running tinymce version " + tinymce.majorVersion + "." + tinymce.minorVersion);
        var t = AJS.Editor.Adapter, contextPath = Confluence.getContextPath();
        //negative margin so we need to add a negative back onto the height.
        var height = t.getTinyMceEditorMinHeight(-$("#toolbar .toolbar-shadow").height() || 200),
            bodyClass = "wiki-content",
            plugins = "auiwindowmanager,table,paste,emotions,confluence,autocomplete,macroplaceholder," +
                    "customtoolbar,insertwikimarkup,propertypanel,keyboardshortcuts,confmonospace,confcharmap,lists," +
                    "confluencepasteplugin,cursorTarget,confluencecleanupplugin,confluencepastelistplugin," +
                    "confluencepastetableplugin,confluencepastemacroplugin,showformat";

        if(t.getEditorContainer().hasClass("resize")) {
            plugins += ",autoresize";
            bodyClass += " editor-comment";
        } else {
            bodyClass += " fullsize";
            plugins += ",flextofullsize";
            height = height - $("#toolbar").outerHeight() - $("#savebar-container").outerHeight();
        }

        t.resizeOnMessageClose();
        t.settings = {
            //atlassian specific settings
            atlassian: true,
            autoresize_min_height: height,

            // general
            width: "100%",
            height: height,
            keep_values: true,
            convert_urls: true,
            relative_urls: false,
            // CONFDEV-1555 - Required for IE and SSL to ensure complete URLs for inline style urls. Otherwise IE displays security warnings.
            remove_script_host: false,
            document_base_url: Confluence.getBaseUrl() + "/",
            language: AJS.Meta.get('action-locale'),
            button_tile_map: true,
            plugins: plugins,
            gecko_spellcheck : true,
            apply_source_formatting: false, //we don't want new lines between html elements when serialized and sent to the backend
            list_outdent_on_enter: true,

            // advanced theme params
            theme: "advanced",
            // event though we don't use the advanced theme to generate the toolbar, we need the controls registered in TinyMCE
            theme_advanced_buttons1: "formatselect,bold,italic,underline,strikethrough,forecolor,separator," +
                            "table,row_before,row_after,delete_row,col_before,col_after,delete_col,delete_table,separator," +
                            "bullist,numlist,outdent,indent,blockquote,justifyleft,justifycenter,justifyright,justifyfull,separator,sup,sub,separator," +
                            "undo,redo,separator," +
                            "conflink,oldlinkbrowserButton,confimage,conf_macro_browser,separator," +
                            "search,code,customtoolbar",
            theme_advanced_buttons2: "monospace",
            theme_advanced_buttons3: "",
            theme_advanced_toolbar_location: "top",
            theme_advanced_toolbar_align: "left",
            theme_advanced_resizing: false,
            theme_advanced_resize_horizontal: false,
            theme_advanced_statusbar_location: "none",
            theme_advanced_path: false,
            theme_advanced_blockformats: "p,h1,h2,h3,h4,h5,h6,pre,blockquote",

            // selectors for tinymce editors
            mode: "textareas",
            editor_selector: "tinymce-editor",

            // callbacks
            oninit: "AJS.Editor.Adapter.tinyMceOnInit",
            handle_event_callback: "AJS.Editor.Adapter.tinyMceEventHandler",

            // table settings
            visual: false,
            confluence_table_style: "confluenceTable",
            confluence_table_cell_style: "confluenceTd",
            confluence_table_heading_style: "confluenceTh",
            confluence_table_default_rows: 4,
            confluence_table_default_cols: 3,
            confluence_table_default_heading: true,

            // output settings
            cleanup: true,
            /**
             * This is to ensure that any empty elements that require padding are padded with an &nbsp; (say PRE, P, DIV etc.)
             * These spaces are an editor concern and should not be persisted to our storage.
             */
            cleanup_on_startup: true,

            /**
             * This must be false since browsers depend on broken list markup to provide working bullet functionality.
             * We fix up the broken list markup on the server side before we save it so its not necessary here anyway.
             */
            fix_list_elements : false,
            fix_table_elements : true,

            valid_elements : // Theoretically, most elements are allowed to have a wysiwyg parameter set.  See ExternallyDefinedConverter
                             '@[id|class|style|title|wysiwyg|dir<ltr?rtl|lang|xml::lang|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],' +
                             // We add extra link attributes to help conversion to wiki markup
                             'a[*],' +
                             'strong/b,em/i,s,u,' +
                             // User paragraphs indicate a blank line.  See DefaultWysiwygConverter.isUserNewline
                             '#p[align|user],' +
                             '-ol[type|compact],-ul[type|compact],-li,br,' +
                             // The ImageConverter uses an imagetext attribute
                             'img[imagetext|longdesc|usemap|src|border|alt=|title|hspace|vspace|width|height|align],' +
                             '-sub,-sup,'+
                             // the markup tag is used to distinguish bq. markup from {quote}s.  See BlockQuoteConverter.java
                             '-blockquote[cite|markup],' +
                             '-table[*],' +
                             '-tr[rowspan|width|height|align|valign|bgcolor|background|bordercolor],tbody,thead,tfoot,' +
                             '#td[colspan|rowspan|width|height|align|valign|bgcolor|background|bordercolor|scope],' +
                             '#th[colspan|rowspan|width|height|align|valign|scope],caption,' +
                             '#div[*],' +
                             '#span[*],-code,#pre[*],address,-h1,-h2,-h3,-h4,-h5,-h6,hr[size|noshade],' +
                             '-font[face|size|color],dd,dl,dt,cite,abbr,acronym,del[datetime|cite],ins[datetime|cite],object[classid|width|height|codebase|*],param[name|value],' +
                             'embed[type|width|height|src|*],map[name],area[shape|coords|href|alt|target],bdo,button,col[align|char|charoff|span|valign|width],' +
                             'colgroup[align|char|charoff|span|valign|width],dfn,fieldset,form[action|accept|accept-charset|enctype|method],' +
                             'input[accept|alt|checked|disabled|maxlength|name|readonly|size|src|type|value],kbd,label[for],legend,noscript,' +
                             'optgroup[label|disabled],option[disabled|label|selected|value],q[cite],samp,select[disabled|multiple|name|size],small,' +
                             'textarea[cols|rows|disabled|name|readonly],tt,var,big',

            extended_valid_elements: "img[*]",

            //white list the removeformat elements we want to support
            formats: {
                removeformat : [
                    {selector : 'h1,h2,h3,h4,h5,h6,pre',
                                block : 'p', remove : 'all', split : true, expand : false, block_expand : true, deep : true},
					{selector : 'address,article,b,big,blockquote,center,cite,code,date,dd,del,dfn,dl,dt,em,embed,font,footer,' +
                                'header,hgroup,i,ins,kbd,link,menu,nav,object,param,q,s,samp,script,' +
                                'section,small,strike,strong,style,sub,sup,time,tt,u,var',
                                remove : 'all', split : true, expand : false, block_expand : true, deep : true},
					{selector : 'span', attributes : ['style','class'], remove : 'empty', split : true, expand : false, deep : true}, // colored text
					{selector : 'table', attributes : ['cellpadding','cellspacing','border'], split : false, expand : false, deep : true},
                    {selector : '*', attributes : ['style','color','bgcolor','title','lang'], split : false, expand : false, deep : true}
				]
            },

            forced_root_block : 'p',
            force_p_newlines: true, // default
            force_br_newlines: false, // default

            // lutoout settings
            body_class: bodyClass,
            contentCssTags: $("script[title='editor-css']").html(),
            popup_css: false,
            content_css: false,
            editor_css: false,

            // confluence-specific settings
            context_path: contextPath,
            plugin_action_base_path: contextPath + "/plugins/tinymce",
            page_id: AJS.Meta.get('page-id'),
            draft_type: null,
            form_name: AJS.Meta.get('form-name'),
            space_key: encodeURI(AJS.Meta.get('space-key')),
            confluence_popup_width: 620,
            confluence_popup_height: 550,
            elements: "wysiwygTextarea",
            editor_id: "wysiwygTextarea",

            /**
             * Disable resize controls on tables and images in firefox. They render out of place when margins are applied to the table. 
             */
            object_resizing: false,

            //Trigger a pre paste event that can be captured by the Confluence paste plugin
            paste_preprocess : function(pl, o) {
              $(document).trigger('prePaste', [pl, o]);
            },
            // add class=" confluence-embedded-image" to images so that we get the image property panel if image clicked.
            // The 'not' operation is to avoid the property panel applying to copy and pasted emoticons and macro placeholders.
            paste_postprocess : function(pl, o) {
                $("img", o.node)
                        .not('[data-emoticon-name]')
                        .not('.editor-inline-macro')
                        .not('.confluence-embedded-image')
                        .addClass("confluence-embedded-image confluence-external-resource");
                //Trigger a post paste event that can be captured by the confluence paste plugin
                 $(document).trigger('postPaste', [pl, o]);
            }
        };

        // alter settings for drafts
        if (t.settings.page_id == 0) {
            t.settings.page_id = null;
            t.settings.draft_type = AJS.Meta.get('draft-type');
        }

        // load the i18n keys for tinymce
        if (typeof TinyMCELang != "undefined") {
            var ctrlTrans = new RegExp(TinyMCELang.ctrl_key + "\\+", 'g'),
                shiftTrans = new RegExp(TinyMCELang.shift_key + "\\+", 'g');

            // helper to make tool-tips mac friendly
            var replaceForMac = function(str) {
                return str.replace(ctrlTrans, "\u2318").replace(shiftTrans, "\u21E7");
            };

            for (var key in TinyMCELang) {
                var langGroup = TinyMCELang[key];
                if (typeof langGroup == "object") {
                    for (var strKey  in langGroup) {
                        if(tinymce.isMac) {
                            langGroup[strKey] = replaceForMac(langGroup[strKey]);
                        }
                    }
                }

                tinymce.EditorManager.addI18n(t.settings.language + "." + key, TinyMCELang[key]);
            }
            // fix up the the tooltips ont the toolbar
            if (tinymce.isMac) {
                $("#rte-toolbar a, #rte-savebar button, #rte-savebar a").each(function() {
                    var $this = $(this),
                        title = $this.attr("title");
                    if (title) {
                        $this.attr("title", replaceForMac(title));
                    }
                });
            }
        }
        else {
            AJS.log("ERROR: could not find the TinyMCE language pack");
        }

        // plugin point for tinymce plugins to configure settings
        for (var i = 0, ii = t._tinymcePluginInits.length; i < ii; i++) {
            if (typeof t._tinymcePluginInits[i] == "function") {
                t._tinymcePluginInits[i](t.settings);
            }
        }

        tinyMCE.init(t.settings);
        AJS.log("Adapter:after editor manager init");
        t.editorResetContentChanged(); // so we don't trigger drafts due to macro manipulation

        /**
         * Add a tab index to the Editor, only if we're editing a page with a title.
         * The tab index will be set to be just after the title input field in the tab order.
         */
        if ($("#content-title").length) {
            $(t.getEditorFrame()).attr("tabindex", 1);
        }
        if (tinymce.isIE) {
            $(t.getEditorFrame()).attr("hidefocus", "hidefocus");
        }
    }

};

AJS.toInit(AJS.Editor.Adapter.initTinyMce);

})(AJS.$);
(function ($) {
/**
 * The Link object contains all of Confluence's business logic about a front-end Link.
 *
 * It should not interact with the RTE directly but rather use the LinkAdapter singleton.
 */
Confluence.Link = {};

var newLink = function (props) {

    // If the object passed in is already a Link just return it.
    // Until this gets refactored to use 'proper' prototyping just do a duck-type check
    // instead of a constructor check.
    if (props && props.fillNode) {
        return props;
    }

    var link = {

        /**
         * Inserts this link into the RTE
         */
        insert: function () {
            Confluence.Editor.LinkAdapter.setLink(link);
        },

        /**
         * Fills the passed DOM node with the contents of this Link object.
         * @param linkNode the jQuery-wrapped node to fill with the link data
         */
        fillNode: function ($link) {
            var attrs = this.attrs;
            attrs.href = attrs.href || '#';
            $link.attr(attrs);

            $link.html(this.body.html);
            return $link;
        },

        /**
         * Returns a representation of this link as data only - no functions.
         */
        getData: function () {
            var props = {}, key;

            for (key in this) if (this.hasOwnProperty(key) && !$.isFunction(this[key]))
                props[key] = this[key];

            return props;
        },

        /**
         * If the body for this link is a lone image, return it.
         */
        getLinkedImage: function () {
            if (this.body && this.body.jquery) {
                return this.body.length == 1 && this.body.is('img') && this.body;
            }
            return null;
        },

        getResourceId: function () {
            return this.attrs["data-linked-resource-id"] || "";
        },

        /**
         * Returns true if this link points to Confluence content like a Page, Attachment, Space, et al.
         */
        isToConfluenceEntity: function () {
            return this.attrs["data-linked-resource-id"];
        },

        isToAttachmentOnSamePage: function (contentId) {
            return this.attrs["data-linked-resource-type"] == 'attachment' && this.attrs["data-linked-resource-container-id"] == contentId;
        },

        /**
         * A standard link will be stored as an a tag whereas a custom 'atlassian-content' link will be stored as an ac:link
         * tag. This method lets you differentiate between them in the browser.
         *  
         * When used in conjunction with isToConfluenceEntity this can identify a relative link (link to the current page).
         * 
         * @return true if the link is an atlassian-content (custom XHTML namespace) link (as opposed to an HTML link)
         */
        isCustomAtlassianContentLink: function() {
            return this.attrs["data-base-url"];
        },
        
        /**
         * @return true if the link has the correct anchor attribute 
         */
        hasAnchor: function() {
            return this.attrs["data-anchor"];
        },
        
        getResourceType: function () {
            return this.attrs["data-linked-resource-type"];
        },

        getDefaultAlias: function () {
            return this.attrs["data-linked-resource-default-alias"];
        },

        getHref: function () {
            return this.attrs.href;
        },

        getHtml: function () {
            return this.body.html;
        },

        isHrefValid: function () {
            return this.attrs.href && this.attrs.href != 'http://';
        },

        isImage: function () {
            return this.body.isImage;
        },

        isNewLink: function () {
            return $.isEmptyObject(this.attrs);    // a link with no attributes has no destination ==> new
        },

        showsBreadcrumbs: function () {
            return true;
        }
    };
    $.extend(link, props);

    return link;
};

Confluence.Link.fromSelection = function (rng, selectedNode, selectedHtml, selectedText, attrs) {
    function isImageSelected(selectedNode) {
        //we are only interested in nodes that have just an image node.
        var hasOneChild = selectedNode.hasChildNodes() && selectedNode.childNodes.length == 1;
        return $.nodeName(selectedNode, 'img') ||
                (hasOneChild && $.nodeName(selectedNode, "a") && $.nodeName(selectedNode.firstChild, "img"));
    }

    var isImage = isImageSelected(selectedNode);
    var isEditable = rng.collapsed; // Only allow the user to edit the text if this is a new link

    return newLink({
        attrs: attrs,
        body: {
            isEditable: isEditable,
            isImage: isImage,
            html: selectedHtml,
            imgName: isImage && ($(selectedNode).attr('data-linked-resource-default-alias') || $(selectedNode).attr('src')),
            mixedContentText: selectedText   // only used if mixed content being linked
        }
    });
};


/**
 * Creates a Link object from a REST object
 * @param restObj
 */
Confluence.Link.fromREST = function (restObj) {
    var link = newLink({
        attrs: {
            "data-base-url": AJS.Confluence.getBaseUrl(),
            "data-linked-resource-id": restObj.id,
            "data-linked-resource-type": restObj.type,
            href: AJS.REST.findLink(restObj.link),
            "data-linked-resource-default-alias": restObj.title
        },
        body: {
            html: AJS.escapeHtml(restObj.title)
        }
    });

    // CONDEV-5286: The problem here is that the REST response will have a type of 'user'
    // whereas the conversion to storage format requires 'userinfo'.
    if (restObj.type == "user")
        link.attrs["data-linked-resource-type"] = "userinfo";
    
    return link;
};

/**
 * Given a link node and an alias, creates a link object with correct body and attributes.
 * @param linkNode The link node DOM object.
 * @param alias The link alias.
 */
Confluence.Link.fromNode = function(linkNode, alias) {
    var link = newLink({
        attrs: {},
        body: {
            html: alias
        }
    });

    // Add attributes from the link node to the link object.
    for (var attr, i=0, attrs=linkNode.attributes, l=attrs.length; i<l; i++){
        attr = attrs.item(i)
        link.attrs[attr.nodeName] = attr.nodeValue;
    }

    return link;    
};

/**
 * Creates a link that points to a new page.
 */
Confluence.Link.createLinkToNewPage = function (pageTitle, spaceKey) {
    return newLink({
        attrs: {
            "class": 'createlink',
            "data-space-key": spaceKey,
            "data-content-title": pageTitle,
            href: '/pages/createpage.action?spaceKey=' + spaceKey + '&title=' + pageTitle
        },
        body: {
            html: pageTitle
        }
    });
};

/**
 * Creates a link to an external webpage or email address.
 */
Confluence.Link.makeExternalLink = function (href) {
    return newLink({
        attrs: {
            href: href
        },
        body: {
            html: href
        }
    });
};

})(AJS.$);

/**
 * LinkAdapter is an adapter between the RTE and a Link object.
 *
 * It should not know about the Link Browser or how the internals of a Link object work.
 * Any interaction with RTE nodes and Links should be via this object.
 */
Confluence.Editor.LinkAdapter = (function ($) {
    return {

        /**
         * Inserts a link at the position of the current range.
         * @param linkObj a Confluence.Link instance
         */
        setLink: function (linkObj) {
            var ed = AJS.Rte.getEditor(),
                newLinkNode = $(ed.dom.create("a"), ed.getDoc());

            linkObj.fillNode(newLinkNode);
            tinymce.confluence.NodeUtils.replaceSelection(newLinkNode);

            // CONFDEV-5203 - Prevent cursor loss when using arrow keys after inserting an
            // image. This is a FF-specific issue that occurs when adding or editing comments.
            if ((tinyMCE.isGecko) && (!!$("#comments-section").length)) {
                var ed = tinyMCE.activeEditor,
                    bookmark = ed.selection.getBookmark(),
                    $body = $(ed.getBody());

                $body.removeAttr("contenteditable");
                $body.attr("contenteditable", "true");
                ed.selection.moveToBookmark(bookmark);
            }
        },

        /**
         * Constructs a javascript object containing link information.
         * Invoked when the user tries to edit an existing link or make a piece of selected content into a link.
         *
         * Returns a new or existing link based on the current Editor selection.
         */
        getLink: function() {
            var selectedHtml;

            var ed = AJS.Rte.getEditor(),
                selection = ed.selection,
                selectedNode = selection.getNode(),
                linkNode = $(selectedNode).parents().andSelf().filter("a[href]")[0],
                attrs = {};

            if (linkNode) {
                $(linkNode.attributes).each(function () {
                    attrs[this.name] = this.value;
                });
                selection.select(linkNode);
                selectedHtml =  linkNode.innerHTML;
            }

            var rng = selection.getRng(true);
            var selectedText = selection.getContent({format : 'text'});
            selectedHtml = selectedHtml || selection.getContent();
            return Confluence.Link.fromSelection(rng, selectedNode, selectedHtml, selectedText, attrs);
        }

    };
}(AJS.$));

(function($) {
    // Keep a count of the number of panels opened, so the "Goto location" button always opens the link location in a
    // new named window
    var linkPanelCounter = 0;

    AJS.Confluence.PropertyPanel.Link = {
        name : "link",

        canHandleElement : function ($element) {
            return $element.is("a") && $element.attr('href') != '#';
        },

        handle : function(data) {
            var link = data.containerEl;

            var ed = data.ed,
                getLang = function(key) { return AJS.Rte.getEditor().getLang(key); },
                options = {
                    anchorIframe: Confluence.Editor.Adapter.getEditorFrame()
                },
                buttons = [{
                    className: "link-property-panel-goto-button",
                    text: getLang("propertypanel.links_goto"),
                    tooltip: link.href,
                    href: link.href,
                    click: function () {
                        AJS.Confluence.PropertyPanel.destroy();

                        // Give the window a name to help testing, but make sure the name is unique.
                        // Note: IE doesn't allow named windows; just open an unnamed one.
                        var windowName = tinymce.isIE ? "_blank" : "confluence-goto-link-" + AJS.params.pageId + "-" + linkPanelCounter;

                        // mailto links in IE can return null sometimes for window.open
                        // see http://msdn.microsoft.com/en-us/library/ms536651(VS.85).aspx
                        var win = window.open(link.href, windowName);
                        if (win) win.focus();

                    }
                }, {
                    className: "link-property-panel-edit-button",
                    text: getLang("propertypanel.links_edit"),
                    tooltip: getLang("propertypanel.links_edit_tooltip"),
                    disabled: $(link).hasClass("createlink"),
                    click: function () {
                        AJS.Confluence.PropertyPanel.destroy();
                        ed.selection.select(link);
                        Confluence.Editor.LinkBrowser.open();
                    }
                }, {
                    className: "link-property-panel-unlink-button",
                    text: getLang("propertypanel.links_unlink"),
                    tooltip: getLang("propertypanel.links_unlink_tooltip"),
                    click: function () {
                        AJS.Confluence.PropertyPanel.destroy();
                        ed.execCommand("mceConfUnlink", false, link);
                        ed.focus();
                    }
                }];

            AJS.Confluence.PropertyPanel.createFromButtonModel(this.name, link, buttons, options);

            linkPanelCounter++;
        }
    };
    AJS.trigger("add-handler.property-panel", AJS.Confluence.PropertyPanel.Link);
})(AJS.$);

AJS.Confluence.PropertyPanel.Image = (function($) {
     /**
      * Pixel sizes for image resize buttons.
      */
     var sizes = {
         small : 100,
         medium : 300,
         large : 500
     },

     /**
      * Image resizing button classes.
      */
     buttons = {
        small: ".image-size-small",
        medium: ".image-size-medium",
        large: ".image-size-large",
        original: ".image-size-original"
     },

     lowerImageSizeBound = 16,  // The lower bound for an image size in pixels.
     upperImageSizeBound = 900; // The upper bound for an image size in pixels.

    /**
    * Unselects all sizing buttons on the property panel.
    */
    function clearImageSizeButtons() {
        var selectAll = "";
        $.each(buttons, function(i, val) {
            selectAll += val + ',';
        });
        AJS.Confluence.PropertyPanel.current.panel.find(selectAll).removeClass("selected");
    }

    /**
     * Returns the displayed image size. Example - '200px'.
     */
    function getImageSizeText() {
        return ($("#image-size-input").val());
    }

    /**
     * Resizes an image's width to the specified size. The image proportions will be constrained.
     * @param imageProps The image properties
     * @param sizepx The resize in pixels to resize the width to
     * @return true if the image was resized, false otherwise
     */
    function resizeImage(imageProps, sizepx) {
        sizepx = validateSize(imageProps, sizepx);
        if (sizepx) {
            var isThumbnailable = !tinymce.confluence.ImageUtils.isRemoteImg(imageProps.destination);
            delete imageProps["height"];
            imageProps["width"] = sizepx;
            imageProps.thumbnail = isThumbnailable && (sizepx <= AJS.Meta.get("max-thumb-width"));
            updateImageElement(imageProps);
            clearImageSizeButtons();
            return true;
        }
        return false;
    }

    /**
     * Given a size name, resizes the image width correspondingly. The proportions will be constrained.
     * @param imageProps The image properties
     * @param size The size - can be small, medium or large
     * @return true if the image was resized, false otherwise
     */
     function resizeImageToPresetSize(imageProps, size) {
         if (resizeImage(imageProps, sizes[size])) {
            selectButton(size);
            updateImageElement(imageProps);
            return true;
         }
         return false;
     }

    /**
     * Resizes an image to its original width and height.
     * @param imageProps
     * @return true if the image was resized
     */
     function resizeImageToOriginalWidth(imageProps) {
         delete imageProps.width;
         delete imageProps.height;
         imageProps.thumbnail = false;
         clearImageSizeButtons();
         updateImageElement(imageProps);
         selectButton("original");
         return true;
     }

    /**
    * Given a size, selects the corresponding button.
    * @param size Can be small, medium, large or original.
    */
    function selectButton(size) {
        var selectedButton = AJS.Confluence.PropertyPanel.current.panel.find(buttons[size]);
        selectedButton.addClass("selected");
    }

    /**
     * Toggles the image's border.
     * @param imageProps The image properties
     */
    function toggleBorder(imageProps) {
        imageProps.border = +!imageProps.border || false;
        $(".image-border-toggle").toggleClass("selected", imageProps.border);
        updateImageElement(imageProps);
    }

     /**
      * Update the image element to match the ImageProperties passed in, relocate the property panel to match the
      * resized image, and cleanup any image-selection handles.
      */
     function updateImageElement(imageProps) {
         var ppanel = AJS.Confluence.PropertyPanel.current,
                 $img = $(ppanel.anchor),
                 oldSrc = $img.attr("src"),
                 oldHeight = $img.height();

         // Turn off scroll binding, in case an img resize causes a scroll event, and rebind after the resize.
         ppanel.updating = true;
         var rebindAndSnap = function () {
             ppanel.updating = false;
             ppanel.snapToElement({
                 animate: true,
                 animateDuration: 100
             });
         };

         tinymce.confluence.ImageUtils.updateImageElement($img, imageProps);
         if (tinymce.isGecko) {
             // Repaint to clear the image handles from their old positions.
             AJS.Rte.getEditor().execCommand('mceRepaint', false);
         }

         if (imageProps.src != oldSrc) {
             // Image source changed - may have to wait for height to change to snapToElement
             var snapInterval = setInterval(function() {
                 var newHeight = $img.height();
                 if (newHeight != oldHeight) {
                     AJS.log("updateImageElement : height changed after image src change - " + oldHeight + " to " + newHeight);
                     clearTimeout(snapInterval);
                     snapInterval = null;
                     rebindAndSnap();
                 }
             }, 10);
             setTimeout(function() {
                 if (snapInterval) {
                     clearTimeout(snapInterval);
                     snapInterval = null;
                     rebindAndSnap();
                 }
             }, 1000);
         } else {
             rebindAndSnap();
         }

         updateImageSizeText(imageProps);
     }

    /**
     * Updates the image-size field with the current image width. If this is not available in
     * the imageProps, the image element's width is used.
     * @param imageProps The image properties.
     */
    function updateImageSizeText(imageProps) {
        var width = (imageProps.width) ? imageProps.width : $(AJS.Confluence.PropertyPanel.current.anchor).width();
        width = Math.floor(width);
        $("#image-size-input").val(width + 'px');
    }

    /**
     * Validates a provided image size. The image size must be numeric. If it is greater than the upper
     * bound for image size, the upper bound is returned. If it is less than the lower bound for image
     * size, the lower bound is returned.
     * @param imageProps The image properties
     * @param sizepx The size in pixels to validate
     * return The validated image size, or null if the provided size could not be parsed.
     */
    function validateSize(imageProps, sizepx) {
        sizepx = parseInt(sizepx);
        if (!isNaN(sizepx)) {
            // Restrict the size to the upper and lower bounds.
            if (sizepx < lowerImageSizeBound) {
                sizepx = lowerImageSizeBound;
            } else if (sizepx > upperImageSizeBound) {
                sizepx = upperImageSizeBound;
            }
            return sizepx;
        } else {
            return null;
        }
    }

    function makeController(imageProps) {
        return {
            setPresetSize: function (size) {
                resizeImageToPresetSize(imageProps, size);
            },

            setPixelSize: function (size) {
                resizeImage(imageProps, size);
            },

            setToOriginalSize: function () {
                resizeImageToOriginalWidth(imageProps);
            },

            toggleBorder: function () {
                toggleBorder(imageProps);
            },

            getWidth: function () {
                return imageProps["width"];
            },

            getDisplayWidth: function () {
                return getImageSizeText();
            },

            isButtonSelected: function(button) {
                return AJS.Confluence.PropertyPanel.current.panel.find(buttons[button]).hasClass("selected");
            }
        }
    }

    return {
        pluginButtons: [],
        
        name : "image",

        canHandleElement : function ($element) {
           return ($element.is("img") && !$element.hasClass("editor-inline-macro"));
        },

        handle : function (data) {
            // This function may be called directly or via an event handler. If it is called
            // directly, the image node can be passed in.
            if (data.nodeName == "IMG") {
                var img = data;
            } else {
                var img = data.containerEl;
            }

            var $img = $(img),
                imageProps = tinymce.confluence.ImageProperties(img);
            if (!imageProps) {
                return;         // not a "proper" image, e.g. an emoticon
            }

            var ed = AJS.Rte.getEditor(),
                getLang = function(key) { return ed.getLang(key); },

                imageSizeButton = function (size) {
                    return {
                        className: "image-size-" + size,
                        text: getLang("propertypanel.images_" + size),
                        tooltip: getLang("propertypanel.images_" + size + "_tooltip"),
                        click : function (a) {
                            resizeImageToPresetSize(imageProps, size);
                        },
                        selected: imageProps["width"] == sizes[size]
                    };
                },
                buttons = [
                    {
                        className: "editable",
                        tooltip: getLang("propertypanel.images_sizing_tooltip"),
                        html: "<input id=\"image-size-input\"/>"
                    },
                    null,
                    imageSizeButton("small"),
                    imageSizeButton("medium"),
                    imageSizeButton("large"),
                    {
                        className: "image-size-original",
                        text: getLang("propertypanel.images_original"),
                        tooltip: getLang("propertypanel.images_original_tooltip"),
                        click: function (a) {
                            resizeImageToOriginalWidth(imageProps);
                        },
                        selected: !imageProps.width && !imageProps.height && !imageProps.thumbnail
                    },
                    null,   // separator
                    {
                        className: "image-border-toggle",
                        text: getLang("propertypanel.images_border"),
                        tooltip: getLang("propertypanel.images_border_tooltip"),
                        click: function (a) {
                            toggleBorder(imageProps);
                        },
                        selected: (imageProps.border || imageProps.border == 1)
                    }
                ];

                buttons.push(null);     // spacer

                var parent = $img.parent();
                if (parent.is("a[href]")) {
                    // An already-linked image. Show buttons for Editing and Removing the link.
                    buttons.push({
                        className: "image-link-edit",
                        text: getLang("propertypanel.images_link_edit"),
                        tooltip: getLang("propertypanel.images_link_edit_tooltip"),
                        click: function () {
                            AJS.Confluence.PropertyPanel.destroy();
                            ed.selection.select(parent[0]);
                            Confluence.Editor.LinkBrowser.open();
                        }
                    });
                    buttons.push({
                        className: "image-link-remove",
                        text: getLang("propertypanel.images_link_remove"),
                        tooltip: getLang("propertypanel.images_link_remove_tooltip"),
                        click: function () {
                            AJS.Confluence.PropertyPanel.destroy();
                            ed.execCommand("mceConfUnlink", false, img);
                            ed.focus();
                        }
                    });
                } else {
                    // Not a linked image - show 'Link' creation button
                    buttons.push({
                        className: "image-make-link",
                        text: getLang("propertypanel.images_link_create"),
                        tooltip: getLang("propertypanel.images_link_create_tooltip"),
                        click: function () {
                            AJS.Confluence.PropertyPanel.destroy();
                            ed.selection.select(img);
                            Confluence.Editor.LinkBrowser.open();
                        }
                    });
                }
            buttons = buttons.concat(AJS.Confluence.PropertyPanel.Image.pluginButtons);
            AJS.Confluence.PropertyPanel.createFromButtonModel(this.name, img, buttons, { anchorIframe: AJS.Editor.Adapter.getEditorFrame() });

            var imageSizeInput = $("#image-size-input");

            imageSizeInput.bind("focus", function() {
                $(this).select();
            })

            imageSizeInput.bind("change", function() {
                resizeImage(imageProps, getImageSizeText()) ? true : updateImageSizeText(imageProps);
            });

            // Store the state of the image so it can be updated later
            AJS.Confluence.PropertyPanel.current.imageProps = imageProps;
            updateImageSizeText(imageProps);

            return makeController(imageProps);
        }
    };
})(AJS.$);

(function() {
     AJS.trigger("add-handler.property-panel", AJS.Confluence.PropertyPanel.Image);
})();

(function($) {
    AJS.Confluence.PropertyPanel.Macro = (function() {
        var registeredEvents = [];

        return {
            name : "macro",

            registeredEvents: registeredEvents,

            canHandleElement : function ($element) {
                return ($element.hasClass("editor-inline-macro") || $element.hasClass("wysiwyg-macro"));
            },

            handle : function (data) {
                if (data.e.type != "click" && data.e.type != "mouseup") { // only activate this panel on click
                    return;
                }

                var macroNode = data.containerEl,
                        $macroNode = $(macroNode);

                var macroName, macroButtons = [];

                var bodyMacro = !$macroNode.hasClass("editor-inline-macro"),
                        isUnknownMacro = $macroNode.hasClass("wysiwyg-unknown-macro");

                var buttons = [],
                        options = {
                            originalHeight : bodyMacro && $macroNode.height(),
                            anchorIframe: AJS.Editor.Adapter.getEditorFrame()
                        };

                if (!isUnknownMacro) {
                    macroName = $macroNode.attr("data-macro-name");
                    if (AJS.MacroBrowser.getMacroMetadata(macroName)) {
                        macroButtons = AJS.MacroBrowser.getMacroMetadata(macroName).buttons;
                    }

                    var editClass = "macro-placeholder-property-panel-edit-button";
                    if (macroButtons.length > 0 && macroButtons[0].key == "__PROPERTY_PANEL_SPACER") {
                        editClass += " last";
                    }
                    buttons.push({
                        className: editClass,
                        text: "Edit",
                        click: function () {
                            AJS.Confluence.PropertyPanel.destroy();
                            tinymce.confluence.macrobrowser.editMacro($macroNode);
                        }
                    });

                    AJS.$.each(macroButtons, function(i, item) {
                        if (item.key == "__PROPERTY_PANEL_SPACER") {
                            return;
                        }
                        var cls = "macro-property-panel-" + item.key;
                        if (i > 0 && macroButtons[i-1].key == "__PROPERTY_PANEL_SPACER") {
                            cls += " first";
                        }
                        if (i < (macroButtons.length - 1) && macroButtons[i+1].key == "__PROPERTY_PANEL_SPACER") {
                            cls += " last";
                        }

                        buttons.push({
                            className: cls,
                            text: item.label,
                            click: function() {
                                AJS.$(document).trigger(item.key + ".property-panel", $macroNode);
                                AJS.Confluence.PropertyPanel.destroy();
                            }
                        });
                    });
                }
                var removeClass = "macro-placeholder-property-panel-remove-button";
                if (macroButtons.length > 0 && macroButtons[macroButtons.length-1].key == "__PROPERTY_PANEL_SPACER") {
                    removeClass += " first";
                }
                buttons.push({
                    className: removeClass,
                    text: "Remove",
                    click: function () {
                        AJS.Confluence.PropertyPanel.destroy();
    //                    Using a command makes sure the code goes through the tinymce undo code paths.
    //                    Ideally, we would use this command to remove the macro but there seems to be selection problems.
    //                    AJS.Rte.getEditor().execCommand("mceRemoveNode", false, macroNode);
                        AJS.Rte.getEditor().execCommand("mceConfRemoveMacro", macroNode);
                    }
                });

                if ($macroNode.attr("data-macro-parameters")) {

                    var macroParameters = Confluence.MacroParameterSerializer.deserialize($macroNode.attr("data-macro-parameters"));

                    if ("atlassian-macro-output-type" in macroParameters) {

                        var createClickHandlerFor = function (macroOutputType) {
                            return function (buttonElement) {
                                macroParameters["atlassian-macro-output-type"] = macroOutputType;
                                $macroNode.attr("data-macro-parameters", Confluence.MacroParameterSerializer.serialize(macroParameters));

                                if (macroOutputType == "INLINE") {
                                    $(".macro-placeholder-property-panel-display-newline-button").removeClass("selected");
                                } else {
                                    $(".macro-placeholder-property-panel-display-inline-button").removeClass("selected");
                                }

                                $(buttonElement).addClass("selected");
                            }
                        };

                        buttons.push(null); // spacer
                        buttons.push({
                            className: "macro-placeholder-property-panel-display-newline-button",
                            tooltip: "Display on new line",
                            selected: macroParameters["atlassian-macro-output-type"] == "BLOCK",
                            click: createClickHandlerFor("BLOCK")
                        });
                        buttons.push({
                            className: "macro-placeholder-property-panel-display-inline-button",
                            tooltip: "Display inline",
                            selected: macroParameters["atlassian-macro-output-type"] == "INLINE",
                            click: createClickHandlerFor("INLINE")
                        });
                    }
                }

                AJS.$.each(registeredEvents, function() {
                    AJS.$(document).bind(this.id, this.handler);
                });

                AJS.Confluence.PropertyPanel.createFromButtonModel("macro", macroNode, buttons, options);
            },

            registerButtonHandler: function(id, handler) {
                registeredEvents.push({
                    id: id + ".property-panel",
                    handler: handler
                });
            }
        }})();
    AJS.trigger("add-handler.property-panel", AJS.Confluence.PropertyPanel.Macro);
})(AJS.$);
// Register TinyMCE plugin

Confluence.Editor.Adapter.addTinyMcePluginInit(function(settings) {
    settings.plugins += ",linkbrowser";
    var buttons = settings.theme_advanced_buttons1;

    // Splice the Link Browser button into the button string before the Insert menu
    var index = buttons.indexOf("confimage");
    settings.theme_advanced_buttons1 = buttons.substring(0, index) + "linkbrowserButton," + buttons.substring(index);
});

tinymce.create('tinymce.plugins.LinkBrowser', {
    init : function(ed) {
        var LinkBrowser = Confluence.Editor.LinkBrowser;
        ed.addButton("linkbrowserButton", {title: "confluence.conflink_desc", cmd: "mceConflink", "class": "mce_conflink" });

        ed.addCommand('mceConflink', LinkBrowser.open);
        ed.addCommand('mceConfAttachments', function () {
            return LinkBrowser.open({
                panelKey: LinkBrowser.ATTACHMENTS_PANEL
            });
        }); // attachments item in insert menu
    },
    getInfo : function () {
        return {
            longname : "Confluence Link Browser",
            author : "Atlassian",
            authorurl : "http://www.atlassian.com",
            version : tinymce.majorVersion + "." + tinymce.minorVersion
        };
    }
});

tinymce.PluginManager.add('linkbrowser', tinymce.plugins.LinkBrowser);

Confluence.Editor.LinkBrowser = (function($) {

    var popup, locationPresenter, submitButton, currentTab, dialogId = "insert-link-dialog";

    // The key of the search panel, uses internally and exposed.
    var SEARCH_PANEL = 'search',
        ATTACHMENTS_PANEL = 'attachments',
        WEBLINK_PANEL = 'weblink',
        ADVANCED_PANEL = 'advanced';

    /**
     * Called when the user pressed the Insert/Edit button or presses Enter.
     */
    function submit() {
        if (submitButton.attr("disabled")) return;

        submitButton.attr('disabled', 'disabled');
        AJS.debug("link-browser.js: submit");

        // Some tabs may need to alter the location if the submit has been called before blur on some element.
        currentTab.preSubmit && currentTab.preSubmit();

        var link = locationPresenter.getLink();
        close();

        link.insert();
    }

    /**
     * Closes the dialog and resets state to before it was opened - called by both submit and cancel.
     */
    function close() {
        popup.hide().remove();
        AJS.Rte.BookmarkManager.restoreBookmark();
    }

    /**
     * Called when the user presses the Cancel link or presses Escape
     */
    function cancel() {
        close();
    }

    /**
     * Make the Link Browser dialog element, setting button text based on whether a link is being created or edited.
     */
    function makePopup(isNew) {
        var dialog, dialogTitle, submitText;

        dialog = new AJS.ConfluenceDialog({
            width: 800,
            height: 590,
            id: dialogId,
            onCancel: cancel,
            onSubmit: submit
        });

        dialogTitle = isNew ? "Insert Link" : "Edit Link";
        submitText = isNew ? "Insert" : "Save";

        dialog.addHeader(dialogTitle);

        dialog.addButton(submitText, submit);

        dialog.addCancel("Cancel", cancel);
        dialog.addHelpText("Hint: type \"[\" in the Editor to see a list of suggested pages and insert a link.");

        submitButton = dialog.get("button:0")[0].item;
        submitButton.attr({
            id: 'link-browser-insert',
            disabled: "disabled"
        });

        return dialog;
    }

    // Creates the panel element for the tab specified by the web-item,
    // and initializes the tab-controller for it.
    function makeTab(i, tabPresenter, controller) {
        var key = tabPresenter.key,
            tabTitle = tabPresenter.label,
            className = key + "-panel",
            panel = Confluence.Templates.LinkBrowser[key + 'Panel']();

        popup.addPanel(tabTitle, panel, className, className + '-id');

        // The Panel object instance.
        var panelObj = popup.get("panel:" + i);

        // The controller.tabs map will have been pre-populated by a 'dialog-created.link-browser' event binding.
        var tab = controller.tabs[key];
        tab.panelObj = panelObj;
        tab.key = key;
        tab.createPanel({
            baseElement: $(panelObj[0].body)
        });

        // Called automatically by the AJS.Dialog code when a panel is changed
        panelObj[0].onblur = tab.onDeselect;

        panelObj[0].onselect = function () {
            // Most tabs don't show breadcrumbs so it's opt-in by declaring a property.
            var hasBreadcrumbs = !!tab.hasBreadcrumbs;
            AJS.debug("Link Browser: on tab select, breadcrumbs enabled: " + hasBreadcrumbs);
            tab.onSelect();
            locationPresenter.refresh(hasBreadcrumbs);
            currentTab = tab;
        };

        return tab;
    }

    /**
     * Browser tabs are pluggable based on web-items with section
     * "system.editor.link.browser.tabs", sorted by item weight.
     * {@see link-browser-web-items.vm}
     */
    function getTabPresenters() {
        return $("#link-browser-tab-items div").map(function() {
            var item = $(this);
            return {
                key: item.text(),
                weight: item.attr("data-weight"),
                label: this.title
            }
        }).sort(function (a, b) {
            return a.weight - b.weight;
        });
    }

    /**
     * Sets up the Link Browser tabs and the tab it will be opened to, based on any existing link being edited or a 
     * present panel key.
     * 
     * @param controller the LB controller
     * @param presetPanelKey a panel key like 'search', 'attachments' or 'web-link'
     * @param linkObj a existing link object being edited
     */
    function setupTabs(controller, linkObj, presetPanelKey) {
        
        var tabPresenters = getTabPresenters();
        
        // Loops through each web-item tab, setting up the associated panel.
        var firstTab;
        var currentTab = null;
        for (var i = 0, ii = tabPresenters.length; i < ii; i++) {
            var tab = makeTab(i, tabPresenters[i], controller);
            if (i == 0) {
                firstTab = tab;
            }
            if (!linkObj.isNewLink() && $.isFunction(tab.handlesLink) && tab.handlesLink(linkObj)){
                currentTab = tab;
                tab.openedLink = linkObj;
            }
            else if (presetPanelKey == tab.key) {
                // User wants to open the Link Browser to a particular panel
                currentTab = tab;
            }
        }
        if (currentTab) {
            // A particular tab is selected either by user choice or editing an existing link:
            // don't let the dialog perform its default action of opening the last tab selected.
            controller.popup.overrideLastTab();
        } else {
            currentTab = firstTab;  // Select the first tab by default
        }
        return currentTab;
    }

    /**
     * Returns the Main controller object, separate to the DOM. Should be QUnit-testable.
     */
    function makeController() {
        return {

            // Contains a map of LinkTabPanel objects.
            tabs: {},

            // Called by Tabs when the link they represent is changed
            setLink: function (link, hasBreadcrumbs) {
                locationPresenter.setLink(link, hasBreadcrumbs);
            },

            getLink: function () {
                return locationPresenter.getLink();
            },

            // Called by Tabs when the link they represent is valid or not.
            linkValid: function (valid) {
                submitButton.attr("disabled", valid ? "" : "disabled");
            },

            // If the link text can't be focused because it isn't visible, focus the submit button instead.
            focusLinkText: function () {
                if (!locationPresenter.focusLinkText()) {
                    AJS.debug('LinkBrowser: focusing submit button');
                    submitButton.focus();
                }
            },

            getLinkText: function () {
                return locationPresenter.getLinkText();
            },

            isLinkTextVisible: function () {
                return locationPresenter.isLinkTextVisible();
            },

            getLocationPresenter: function () {
                return locationPresenter;
            },

            // Select the 'Search' tab and perform a search on the passed text
            doSearch: function (searchText) {
                this.tabs[SEARCH_PANEL].doSearch(searchText);
            },

            // Moves the location panel to be inside the new parent. Used for styling.
            moveLocationPanel: function (newParent) {
                locationPresenter.moveLocationPanel(newParent);
            },

            // Moves the location panel back to its normal position
            restoreLocationPanel: function () {
                locationPresenter.restoreLocationPanel();
            },

            /**
             * Selects the Link Browser tab with the given index.
             */
            gotoPanel: function (index) {
                this.popup.gotoPanel(index);
            },

            /**
             * Returns the currently-selected tab.
             */
            getCurrentPanel: function () {
                return this.popup.getCurrentPanel();
            },

            /**
             * If the web-link panel is visible, sets the URL field to be the passed value.
             */
            setWebLinkURL: function (url) {
                var webLinkTab = this.tabs[WEBLINK_PANEL];
                if (currentTab != webLinkTab) {
                    AJS.log('Cannot set URL ' + url + ' on hidden Web Link panel');
                    return;
                }
                webLinkTab.setURL(url);
            },

            getWebLinkUrl: function () {
                var webLinkTab = this.tabs[WEBLINK_PANEL];
                if (currentTab != webLinkTab) {
                    AJS.log('Cannot get URL ' + url + ' on hidden Web Link panel');
                    return null;
                }
                return webLinkTab.getURL();
            },

            getTitle: function () {
                return this.popup.getTitle();
            },

            getSubmitButtonText: function () {
                return submitButton.text();
            },

            isSubmitButtonEnabled: function () {
                return submitButton.is(':enabled');
            },

            submit: submit,
            cancel: cancel
        };
    }
    
    /**
     * Main method to open the Link Browser, called from open.
     */
    function openLinkDialog(options) {

        var controller = makeController();

        var linkObj = options.linkInfo;
        popup = makePopup(linkObj.isNewLink());
        controller.popup = popup;

        // Link browser tabs aren't registered until this point - new tabs
        // need to add a binding to this event.
        AJS.trigger("dialog-created.link-browser", [controller]);

        // Create the location panel that is shared between all Dialog panels.
        locationPresenter = Confluence.Editor.LinkBrowser.LinkInfoPresenter(controller);
        locationPresenter.setLinkBody(linkObj.body);

        currentTab = setupTabs(controller, linkObj, options.panelKey);

        popup.popup.element.find('.dialog-page-body:first').append(locationPresenter.getContainer());

        currentTab.panelObj.select();
        currentTab.openedLink = null;   // this ref shouldn't be used after the first tab is selected, so kill it

        popup.show();
        //controller the right thing to send?
        AJS.trigger("dialog-shown.link-browser", popup);
        return controller;
    }

    return {
        // These ids let RTE menu and autocomplete items open the Link Browser to a particular panel.
        SEARCH_PANEL: SEARCH_PANEL,
        ATTACHMENTS_PANEL: ATTACHMENTS_PANEL,
        WEBLINK_PANEL: WEBLINK_PANEL,
        ADVANCED_PANEL: ADVANCED_PANEL,

        /**
         * If the options include an "opener" function, that is used to launch the dialog; otherwise a LinkInfo object is
         * created for the current Editor and the default launcher called.
         *
         * @param options : panelKey - (optional) a panel of the Link Browser to open to
         *                  opener - (optional) a function used to launch the Link Browser
         */
        open: function (options) {
            // Prevent it from opening if another popup dialog is open
            if ($('.aui-dialog:visible').length) return null;

            options = options || {};

            // Store the current selection and scroll position, and get the selected text.
            AJS.Rte.BookmarkManager.storeBookmark();

            options.linkInfo = options.linkInfo || Confluence.Editor.LinkAdapter.getLink();

            if (options.opener)
                // Any supplied opener function must include required state in its scope.
                return options.opener(options.linkInfo.alias, options.linkInfo);

            return openLinkDialog(options);
        },

        cancel: cancel
    };
})(jQuery);

/**
 * Represents the destination panel containing the Link location (internal or external) and the link Alias.
 */
(function($) {
Confluence.Editor.LinkBrowser.LinkInfoPresenter = function (lbController) {

    var container,              // the top-level container holding the location elements
        breadcrumbsEl,          // the element containing any Breadcrumbs
        breadcrumbController,   // the controller for updating and rendering Breadcrumbs
        currentLink,            // the link object currently selected or entered
        defaultAliasClass,      // class that indicates the default alias has been used
        linkTextField,
        linkTextRow,
        linkImageRow,
        linkMixedRow,
        linkMixedContent,
        linkImageName,
        finalBody,              // the body of the link when an image or range is selected - it cannot be changed
        movedPanel,             // when panels are moved, stores the panel that was moved
        movedPanelParent;       // when panels are moved, stores the location the panel should be restored to

    container = $(Confluence.Templates.LinkBrowser.locationPanel());
    breadcrumbsEl = container.find('#breadcrumbs-container');
    breadcrumbController = AJS.MoveDialog.Breadcrumbs(breadcrumbsEl, AJS.Breadcrumbs.getBreadcrumbs);
    defaultAliasClass = "default-alias";
    linkImageRow = container.find('.link-image');
    linkMixedRow = container.find('.link-mixed');
    linkImageName = container.find('#link-image-filename');
    linkMixedContent = container.find('#link-mixed-content');
    linkTextRow = container.find('.link-text');
    linkTextField = linkTextRow.find('input');

    // If the user types in the link text field, we remove the default alias class to indicate this.
    linkTextField.change(function(e) {
        e.keyCode = e.keyCode || e.which;
        if (e.keyCode != 13)
            linkTextField.removeClass(defaultAliasClass);
    });

    /**
     * Returns the text in the link text field.
     */
    function getLinkText() {
        return AJS.escapeHtml($.trim(linkTextField.val()));
    }

    /**
     * Sets the link text in the link text field only if the field is empty
     * or has the default alias class (signifying that the text currently in the field
     * was generated from the link details and is not user-specified).
     * @param text The text to display in the link text field
     */
    function setLinkText(text) {
        if ((getLinkText() == "") || linkTextField.hasClass(defaultAliasClass)) {
            linkTextField.addClass(defaultAliasClass);
            linkTextField.val(text);
        }
    }

    /**
     * Sets the link text from a link object. Uses the data-linked-resource-default-alias if
     * available, otherwise defaults to the body html.
     */
    function setLinkTextFromLink(linkObj) {
        var alias = linkObj.attrs["data-linked-resource-default-alias"] || linkObj.getHtml();
        setLinkText(alias);                    
    }

    /**
     * Returns the name of the image being linked.
     */
    function getLinkImageName() {
        return linkImageName.text();
    }

    /**
     * Returns the content being linked.
     */
    function getLinkContent() {
        return linkMixedContent.text();
    }

    /**
     * Updates the link body fields to reflect current link body information. Sets and toggles
     * the link text, link image or link mixed content as appropriate.
     *
     * @param body - a body object
     */
    function setLinkBody(body) {
        // Don't modify the link body if the original selection should persist.
        if (finalBody) return;

        if (body.isEditable) {
            linkTextField.val(body.html);
        }
        else if (body.isImage) {
            linkImageName.text(body.imgName);
        }
        else {
            linkMixedContent.text(body.mixedContentText);
        }

        finalBody = body.isEditable ? null : body;

        linkTextRow.toggleClass('hidden',  !body.isEditable);
        linkImageRow.toggleClass('hidden', !body.isImage);
        linkMixedRow.toggleClass('hidden', body.isEditable || body.isImage);
    }

    /**
     * If true, show the Breadcrumbs element, else hide it. After the element is shown/hidden, resize the location
     * container by setting a CSS class.
     *
     * @param show true if breadcrumbs should be displayed
     */
    function showBreadcrumbsElement(show) {
        breadcrumbsEl.closest('.row').toggleClass('hidden', !show);
        container.toggleClass('has-breadcrumbs', !!show);       // must pass a boolean to toggleClass to force!
    }

    /**
     * Updates the breadcrumbs for the passed Link object, retrieving breadcrumb data from the back end and display
     * it.
     */
    function updateBreadcrumbs(linkObj) {
        var controls = {
            clearErrors: function () {
                // do nothing - errors will appear in the breadcrumbs element
            },
            error: function (errMsg) {
                // do nothing - errors will appear in the breadcrumbs element
            },

            // called when a destination is selected on one of the panels
            select: function (options) {
                breadcrumbController.update(options, controls);
            }
        };

        var options = {
            id: linkObj.getResourceId(),
            type: linkObj.getResourceType()
        };
        breadcrumbController.update(options, controls);
    }

    /**
     * Updates the link destination in the Location panel of the Link Browser.
     *
     * @param linkObj a linkable ContentEntity with an id and title, or some other object with an href property
     */
    function setLink(linkObj, showBreadcrumbs) {
        AJS.debug("Link Browser: setting link : " + linkObj);

        setLinkTextFromLink(linkObj);

        showBreadcrumbs && updateBreadcrumbs(linkObj);
        // Show/hide breadcrumbs element immediately (before any potential AJAX requests). This allows the breadcrumb
        // controller to show loading messages.
        showBreadcrumbsElement(showBreadcrumbs);

        currentLink = linkObj;
        lbController.linkValid(currentLink && currentLink.isHrefValid());
    }

    /**
     * Returns the link object currently stored in the panel.
     */
    function getLink() {
        if (!currentLink) return null;

        var body = finalBody;
        if (!body) {
            // Not an image/mixed content link. Perhaps there is alias text entered in the field?
            var linkText = getLinkText();
            if (!linkText) {
                // If there is no link text, use a defaultAlias in the case of internal Confluence
                // links, or the href for an external link.
                linkText = currentLink.getDefaultAlias() || currentLink.getHref();
            }
            body = {
                html: linkText
            }
        }
        currentLink.body = body;

        return currentLink;
    }

    /**
     * Puts the cursor in the 'Link Text' field, selecting the current text. Returns true on success, else false.
     */
    function focusLinkText() {
        if (isLinkTextVisible()) {
            AJS.debug('LinkInfoPresenter.focusLinkText focusing alias');
            linkTextField.select();
            return true;
        }
        return false;
    }

    function isLinkTextVisible() {
        return linkTextField.is(':visible');
    }

    function isLinkImageVisible() {
        return linkImageName.is(':visible');
    }

    function isLinkMixedContentVisible() {
        return linkMixedContent.is(':visible');
    }

    /**
     * Called when the Link Browser tab is changed, has the location panel refresh itself based on required size
     * and whether breadcrumbs should be shown.
     */
    function refresh(tabShowsBreadcrumbs) {
        currentLink && showBreadcrumbsElement(tabShowsBreadcrumbs);
    }

    /**
     * Returns the container with the Location elements.
     */
    function getContainer() {
        return container;
    }

    /**
     * Moves the location panel to be the last element inside the new parent. Used for styling.
     * @param newParent
     */
    function moveLocationPanel(newParent) {
        if (!movedPanel) {
            movedPanel = container.find('.row:not(.hidden) .input-field');
            movedPanelParent = movedPanel.parent();
        }
        newParent.append(movedPanel);
        container.hide();
    }

    /**
     * Moves the location panel back to its normal position
     */
    function restoreLocationPanel() {
        if (!movedPanelParent) {
            // Due to AJS.Dialog selection weirdness, the Web Link may be deselected when the Search panel is
            // selected *before* the Link Browser opens. Just ignore it, it doesn't matter.
            return;
        }
        // Put the location panel back where it was.
        movedPanelParent.append(movedPanel);
        container.show();

        movedPanel = null;
        movedPanelParent = null;
    }

    return {
        setLink: setLink,
        getLink: getLink,
        refresh: refresh,
        setLinkBody: setLinkBody,
        getContainer: getContainer,
        isLinkTextVisible: isLinkTextVisible,
        isLinkImageVisible: isLinkImageVisible,
        isLinkMixedContentVisible: isLinkMixedContentVisible,
        focusLinkText: focusLinkText,
        getLinkText: getLinkText,
        getLinkContent: getLinkContent,
        getLinkImageName: getLinkImageName,
        moveLocationPanel: moveLocationPanel,
        restoreLocationPanel: restoreLocationPanel
    };
};
})(jQuery);

(function($) {
// The Search tab registers itself when the Link Browser is created.
AJS.bind("dialog-created.link-browser", function (e, linkBrowser) {

    var searchTextField,
        searchSpaceField,
        resultGrid,
        messageHandler,
        key = 'search',
        restSearchUrl = AJS.REST.getBaseUrl() + "search.json";

    var setLink = function (restObj, updateSearchField) {
        updateSearchField && searchTextField.val(restObj.title);

        var linkObj = Confluence.Link.fromREST(restObj);
        linkBrowser.setLink(linkObj, true);
        linkBrowser.focusLinkText();
    };

    // Perform a search on the passed text
    var doSearch = function (callback) {
        AJS.debug("link-browser-tab-search.js: doing search");

        searchTextField.trigger('hide.autocomplete');
        var searchText = $.trim(searchTextField.val());

        if (searchText) {
            resultGrid.loading();

            AJS.getJSONWrap({
                url: restSearchUrl,
                data: {
                    search: 'site',
                    query: searchText,
                    spaceKey: searchSpaceField.val()
                },
                successCallback: function (data) {
                    if ($.isFunction(callback)) {
                        resultGrid.update(data.result);
                        callback();
                    } else {
                        resultGrid.updateAndSelect(data.result);
                    }

                    // If the autocomplete result has appeared, hide it.
                    searchTextField.trigger('hide.autocomplete');
                },
                messageHandler: messageHandler
            });
        } else {
            $.isFunction(callback) && callback();
        }
    };

    linkBrowser.tabs[key] = {

        hasBreadcrumbs: true,

        createPanel: function (context) {

            var $panel = context.baseElement;
            searchTextField = $panel.find("#link-search-text");
            searchSpaceField = $panel.find("#search-panel-space");

            // Space may have changed since page load if page has been moved.
            var currentLocation = Confluence.PageLocation.get();
            searchSpaceField.find("option:eq(1)").text(currentLocation.spaceName).val(currentLocation.spaceKey);

            searchSpaceField.change(function () {
                // When the space changes update the attribute on the autocomplete field so it can re-loaded when the
                // field gets focus.
                searchTextField.attr('data-spacekey', searchSpaceField.val());
                searchTextField.trigger('clearCache.autocomplete');
            });

            Confluence.Binder.autocompleteSearch(searchTextField.parent());

            searchTextField.bind("selected.autocomplete-content", function (e, data) {
                // If the user selects the "Search for" link from the autocomplete, perform the search.
                if (data.searchFor) {
                    doSearch();
                } else {
                    setLink(data.content, false);
                }
            });

            var columns = [

                // File column has a link to the file and a class for styling with an icon
                AJS.SelectGrid.Column({
                    key: 'title',
                    heading: "Title",
                    getHref: function (rowData) {
                        return AJS.REST.findLink(rowData.link);
                    },
                    getInnerClass: function (rowData) {
                        return rowData.iconClass || ('content-type-' + rowData.type);
                    }
                }),

                // Space column
                AJS.SelectGrid.Column({
                    key: 'space',
                    heading: "Space",
                    getText: function (rowData) {
                        return rowData.space && rowData.space.title || '';
                    }
                }),

                // Last Modified column
                AJS.SelectGrid.Column({
                    key: 'last-modified',
                    heading: "Last Modified",
                    getText: function (rowData) {
                        return rowData.lastModifiedDate && rowData.lastModifiedDate.friendly || '';
                    },
                    getTitle: function (rowData) {
                        return rowData.lastModifiedDate && rowData.lastModifiedDate.date || '';
                    }
                })
            ];

            messageHandler = AJS.MessageHandler({
                baseElement: context.baseElement.find('.message-panel')
            });

            resultGrid = new AJS.ResultGrid({
                baseElement: context.baseElement,
                columns: columns,

                // Called when a row of the search results is selected by mouse or keyboard.
                selectionCallback: function (rowElement, data) {
                    setLink(data);
                },

                noResultMessage: "No search results found."
            });

            // Performs the search when the form at the top of the panel is submitted.
            // The form may be submitted by the user clicking the Search button or pressing
            // 'Enter' in one of the form components.
            $panel.find('.search-form').submit(function () {
                doSearch();
                return false;
            })
            .keydown(function (e) {
                if (e.keyCode === 13) {
                    // If the search autocomplete has a dropdown visible with an item selected we should let it handle
                    // the Enter - if nothing is selected we should close the dropdown and do the search.
                    var dropdownWithSelection = $('.aui-dropdown:visible .active', this).length;
                    if (!dropdownWithSelection) {
                        $("#search-panel-button").focus();
                        e.stopPropagation();     // Don't let the dialog try to Submit itself when it catches the enter.
                    }
                }
            });
        },

        onSelect: function () {
            AJS.debug('Link Browser Search panel selected');

            var openedLink = this.openedLink;
            if (openedLink) {
                AJS.debug('Link Browser Search panel setting link info');
                this.doSearch(openedLink.getDefaultAlias(), function () {
                    // the link may not have an id if it is a relative link to this same page
                    var resourceId = openedLink.getResourceId() ? openedLink.getResourceId() : AJS.params.contentId;
                    resultGrid.select(resourceId);   // Select the row in the result grid
                });
            } else {
                searchTextField.focus();
            }
        },

        // The Search panel handles links that have a Confluence resource id, unless they're attachment links on the
        // same page or links with anchors.
        handlesLink: function (linkObj) {
            return linkObj.isCustomAtlassianContentLink() && !linkObj.hasAnchor() && !linkObj.isToAttachmentOnSamePage(AJS.Meta.get('content-id'));
        },

        /*
         * Perform the search by setting the search field value and reproducing a search-form submit.
         *
         * @param searchText - the text to set in the search field and search on
         * @param callback (optional) - a function to call after the search results have been populated
         */
        doSearch: function (searchText, callback) {
            searchTextField.val(searchText);
            doSearch(callback);
        }
    };
});
})(jQuery);

(function($) {
// The History tab registers itself when the Link Browser is created.
AJS.bind("dialog-created.link-browser", function (e, linkBrowser) {

    /**
        Link Dialog tab object is a method that initializes the tab and returns an object
        with these methods:
            1. onSelect - code that should run every time the panel is selected
            2. getLinkDetails - returns the current link represented by this panel's state
        The init method takes the following arguments:
            a. linkDialog - the JS link dialog controller
    */
    var restSearchUrl = AJS.REST.makeUrl('session/history.json?max-results=20'),
        resultGrid,
        messageHandler,
        key = 'recentlyviewed';

    linkBrowser.tabs[key] = {

        createPanel: function (context, getOverrideController) {

            var rowSelected = function (rowElement, data) {
                var linkObj = Confluence.Link.fromREST(data);
                linkBrowser.setLink(linkObj);
                linkBrowser.focusLinkText();
            };

            var columns = [

                // File column has a link to the file and a class for styling with an icon
                AJS.SelectGrid.Column({
                    key: 'title',
                    heading: "Title",
                    getHref: function (rowData) {
                        return AJS.REST.findLink(rowData.link);
                    },
                    getInnerClass: function (rowData) {
                        return 'content-type-' + rowData.type;
                    }
                }),
                // Space column
                AJS.SelectGrid.Column({
                    key: 'space',
                    heading: "Space",
                    getText: function (rowData) {
                        return rowData.space.title;
                    }
                }),

                // Last Modified column
                AJS.SelectGrid.Column({
                    key: 'last-modified',
                    heading: "Last Modified",
                    getText: function (rowData) {
                        return rowData.lastModifiedDate.friendly;
                    },
                    getTitle: function (rowData) {
                        return rowData.lastModifiedDate.date;
                    }
                })
            ];
            var getRowId = function (rowData) {
                return rowData.attachmentId;
            };

            messageHandler = AJS.MessageHandler({
                baseElement: context.baseElement.find('.message-panel')
            });

            resultGrid = new AJS.ResultGrid({
                baseElement: context.baseElement,
                columns: columns,
                getRowId: getRowId,
                selectionCallback: rowSelected,
                messageHandler: messageHandler,
                noResultMessage: "You have no recently viewed content."
            });
        },

        // The History list is refreshed each time the user selects the "Recently Viewed" tab
        onSelect: function () {
            resultGrid.loading();

            AJS.getJSONWrap({
                url: restSearchUrl,
                messageHandler: messageHandler,
                successCallback: function (data) {
                    resultGrid.update(data.content);
                }
            });
        }
    };
});
})(jQuery);

(function($) {
AJS.bind("dialog-created.link-browser", function (e, linkBrowser) {

    var restUrl = AJS.REST.makeUrl("content/" + AJS.Meta.get('attachment-source-content-id') + "/attachments.json"),
        key = 'attachments',
        controller,
        uploaderController,
        messageHandler,
        resultGrid;

    // Returns the controller for this panel
    var getController = function (context, getOverrideController) {

        var uploadForm = context.baseElement.find('.attach-file-form');

        return $.extend(
            {
                getUploaderController: function () {
                    var uploaderContext = {
                        baseElement: uploadForm
                    },
                    uploaderControllerOverride = function (context) {
                        return {
                            onUploadSuccess: function (attachmentsAdded) {
                                // Jsonated object properties should match REST ones.
                                for (var i = 0, len = attachmentsAdded.length; i < len; i++) {
                                    attachmentsAdded[i].type = 'attachment';
                                }
                                resultGrid.prependAndSelect(attachmentsAdded);
                                AJS.Meta.set("num-attachments", $("#attachments-table tr[data-id]").length);
                                $("#rte-button-attachments").trigger("updateLabel");
                            }
                        };
                    };
                    return Confluence.AttachmentUploader(uploaderContext, uploaderControllerOverride);
                }
            },
            getOverrideController && getOverrideController(context)
        );
    };

    linkBrowser.tabs[key] = {
        hasBreadcrumbs: false,

        // Called by the main Link Browser controller on startup to initialize the by-now-existing panel DOM
        // element.
        createPanel: function (context, getOverrideController) {

            var tab = this;

            controller = getController(context, getOverrideController),
            uploaderController = controller.getUploaderController(context);
            messageHandler = uploaderController.getMessageHandler();

            var columns = [

                // File column has a link to the file and a class for styling with an icon
                AJS.SelectGrid.Column({
                    key: 'title',
                    heading: "Name",
                    getHref: function (rowData) {
                        if (rowData.link) {
                            // REST object returned by attachment search
                            return AJS.REST.findLink(rowData.link);

                        }
                        // JSON returned by upload attachment action
                        return rowData.url;
                    },
                    getInnerClass: function (rowData) {
                        return rowData.iconClass;
                    }
                }),
                // Size column
                AJS.SelectGrid.Column({
                    key: 'size',
                    heading: "Size",
                    getText: function (rowData) {
                        return rowData.niceFileSize;
                    }
                }),

                // Comment column
                AJS.SelectGrid.Column({
                    key: 'comment',
                    heading: "Comment"
                })
            ];

            resultGrid = new AJS.ResultGrid({
                baseElement: context.baseElement,
                columns: columns,

                selectionCallback: function (rowElement, data) {
                    var linkObj = Confluence.Link.fromREST(data);
                    // The attachment will be on the same page so set the data-linked-resouce-container-id.
                    linkObj.attrs["data-linked-resource-container-id"] = AJS.Meta.get('content-id');

                    // Need to check that the href leads to the download path for the attachment.
                    if (AJS.$.isArray(data.link)) {
                        for (var i=0,ii=data.link.length; i<ii; i++) {
                            var link = data.link[i];
                               if (link.rel == "download") {
                                   linkObj.attrs.href= link.href;
                               }
                            }
                        }
                    linkBrowser.setLink(linkObj);
                    linkBrowser.focusLinkText();
                },
                noResultMessage: "There are no attachments on this page."
            });
        },

        // When the Attachment tab is selected the current attachments are refreshed.
        onSelect: function () {
            // Store a ref in scope to last across the async AJAX request - the tab.openedLink ref is dropped by the
            // main controller after the tab.select() call is made.
            var openedLink = this.openedLink,
                selectedLink = linkBrowser.getLink();

            resultGrid.loading();
            AJS.getJSONWrap({
                url: restUrl,
                messageHandler: messageHandler,
                successCallback: function (data) {

                    resultGrid.update(data.attachment);

                    if (selectedLink) {
                       if (selectedLink.getResourceType() == "attachment") {
                           resultGrid.select(selectedLink.getResourceId());
                       }
                    }
                    else if (openedLink) {
                        // If this panel has been opened automatically to edit an existing attachment link,
                        // select that link.
                        resultGrid.select(openedLink.getResourceId());
                    } else if (data.attachment.length) {
                        resultGrid.select(data.attachment[0].id);
                    }
                    AJS.debug('Loaded attachments');
                }
            });
        },

        // The Attachment panel opens existing links for attachments that are on the current page or blogpost.
        handlesLink: function (linkObj) {
            return linkObj.isToAttachmentOnSamePage(AJS.Meta.get('content-id'));
        }
    };
});
})(jQuery);

(function($) {
// The Web Link tab registers itself when the Link Browser is created.
AJS.bind("dialog-created.link-browser", function (e, linkBrowser) {

    var urlField,
        key = 'weblink',
        thisPanel,
        tab,
        protocolDelimRegex= new RegExp('[:/]');

    function getURL() {
        return $.trim(urlField.val());
    }

    // Checks that the URL has a protocol, fixes if it can.
    function fixUrl() {
        var url = getURL();
        if (AJS.Validate.url(url) || url.indexOf('mailto:') == 0) {
            // A complete URL - leave it alone
            return;
        }
        // If the URL is not an absolute URL it might be a http URL missing the http or a mailto: missing the mailto.
        var defaultAlias = url;
        if (AJS.Validate.email(url)) {
            // Email link is missing mailto - add it
            url = 'mailto:' + url;
        }
        else if (!protocolDelimRegex.test(url)) {
            // *Probably* just a web address like 'www.atlassian.com' that needs an http:// prefix
            url = 'http://' + url;
        } else {
            // Not a valid URL that we know of - might have deleted half of the protocol or something... as they wish!
            return;
        }

        AJS.debug('Updating Link Browser Web Link URL to: ' + url);
        urlField.val(url);

        // Pass the original URL as the title so that the alias can be set to the non-prefixed URL if it
        // isn't already set.
        var linkObj = Confluence.Link.makeExternalLink(url);
        linkBrowser.setLink(linkObj);
    }

    tab = linkBrowser.tabs[key] = {

        createPanel: function (context) {
            thisPanel = context.baseElement;
            urlField = thisPanel.find("input[name='destination']");
            urlField.keyup(function (e) {
                var url = getURL();
                var linkObj = url ? Confluence.Link.makeExternalLink(url) : null;
                linkBrowser.setLink(linkObj); // will enable the Submit button when a URL is added
            });

            urlField.change(fixUrl);
            urlField.bind('paste', function () {
                AJS.debug('Link Browser web link url pasted');
                // CONF-22287 - use setTimeout to wait for the paste event to update the field value
                setTimeout(fixUrl, 0);
            });
        },

        // Called when the panel is selected - may be called before the LB dialog has its 'show' method called
        onSelect: function () {
            // Put the location panel inside the web-link panel for easier styling.
            linkBrowser.moveLocationPanel(thisPanel);

            if (this.openedLink) {
                tab.setURL(this.openedLink.attrs.href);
                linkBrowser.setLink(this.openedLink);
            }

            // Defer focus to after LB is shown, gets around AJS.Dialog tabindex issues
            setTimeout(function() {
                urlField.focus();
            });
        },

        onDeselect: linkBrowser.restoreLocationPanel,

        // Called when the submit button is clicked, before the location is retrieved from the Location controller.
        // Re-validate and prefix the URL.
        preSubmit: fixUrl,

        // The Web link panel handles non Confluence links
        handlesLink: function (linkObj) {
            return !linkObj.isCustomAtlassianContentLink();
        },

        setURL: function (url) {
            urlField.val(url);
            urlField.keyup();
            urlField.change();
        },

        getURL: getURL
    };
});
})(jQuery);

(function($) {

    // The Advanced tab registers itself when the Link Browser is created.
    AJS.bind("dialog-created.link-browser", function (e, linkBrowser) {

        var key = 'advanced',                   // This panel's key.
            linkFieldName = 'advanced-link',    // The ID of the link input element.
            errorFieldName = 'advanced-error',  // The ID of the error element.
            $linkField,                         // The jQueryfied link input element.
            $errorField,                        // The jQueryfied error field.
            thisPanel,                          // A reference to this panel.


        /**
         * Triggered on a successful AJAX request.
         */
        successfulConversion = function (data, textStatus) {
            var $data = $(data);

            if (!$data.length) {
                return; // Do nothing if no data was returned.
            }

            // Use the first available link.
            var $link = $data.find("a:first");

            if (!$link.length) { // The markup wasn't link markup.
                $errorField.text("The markup provided is not valid link markup");
            } else if ($link.hasClass("unresolved")) {
                // If the link used an unspecified shortcut or unrecognised space, it will return with an unresolved class
                var shortcutKey = $link.attr("shortcut-key");
                var spaceKey = $link.attr("data-space-key");
                if (!!shortcutKey) {
                    $errorField.text(AJS.format("{0} is not a recognised shortcut", shortcutKey));
                } else if (!!spaceKey) {
                    $errorField.text(AJS.format("{0} is not a recognised space", spaceKey));
                }
            } else { // The link is valid.
                var linkObj = Confluence.Link.fromNode($link[0], $linkField.val());
                linkBrowser.setLink(linkObj);
            }
        },


        /**
         * Triggered on an unsuccessful AJAX request.
         */
        erroredConversion = function(XMLHttpRequest, textStatus, errorThrown) {
            AJS.debug("Error during conversion: textStatus = " + textStatus + ", errorThrown = " + errorThrown);
            $errorField.text("An internal server error occurred");
        },


        /**
         * Uses the wikixhtmlconverter REST API to convert the provided wikimarkup into XHTML.
         */
        convert = function(markup, successfulConversion, erroredConversion) {
            var conversionData = {
                wiki: markup,
                entityId: AJS.Meta.get("content-id"),
                spaceKey: AJS.Meta.get("space-key")
            };

            AJS.$.ajax({
                type : "POST",
                contentType : "application/json; charset=utf-8",
                url : AJS.params.contextPath + "/rest/tinymce/1/wikixhtmlconverter",
                data : AJS.$.toJSON(conversionData),
                dataType : "text", 
                success : successfulConversion,
                error : erroredConversion,
                timeout: 10000
            });
        },

        /**
         * Wraps the text currently in the link input field with square braces and sends
         * it to the convert() method.
         */
        parseLinkText = function () {
            var text = $linkField.val();
            convert("[" + text + "]", successfulConversion, erroredConversion);
        };

        // Define the available Link Browser Advanced tab methods.
        linkBrowser.tabs[key] = {

            createPanel: function (context) {
                thisPanel = context.baseElement;
                $linkField = thisPanel.find("input[name='advanced-link']");
                $errorField = thisPanel.find("div[name='advanced-error']");
                thisPanel.find("form").keydown(function(e) {
                    if(e.keyCode == 13 && !linkBrowser.isSubmitButtonEnabled()) {
                        e.preventDefault();
                    }
                });
                $linkField.keyup(function (e) {
                    $errorField.text('');
                });
                $linkField.change(function (e) {
                    parseLinkText();
                });
            },

            // Called when the panel is selected
            onSelect: function () {
                linkBrowser.moveLocationPanel(thisPanel);

                // Defer focus to after LB is shown, gets around AJS.Dialog tabindex issues
                setTimeout(function() {
                    $linkField.focus();
                });
            },

            // Called when this panel is no longer selected
            onDeselect: function () {
                linkBrowser.restoreLocationPanel();
            },

            handlesLink: function (linkObj) {
                return false; // This panel doesn't handle any links as we don't store the markup
            }
        };
    });

})(jQuery);

// This file was automatically generated from link-browser.soy.
// Please don't edit this file by hand.

if (typeof Confluence == 'undefined') { var Confluence = {}; }
if (typeof Confluence.Templates == 'undefined') { Confluence.Templates = {}; }
if (typeof Confluence.Templates.LinkBrowser == 'undefined') { Confluence.Templates.LinkBrowser = {}; }


Confluence.Templates.LinkBrowser.searchPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<form class="aui search-form" onsubmit="return false;"><fieldset class="inline"><div class="search-input"><label for="link-search-text" id="linkSearch-label" class="assistive">', soy.$$escapeHtml("Search"), '</label><input id="link-search-text" type="text" tabindex="0" class="text autocomplete-search" name="linkSearch" size="50" autocomplete="off" data-search-link-message="', soy.$$escapeHtml("Search for \x26lsquo;{0}\x26rsquo;"), '"></div><select tabindex="0" class="search-space select" id="search-panel-space"><option value="">', soy.$$escapeHtml("All Spaces"), '</option><option value=""> </option></select><button type="submit" tabindex="0" class="" id="search-panel-button">', soy.$$escapeHtml("Search"), '</button></fieldset></form><div class="message-panel hidden"></div><div id="search-results-table" class="data-table hidden"></div>');
  if (!opt_sb) return output.toString();
};


Confluence.Templates.LinkBrowser.recentlyviewedPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div class="recently-viewed-panel"><div class="message-panel hidden"></div><div class="data-table"></div></div>');
  if (!opt_sb) return output.toString();
};


Confluence.Templates.LinkBrowser.attachmentsPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div class="attach-file-form"><form method="post" enctype="multipart/form-data" id="attachments-attachfile-form" action="', soy.$$escapeHtml("/confluence"), '/pages/attachfile.action"><div>', soy.$$escapeHtml("Link to a file that is attached to this page or attach a new one."), '</div><span class="upload-field"> <label for="file_0">', soy.$$escapeHtml("Upload file"), ':</label> <input type="file" name="file_0"> </span></form><div class="upload-in-progress upload-field hidden">', soy.$$escapeHtml("Upload in progress\u2026"), '</div><div class="warning"><ul class="hidden message-panel"></ul></div></div><div class="message-panel hidden"></div><div id="attachments-table" class="attachment-list data-table"></div>');
  if (!opt_sb) return output.toString();
};


Confluence.Templates.LinkBrowser.weblinkPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div class="input-field"><label id="destination-label" for="weblink-destination">', soy.$$escapeHtml("Address"), ':</label><input type="text" tabindex="0" class="text" id="weblink-destination" name="destination" size="60"></div><div class="web-link-desc description">', soy.$$escapeHtml("Web, email or any other internet address"), '</div>');
  if (!opt_sb) return output.toString();
};


Confluence.Templates.LinkBrowser.advancedPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<form class="aui" onsubmit="return false;"><div class="advanced-desc title">', AJS.format("Here you can insert a link into the page using \x3ca href\x3d\x22{0}\x22 target\x3d\x22_blank\x22\x3eWiki Markup\x3c/a\x3e.","http://docs.atlassian.com/confluence/docs-40/Linking+to+Pages"), '</div><div class="input-field"><label id="advanced-label" for="advanced-link">', soy.$$escapeHtml("Link"), ':</label><input type="text" tabindex="0" class="text" id="advanced-link" name="advanced-link" size="60"><div name="advanced-error" class="advanced-error error"></div></div><div class="advanced-desc description">', soy.$$escapeHtml("To insert a link to a new page, type in the desired page title."), '<br/>', soy.$$escapeHtml("To insert an anchor link, type #anchorname."), '</div></form>');
  if (!opt_sb) return output.toString();
};


Confluence.Templates.LinkBrowser.locationPanel = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div id="link-browser-location" class="location-info"><div class="row hidden"><label class="link-location-label">', soy.$$escapeHtml("Link Location"), ':</label><div class="breadcrumbs-container"><div class="breadcrumbs-line"><ul id="breadcrumbs-container" class="breadcrumbs"></ul></div></div></div><div class="row link-text"><div class="input-field"><label for="alias" id="alias-label">', soy.$$escapeHtml("Link Text"), ':</label><input type="text" tabindex="0" class = "text" name = "alias" size="50"></div></div><div class="row link-image hidden"><div class="input-field"><label>', soy.$$escapeHtml("Link Image"), ':</label><span id="link-image-filename" class="content-type-attachment-image"></span></div></div><div class="row link-mixed hidden"><div class="input-field"><label>', soy.$$escapeHtml("Link Text"), ':</label><span id="link-mixed-content"></span></div></div></div>');
  if (!opt_sb) return output.toString();
};


