Вход Регистрация
Файл: library/wysihtml5/dist/wysihtml5-0.2.0.js
Строк: 14853
<?php
/**
 * @license wysihtml5 v0.2.0
 * https://github.com/xing/wysihtml5
 *
 * Author: Christopher Blum (https://github.com/tiff)
 *
 * Copyright (C) 2011 XING AG
 * Licensed under GNU General Public License
 *
 */
var wysihtml5 = {
  
version"0.2.0",
  
  
// namespaces
  
commands:   {},
  
dom:        {},
  
quirks:     {},
  
toolbar:    {},
  
lang:       {},
  
selection:  {},
  
views:      {},
  
  
INVISIBLE_SPACE"uFEFF",
  
  
EMPTY_FUNCTION: function() {},
  
  
ELEMENT_NODE1,
  
TEXT_NODE:    3,
  
  
BACKSPACE_KEY:  8,
  
ENTER_KEY:      13,
  
ESCAPE_KEY:     27,
  
SPACE_KEY:      32,
  
DELETE_KEY:     46
};/**
 * @license Rangy, a cross-browser JavaScript range and selection library
 * http://code.google.com/p/rangy/
 *
 * Copyright 2011, Tim Down
 * Licensed under the MIT license.
 * Version: 1.2.2
 * Build date: 13 November 2011
 */
window['rangy'] = (function() {


    var 
OBJECT "object", FUNCTION = "function"UNDEFINED "undefined";

    var 
domRangeProperties = ["startContainer""startOffset""endContainer""endOffset""collapsed",
        
"commonAncestorContainer""START_TO_START""START_TO_END""END_TO_START""END_TO_END"];

    var 
domRangeMethods = ["setStart""setStartBefore""setStartAfter""setEnd""setEndBefore",
        
"setEndAfter""collapse""selectNode""selectNodeContents""compareBoundaryPoints""deleteContents",
        
"extractContents""cloneContents""insertNode""surroundContents""cloneRange""toString""detach"];

    var 
textRangeProperties = ["boundingHeight""boundingLeft""boundingTop""boundingWidth""htmlText""text"];

    
// Subset of TextRange's full set of methods that we're interested in
    
var textRangeMethods = ["collapse""compareEndPoints""duplicate""getBookmark""moveToBookmark",
        
"moveToElementText""parentElement""pasteHTML""select""setEndPoint""getBoundingClientRect"];

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Trio of functions taken from Peter Michaux's article:
    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
    
function isHostMethod(op) {
        var 
typeof o[p];
        return 
== FUNCTION || (!!(== OBJECT && o[p])) || == "unknown";
    }

    function 
isHostObject(op) {
        return !!(
typeof o[p] == OBJECT && o[p]);
    }

    function 
isHostProperty(op) {
        return 
typeof o[p] != UNDEFINED;
    }

    
// Creates a convenience function to save verbose repeated calls to tests functions
    
function createMultiplePropertyTest(testFunc) {
        return function(
oprops) {
            var 
props.length;
            while (
i--) {
                if (!
testFunc(oprops[i])) {
                    return 
false;
                }
            }
            return 
true;
        };
    }

    
// Next trio of functions are a convenience to save verbose repeated calls to previous two functions
    
var areHostMethods createMultiplePropertyTest(isHostMethod);
    var 
areHostObjects createMultiplePropertyTest(isHostObject);
    var 
areHostProperties createMultiplePropertyTest(isHostProperty);

    function 
isTextRange(range) {
        return 
range && areHostMethods(rangetextRangeMethods) && areHostProperties(rangetextRangeProperties);
    }

    var 
api = {
        
version"1.2.2",
        
initializedfalse,
        
supportedtrue,

        
util: {
            
isHostMethodisHostMethod,
            
isHostObjectisHostObject,
            
isHostPropertyisHostProperty,
            
areHostMethodsareHostMethods,
            
areHostObjectsareHostObjects,
            
areHostPropertiesareHostProperties,
            
isTextRangeisTextRange
        
},

        
features: {},

        
modules: {},
        
config: {
            
alertOnWarnfalse,
            
preferTextRangefalse
        
}
    };

    function 
fail(reason) {
        
window.alert("Rangy not supported in your browser. Reason: " reason);
        
api.initialized true;
        
api.supported false;
    }

    
api.fail fail;

    function 
warn(msg) {
        var 
warningMessage "Rangy warning: " msg;
        if (
api.config.alertOnWarn) {
            
window.alert(warningMessage);
        } else if (
typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
            
window.console.log(warningMessage);
        }
    }

    
api.warn warn;

    if ({}.
hasOwnProperty) {
        
api.util.extend = function(oprops) {
            for (var 
i in props) {
                if (
props.hasOwnProperty(i)) {
                    
o[i] = props[i];
                }
            }
        };
    } else {
        
fail("hasOwnProperty not supported");
    }

    var 
initListeners = [];
    var 
moduleInitializers = [];

    
// Initialization
    
function init() {
        if (
api.initialized) {
            return;
        }
        var 
testRange;
        var 
implementsDomRange falseimplementsTextRange false;

        
// First, perform basic feature tests

        
if (isHostMethod(document"createRange")) {
            
testRange document.createRange();
            if (
areHostMethods(testRangedomRangeMethods) && areHostProperties(testRangedomRangeProperties)) {
                
implementsDomRange true;
            }
            
testRange.detach();
        }

        var 
body isHostObject(document"body") ? document.body document.getElementsByTagName("body")[0];

        if (
body && isHostMethod(body"createTextRange")) {
            
testRange body.createTextRange();
            if (
isTextRange(testRange)) {
                
implementsTextRange true;
            }
        }

        if (!
implementsDomRange && !implementsTextRange) {
            
fail("Neither Range nor TextRange are implemented");
        }

        
api.initialized true;
        
api.features = {
            
implementsDomRangeimplementsDomRange,
            
implementsTextRangeimplementsTextRange
        
};

        
// Initialize modules and call init listeners
        
var allListeners moduleInitializers.concat(initListeners);
        for (var 
0len allListeners.lengthlen; ++i) {
            try {
                
allListeners[i](api);
            } catch (
ex) {
                if (
isHostObject(window"console") && isHostMethod(window.console"log")) {
                    
window.console.log("Init listener threw an exception. Continuing."ex);
                }

            }
        }
    }

    
// Allow external scripts to initialize this library in case it's loaded after the document has loaded
    
api.init init;

    
// Execute listener immediately if already initialized
    
api.addInitListener = function(listener) {
        if (
api.initialized) {
            
listener(api);
        } else {
            
initListeners.push(listener);
        }
    };

    var 
createMissingNativeApiListeners = [];

    
api.addCreateMissingNativeApiListener = function(listener) {
        
createMissingNativeApiListeners.push(listener);
    };

    function 
createMissingNativeApi(win) {
        
win win || window;
        
init();

        
// Notify listeners
        
for (var 0len createMissingNativeApiListeners.lengthlen; ++i) {
            
createMissingNativeApiListeners[i](win);
        }
    }

    
api.createMissingNativeApi createMissingNativeApi;

    
/**
     * @constructor
     */
    
function Module(name) {
        
this.name name;
        
this.initialized false;
        
this.supported false;
    }

    
Module.prototype.fail = function(reason) {
        
this.initialized true;
        
this.supported false;

        throw new 
Error("Module '" this.name "' failed to load: " reason);
    };

    
Module.prototype.warn = function(msg) {
        
api.warn("Module " this.name ": " msg);
    };

    
Module.prototype.createError = function(msg) {
        return new 
Error("Error in Rangy " this.name " module: " msg);
    };

    
api.createModule = function(nameinitFunc) {
        var 
module = new Module(name);
        
api.modules[name] = module;

        
moduleInitializers.push(function(api) {
            
initFunc(apimodule);
            
module.initialized true;
            
module.supported true;
        });
    };

    
api.requireModules = function(modules) {
        for (var 
0len modules.lengthmodulemoduleNamelen; ++i) {
            
moduleName modules[i];
            
module api.modules[moduleName];
            if (!
module || !(module instanceof Module)) {
                throw new 
Error("Module '" moduleName "' not found");
            }
            if (!
module.supported) {
                throw new 
Error("Module '" moduleName "' not supported");
            }
        }
    };

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Wait for document to load before running tests

    
var docReady false;

    var 
loadHandler = function(e) {

        if (!
docReady) {
            
docReady true;
            if (!
api.initialized) {
                
init();
            }
        }
    };

    
// Test whether we have window and document objects that we will need
    
if (typeof window == UNDEFINED) {
        
fail("No window found");
        return;
    }
    if (
typeof document == UNDEFINED) {
        
fail("No document found");
        return;
    }

    if (
isHostMethod(document"addEventListener")) {
        
document.addEventListener("DOMContentLoaded"loadHandlerfalse);
    }

    
// Add a fallback in case the DOMContentLoaded event isn't supported
    
if (isHostMethod(window"addEventListener")) {
        
window.addEventListener("load"loadHandlerfalse);
    } else if (
isHostMethod(window"attachEvent")) {
        
window.attachEvent("onload"loadHandler);
    } else {
        
fail("Window does not have required addEventListener or attachEvent method");
    }

    return 
api;
})();
rangy.createModule("DomUtil", function(apimodule) {

    var 
UNDEF "undefined";
    var 
util api.util;

    
// Perform feature tests
    
if (!util.areHostMethods(document, ["createDocumentFragment""createElement""createTextNode"])) {
        
module.fail("document missing a Node creation method");
    }

    if (!
util.isHostMethod(document"getElementsByTagName")) {
        
module.fail("document missing getElementsByTagName method");
    }

    var 
el document.createElement("div");
    if (!
util.areHostMethods(el, ["insertBefore""appendChild""cloneNode"] ||
            !
util.areHostObjects(el, ["previousSibling""nextSibling""childNodes""parentNode"]))) {
        
module.fail("Incomplete Element implementation");
    }

    
// innerHTML is required for Range's createContextualFragment method
    
if (!util.isHostProperty(el"innerHTML")) {
        
module.fail("Element is missing innerHTML property");
    }

    var 
textNode document.createTextNode("test");
    if (!
util.areHostMethods(textNode, ["splitText""deleteData""insertData""appendData""cloneNode"] ||
            !
util.areHostObjects(el, ["previousSibling""nextSibling""childNodes""parentNode"]) ||
            !
util.areHostProperties(textNode, ["data"]))) {
        
module.fail("Incomplete Text Node implementation");
    }

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
    // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
    // contains just the document as a single element and the value searched for is the document.
    
var arrayContains /*Array.prototype.indexOf ?
        function(arr, val) {
            return arr.indexOf(val) > -1;
        }:*/

        
function(arrval) {
            var 
arr.length;
            while (
i--) {
                if (
arr[i] === val) {
                    return 
true;
                }
            }
            return 
false;
        };

    
// Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
    
function isHtmlNamespace(node) {
        var 
ns;
        return 
typeof node.namespaceURI == UNDEF || ((ns node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
    }

    function 
parentElement(node) {
        var 
parent node.parentNode;
        return (
parent.nodeType == 1) ? parent null;
    }

    function 
getNodeIndex(node) {
        var 
0;
        while( (
node node.previousSibling) ) {
            
i++;
        }
        return 
i;
    }

    function 
getNodeLength(node) {
        var 
childNodes;
        return 
isCharacterDataNode(node) ? node.length : ((childNodes node.childNodes) ? childNodes.length 0);
    }

    function 
getCommonAncestor(node1node2) {
        var 
ancestors = [], n;
        for (
node1nn.parentNode) {
            
ancestors.push(n);
        }

        for (
node2nn.parentNode) {
            if (
arrayContains(ancestorsn)) {
                return 
n;
            }
        }

        return 
null;
    }

    function 
isAncestorOf(ancestordescendantselfIsAncestor) {
        var 
selfIsAncestor descendant descendant.parentNode;
        while (
n) {
            if (
=== ancestor) {
                return 
true;
            } else {
                
n.parentNode;
            }
        }
        return 
false;
    }

    function 
getClosestAncestorIn(nodeancestorselfIsAncestor) {
        var 
pselfIsAncestor node node.parentNode;
        while (
n) {
            
n.parentNode;
            if (
=== ancestor) {
                return 
n;
            }
            
p;
        }
        return 
null;
    }

    function 
isCharacterDataNode(node) {
        var 
node.nodeType;
        return 
== || == || == // Text, CDataSection or Comment
    
}

    function 
insertAfter(nodeprecedingNode) {
        var 
nextNode precedingNode.nextSiblingparent precedingNode.parentNode;
        if (
nextNode) {
            
parent.insertBefore(nodenextNode);
        } else {
            
parent.appendChild(node);
        }
        return 
node;
    }

    
// Note that we cannot use splitText() because it is bugridden in IE 9.
    
function splitDataNode(nodeindex) {
        var 
newNode node.cloneNode(false);
        
newNode.deleteData(0index);
        
node.deleteData(indexnode.length index);
        
insertAfter(newNodenode);
        return 
newNode;
    }

    function 
getDocument(node) {
        if (
node.nodeType == 9) {
            return 
node;
        } else if (
typeof node.ownerDocument != UNDEF) {
            return 
node.ownerDocument;
        } else if (
typeof node.document != UNDEF) {
            return 
node.document;
        } else if (
node.parentNode) {
            return 
getDocument(node.parentNode);
        } else {
            throw new 
Error("getDocument: no document found for node");
        }
    }

    function 
getWindow(node) {
        var 
doc getDocument(node);
        if (
typeof doc.defaultView != UNDEF) {
            return 
doc.defaultView;
        } else if (
typeof doc.parentWindow != UNDEF) {
            return 
doc.parentWindow;
        } else {
            throw new 
Error("Cannot get a window object for node");
        }
    }

    function 
getIframeDocument(iframeEl) {
        if (
typeof iframeEl.contentDocument != UNDEF) {
            return 
iframeEl.contentDocument;
        } else if (
typeof iframeEl.contentWindow != UNDEF) {
            return 
iframeEl.contentWindow.document;
        } else {
            throw new 
Error("getIframeWindow: No Document object found for iframe element");
        }
    }

    function 
getIframeWindow(iframeEl) {
        if (
typeof iframeEl.contentWindow != UNDEF) {
            return 
iframeEl.contentWindow;
        } else if (
typeof iframeEl.contentDocument != UNDEF) {
            return 
iframeEl.contentDocument.defaultView;
        } else {
            throw new 
Error("getIframeWindow: No Window object found for iframe element");
        }
    }

    function 
getBody(doc) {
        return 
util.isHostObject(doc"body") ? doc.body doc.getElementsByTagName("body")[0];
    }

    function 
getRootContainer(node) {
        var 
parent;
        while ( (
parent node.parentNode) ) {
            
node parent;
        }
        return 
node;
    }

    function 
comparePoints(nodeAoffsetAnodeBoffsetB) {
        
// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
        
var nodeCrootchildAchildBn;
        if (
nodeA == nodeB) {

            
// Case 1: nodes are the same
            
return offsetA === offsetB : (offsetA offsetB) ? -1;
        } else if ( (
nodeC getClosestAncestorIn(nodeBnodeAtrue)) ) {

            
// Case 2: node C (container B or an ancestor) is a child node of A
            
return offsetA <= getNodeIndex(nodeC) ? -1;
        } else if ( (
nodeC getClosestAncestorIn(nodeAnodeBtrue)) ) {

            
// Case 3: node C (container A or an ancestor) is a child node of B
            
return getNodeIndex(nodeC) < offsetB  ? -1;
        } else {

            
// Case 4: containers are siblings or descendants of siblings
            
root getCommonAncestor(nodeAnodeB);
            
childA = (nodeA === root) ? root getClosestAncestorIn(nodeAroottrue);
            
childB = (nodeB === root) ? root getClosestAncestorIn(nodeBroottrue);

            if (
childA === childB) {
                
// This shouldn't be possible

                
throw new Error("comparePoints got to case 4 and childA and childB are the same!");
            } else {
                
root.firstChild;
                while (
n) {
                    if (
=== childA) {
                        return -
1;
                    } else if (
=== childB) {
                        return 
1;
                    }
                    
n.nextSibling;
                }
                throw new 
Error("Should not be here!");
            }
        }
    }

    function 
fragmentFromNodeChildren(node) {
        var 
fragment getDocument(node).createDocumentFragment(), child;
        while ( (
child node.firstChild) ) {
            
fragment.appendChild(child);
        }
        return 
fragment;
    }

    function 
inspectNode(node) {
        if (!
node) {
            return 
"[No node]";
        }
        if (
isCharacterDataNode(node)) {
            return 
'"' node.data '"';
        } else if (
node.nodeType == 1) {
            var 
idAttr node.id ' id="' node.id '"' "";
            return 
"<" node.nodeName idAttr ">[" node.childNodes.length "]";
        } else {
            return 
node.nodeName;
        }
    }

    
/**
     * @constructor
     */
    
function NodeIterator(root) {
        
this.root root;
        
this._next root;
    }

    
NodeIterator.prototype = {
        
_currentnull,

        
hasNext: function() {
            return !!
this._next;
        },

        
next: function() {
            var 
this._current this._next;
            var 
childnext;
            if (
this._current) {
                
child n.firstChild;
                if (
child) {
                    
this._next child;
                } else {
                    
next null;
                    while ((
!== this.root) && !(next n.nextSibling)) {
                        
n.parentNode;
                    }
                    
this._next next;
                }
            }
            return 
this._current;
        },

        
detach: function() {
            
this._current this._next this.root null;
        }
    };

    function 
createIterator(root) {
        return new 
NodeIterator(root);
    }

    
/**
     * @constructor
     */
    
function DomPosition(nodeoffset) {
        
this.node node;
        
this.offset offset;
    }

    
DomPosition.prototype = {
        
equals: function(pos) {
            return 
this.node === pos.node this.offset == pos.offset;
        },

        
inspect: function() {
            return 
"[DomPosition(" inspectNode(this.node) + ":" this.offset ")]";
        }
    };

    
/**
     * @constructor
     */
    
function DOMException(codeName) {
        
this.code this[codeName];
        
this.codeName codeName;
        
this.message "DOMException: " this.codeName;
    }

    
DOMException.prototype = {
        
INDEX_SIZE_ERR1,
        
HIERARCHY_REQUEST_ERR3,
        
WRONG_DOCUMENT_ERR4,
        
NO_MODIFICATION_ALLOWED_ERR7,
        
NOT_FOUND_ERR8,
        
NOT_SUPPORTED_ERR9,
        
INVALID_STATE_ERR11
    
};

    
DOMException.prototype.toString = function() {
        return 
this.message;
    };

    
api.dom = {
        
arrayContainsarrayContains,
        
isHtmlNamespaceisHtmlNamespace,
        
parentElementparentElement,
        
getNodeIndexgetNodeIndex,
        
getNodeLengthgetNodeLength,
        
getCommonAncestorgetCommonAncestor,
        
isAncestorOfisAncestorOf,
        
getClosestAncestorIngetClosestAncestorIn,
        
isCharacterDataNodeisCharacterDataNode,
        
insertAfterinsertAfter,
        
splitDataNodesplitDataNode,
        
getDocumentgetDocument,
        
getWindowgetWindow,
        
getIframeWindowgetIframeWindow,
        
getIframeDocumentgetIframeDocument,
        
getBodygetBody,
        
getRootContainergetRootContainer,
        
comparePointscomparePoints,
        
inspectNodeinspectNode,
        
fragmentFromNodeChildrenfragmentFromNodeChildren,
        
createIteratorcreateIterator,
        
DomPositionDomPosition
    
};

    
api.DOMException DOMException;
});
rangy.createModule("DomRange", function(apimodule) {
    
api.requireModules( ["DomUtil"] );


    var 
dom api.dom;
    var 
DomPosition dom.DomPosition;
    var 
DOMException api.DOMException;
    
    
/*----------------------------------------------------------------------------------------------------------------*/

    // Utility functions

    
function isNonTextPartiallySelected(noderange) {
        return (
node.nodeType != 3) &&
               (
dom.isAncestorOf(noderange.startContainertrue) || dom.isAncestorOf(noderange.endContainertrue));
    }

    function 
getRangeDocument(range) {
        return 
dom.getDocument(range.startContainer);
    }

    function 
dispatchEvent(rangetypeargs) {
        var 
listeners range._listeners[type];
        if (
listeners) {
            for (var 
0len listeners.lengthlen; ++i) {
                
listeners[i].call(range, {targetrangeargsargs});
            }
        }
    }

    function 
getBoundaryBeforeNode(node) {
        return new 
DomPosition(node.parentNodedom.getNodeIndex(node));
    }

    function 
getBoundaryAfterNode(node) {
        return new 
DomPosition(node.parentNodedom.getNodeIndex(node) + 1);
    }

    function 
insertNodeAtPosition(nodeno) {
        var 
firstNodeInserted node.nodeType == 11 node.firstChild node;
        if (
dom.isCharacterDataNode(n)) {
            if (
== n.length) {
                
dom.insertAfter(noden);
            } else {
                
n.parentNode.insertBefore(node== dom.splitDataNode(no));
            }
        } else if (
>= n.childNodes.length) {
            
n.appendChild(node);
        } else {
            
n.insertBefore(noden.childNodes[o]);
        }
        return 
firstNodeInserted;
    }

    function 
cloneSubtree(iterator) {
        var 
partiallySelected;
        for (var 
nodefrag getRangeDocument(iterator.range).createDocumentFragment(), subIteratornode iterator.next(); ) {
            
partiallySelected iterator.isPartiallySelectedSubtree();

            
node node.cloneNode(!partiallySelected);
            if (
partiallySelected) {
                
subIterator iterator.getSubtreeIterator();
                
node.appendChild(cloneSubtree(subIterator));
                
subIterator.detach(true);
            }

            if (
node.nodeType == 10) { // DocumentType
                
throw new DOMException("HIERARCHY_REQUEST_ERR");
            }
            
frag.appendChild(node);
        }
        return 
frag;
    }

    function 
iterateSubtree(rangeIteratorfunciteratorState) {
        var 
itn;
        
iteratorState iteratorState || { stopfalse };
        for (var 
nodesubRangeIteratornode rangeIterator.next(); ) {
            
//log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
            
if (rangeIterator.isPartiallySelectedSubtree()) {
                
// The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
                // node selected by the Range.
                
if (func(node) === false) {
                    
iteratorState.stop true;
                    return;
                } else {
                    
subRangeIterator rangeIterator.getSubtreeIterator();
                    
iterateSubtree(subRangeIteratorfunciteratorState);
                    
subRangeIterator.detach(true);
                    if (
iteratorState.stop) {
                        return;
                    }
                }
            } else {
                
// The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
                // descendant
                
it dom.createIterator(node);
                while ( (
it.next()) ) {
                    if (
func(n) === false) {
                        
iteratorState.stop true;
                        return;
                    }
                }
            }
        }
    }

    function 
deleteSubtree(iterator) {
        var 
subIterator;
        while (
iterator.next()) {
            if (
iterator.isPartiallySelectedSubtree()) {
                
subIterator iterator.getSubtreeIterator();
                
deleteSubtree(subIterator);
                
subIterator.detach(true);
            } else {
                
iterator.remove();
            }
        }
    }

    function 
extractSubtree(iterator) {

        for (var 
nodefrag getRangeDocument(iterator.range).createDocumentFragment(), subIteratornode iterator.next(); ) {


            if (
iterator.isPartiallySelectedSubtree()) {
                
node node.cloneNode(false);
                
subIterator iterator.getSubtreeIterator();
                
node.appendChild(extractSubtree(subIterator));
                
subIterator.detach(true);
            } else {
                
iterator.remove();
            }
            if (
node.nodeType == 10) { // DocumentType
                
throw new DOMException("HIERARCHY_REQUEST_ERR");
            }
            
frag.appendChild(node);
        }
        return 
frag;
    }

    function 
getNodesInRange(rangenodeTypesfilter) {
        
//log.info("getNodesInRange, " + nodeTypes.join(","));
        
var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
        var 
filterExists = !!filter;
        if (
filterNodeTypes) {
            
regex = new RegExp("^(" nodeTypes.join("|") + ")$");
        }

        var 
nodes = [];
        
iterateSubtree(new RangeIterator(rangefalse), function(node) {
            if ((!
filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
                
nodes.push(node);
            }
        });
        return 
nodes;
    }

    function 
inspect(range) {
        var 
name = (typeof range.getName == "undefined") ? "Range" range.getName();
        return 
"[" name "(" dom.inspectNode(range.startContainer) + ":" range.startOffset ", " +
                
dom.inspectNode(range.endContainer) + ":" range.endOffset ")]";
    }

    
/*----------------------------------------------------------------------------------------------------------------*/

    // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)

    /**
     * @constructor
     */
    
function RangeIterator(rangeclonePartiallySelectedTextNodes) {
        
this.range range;
        
this.clonePartiallySelectedTextNodes clonePartiallySelectedTextNodes;



        if (!
range.collapsed) {
            
this.sc range.startContainer;
            
this.so range.startOffset;
            
this.ec range.endContainer;
            
this.eo range.endOffset;
            var 
root range.commonAncestorContainer;

            if (
this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
                
this.isSingleCharacterDataNode true;
                
this._first this._last this._next this.sc;
            } else {
                
this._first this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
                    
this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.scroottrue);
                
this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
                    
this.ec.childNodes[this.eo 1] : dom.getClosestAncestorIn(this.ecroottrue);
            }

        }
    }

    
RangeIterator.prototype = {
        
_currentnull,
        
_nextnull,
        
_firstnull,
        
_lastnull,
        
isSingleCharacterDataNodefalse,

        
reset: function() {
            
this._current null;
            
this._next this._first;
        },

        
hasNext: function() {
            return !!
this._next;
        },

        
next: function() {
            
// Move to next node
            
var current this._current this._next;
            if (
current) {
                
this._next = (current !== this._last) ? current.nextSibling null;

                
// Check for partially selected text nodes
                
if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
                    if (
current === this.ec) {

                        (
current current.cloneNode(true)).deleteData(this.eocurrent.length this.eo);
                    }
                    if (
this._current === this.sc) {

                        (
current current.cloneNode(true)).deleteData(0this.so);
                    }
                }
            }

            return 
current;
        },

        
remove: function() {
            var 
current this._currentstartend;

            if (
dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
                
start = (current === this.sc) ? this.so 0;
                
end = (current === this.ec) ? this.eo current.length;
                if (
start != end) {
                    
current.deleteData(startend start);
                }
            } else {
                if (
current.parentNode) {
                    
current.parentNode.removeChild(current);
                } else {

                }
            }
        },

        
// Checks if the current node is partially selected
        
isPartiallySelectedSubtree: function() {
            var 
current this._current;
            return 
isNonTextPartiallySelected(currentthis.range);
        },

        
getSubtreeIterator: function() {
            var 
subRange;
            if (
this.isSingleCharacterDataNode) {
                
subRange this.range.cloneRange();
                
subRange.collapse();
            } else {
                
subRange = new Range(getRangeDocument(this.range));
                var 
current this._current;
                var 
startContainer currentstartOffset 0endContainer currentendOffset dom.getNodeLength(current);

                if (
dom.isAncestorOf(currentthis.sctrue)) {
                    
startContainer this.sc;
                    
startOffset this.so;
                }
                if (
dom.isAncestorOf(currentthis.ectrue)) {
                    
endContainer this.ec;
                    
endOffset this.eo;
                }

                
updateBoundaries(subRangestartContainerstartOffsetendContainerendOffset);
            }
            return new 
RangeIterator(subRangethis.clonePartiallySelectedTextNodes);
        },

        
detach: function(detachRange) {
            if (
detachRange) {
                
this.range.detach();
            }
            
this.range this._current this._next this._first this._last this.sc this.so this.ec this.eo null;
        }
    };

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Exceptions

    /**
     * @constructor
     */
    
function RangeException(codeName) {
        
this.code this[codeName];
        
this.codeName codeName;
        
this.message "RangeException: " this.codeName;
    }

    
RangeException.prototype = {
        
BAD_BOUNDARYPOINTS_ERR1,
        
INVALID_NODE_TYPE_ERR2
    
};

    
RangeException.prototype.toString = function() {
        return 
this.message;
    };

    
/*----------------------------------------------------------------------------------------------------------------*/

    /**
     * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
     * TODO: Look into making this a proper iterator, not requiring preloading everything first
     * @constructor
     */
    
function RangeNodeIterator(rangenodeTypesfilter) {
        
this.nodes getNodesInRange(rangenodeTypesfilter);
        
this._next this.nodes[0];
        
this._position 0;
    }

    
RangeNodeIterator.prototype = {
        
_currentnull,

        
hasNext: function() {
            return !!
this._next;
        },

        
next: function() {
            
this._current this._next;
            
this._next this.nodes[ ++this._position ];
            return 
this._current;
        },

        
detach: function() {
            
this._current this._next this.nodes null;
        }
    };

    var 
beforeAfterNodeTypes = [13457810];
    var 
rootContainerNodeTypes = [2911];
    var 
readonlyNodeTypes = [561012];
    var 
insertableNodeTypes = [1345781011];
    var 
surroundNodeTypes = [134578];

    function 
createAncestorFinder(nodeTypes) {
        return function(
nodeselfIsAncestor) {
            var 
tselfIsAncestor node node.parentNode;
            while (
n) {
                
n.nodeType;
                if (
dom.arrayContains(nodeTypest)) {
                    return 
n;
                }
                
n.parentNode;
            }
            return 
null;
        };
    }

    var 
getRootContainer dom.getRootContainer;
    var 
getDocumentOrFragmentContainer createAncestorFinder( [911] );
    var 
getReadonlyAncestor createAncestorFinder(readonlyNodeTypes);
    var 
getDocTypeNotationEntityAncestor createAncestorFinder( [61012] );

    function 
assertNoDocTypeNotationEntityAncestor(nodeallowSelf) {
        if (
getDocTypeNotationEntityAncestor(nodeallowSelf)) {
            throw new 
RangeException("INVALID_NODE_TYPE_ERR");
        }
    }

    function 
assertNotDetached(range) {
        if (!
range.startContainer) {
            throw new 
DOMException("INVALID_STATE_ERR");
        }
    }

    function 
assertValidNodeType(nodeinvalidTypes) {
        if (!
dom.arrayContains(invalidTypesnode.nodeType)) {
            throw new 
RangeException("INVALID_NODE_TYPE_ERR");
        }
    }

    function 
assertValidOffset(nodeoffset) {
        if (
offset || offset > (dom.isCharacterDataNode(node) ? node.length node.childNodes.length)) {
            throw new 
DOMException("INDEX_SIZE_ERR");
        }
    }

    function 
assertSameDocumentOrFragment(node1node2) {
        if (
getDocumentOrFragmentContainer(node1true) !== getDocumentOrFragmentContainer(node2true)) {
            throw new 
DOMException("WRONG_DOCUMENT_ERR");
        }
    }

    function 
assertNodeNotReadOnly(node) {
        if (
getReadonlyAncestor(nodetrue)) {
            throw new 
DOMException("NO_MODIFICATION_ALLOWED_ERR");
        }
    }

    function 
assertNode(nodecodeName) {
        if (!
node) {
            throw new 
DOMException(codeName);
        }
    }

    function 
isOrphan(node) {
        return !
dom.arrayContains(rootContainerNodeTypesnode.nodeType) && !getDocumentOrFragmentContainer(nodetrue);
    }

    function 
isValidOffset(nodeoffset) {
        return 
offset <= (dom.isCharacterDataNode(node) ? node.length node.childNodes.length);
    }

    function 
assertRangeValid(range) {
        
assertNotDetached(range);
        if (
isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
                !
isValidOffset(range.startContainerrange.startOffset) ||
                !
isValidOffset(range.endContainerrange.endOffset)) {
            throw new 
Error("Range error: Range is no longer valid after DOM mutation (" range.inspect() + ")");
        }
    }

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Test the browser's innerHTML support to decide how to implement createContextualFragment
    
var styleEl document.createElement("style");
    var 
htmlParsingConforms false;
    try {
        
styleEl.innerHTML "<b>x</b>";
        
htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
    
} catch (e) {
        
// IE 6 and 7 throw
    
}

    
api.features.htmlParsingConforms htmlParsingConforms;

    var 
createContextualFragment htmlParsingConforms ?

        
// Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
        // discussion and base code for this implementation at issue 67.
        // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
        // Thanks to Aleks Williams.
        
function(fragmentStr) {
            
// "Let node the context object's start's node."
            
var node this.startContainer;
            var 
doc dom.getDocument(node);

            
// "If the context object's start's node is null, raise an INVALID_STATE_ERR
            // exception and abort these steps."
            
if (!node) {
                throw new 
DOMException("INVALID_STATE_ERR");
            }

            
// "Let element be as follows, depending on node's interface:"
            // Document, Document Fragment: null
            
var el null;

            
// "Element: node"
            
if (node.nodeType == 1) {
                
el node;

            
// "Text, Comment: node's parentElement"
            
} else if (dom.isCharacterDataNode(node)) {
                
el dom.parentElement(node);
            }

            
// "If either element is null or element's ownerDocument is an HTML document
            // and element's local name is "html" and element's namespace is the HTML
            // namespace"
            
if (el === null || (
                
el.nodeName == "HTML"
                
&& dom.isHtmlNamespace(dom.getDocument(el).documentElement)
                && 
dom.isHtmlNamespace(el)
            )) {

            
// "let element be a new Element with "body" as its local name and the HTML
            // namespace as its namespace.""
                
el doc.createElement("body");
            } else {
                
el el.cloneNode(false);
            }

            
// "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
            // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
            // "In either case, the algorithm must be invoked with fragment as the input
            // and element as the context element."
            
el.innerHTML fragmentStr;

            
// "If this raises an exception, then abort these steps. Otherwise, let new
            // children be the nodes returned."

            // "Let fragment be a new DocumentFragment."
            // "Append all new children to fragment."
            // "Return fragment."
            
return dom.fragmentFromNodeChildren(el);
        } :

        
// In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
        // previous versions of Rangy used (with the exception of using a body element rather than a div)
        
function(fragmentStr) {
            
assertNotDetached(this);
            var 
doc getRangeDocument(this);
            var 
el doc.createElement("body");
            
el.innerHTML fragmentStr;

            return 
dom.fragmentFromNodeChildren(el);
        };

    
/*----------------------------------------------------------------------------------------------------------------*/

    
var rangeProperties = ["startContainer""startOffset""endContainer""endOffset""collapsed",
        
"commonAncestorContainer"];

    var 
s2s 0s2e 1e2e 2e2s 3;
    var 
n_b 0n_a 1n_b_a 2n_i 3;

    function 
RangePrototype() {}

    
RangePrototype.prototype = {
        
attachListener: function(typelistener) {
            
this._listeners[type].push(listener);
        },

        
compareBoundaryPoints: function(howrange) {
            
assertRangeValid(this);
            
assertSameDocumentOrFragment(this.startContainerrange.startContainer);

            var 
nodeAoffsetAnodeBoffsetB;
            var 
prefixA = (how == e2s || how == s2s) ? "start" "end";
            var 
prefixB = (how == s2e || how == s2s) ? "start" "end";
            
nodeA this[prefixA "Container"];
            
offsetA this[prefixA "Offset"];
            
nodeB range[prefixB "Container"];
            
offsetB range[prefixB "Offset"];
            return 
dom.comparePoints(nodeAoffsetAnodeBoffsetB);
        },

        
insertNode: function(node) {
            
assertRangeValid(this);
            
assertValidNodeType(nodeinsertableNodeTypes);
            
assertNodeNotReadOnly(this.startContainer);

            if (
dom.isAncestorOf(nodethis.startContainertrue)) {
                throw new 
DOMException("HIERARCHY_REQUEST_ERR");
            }

            
// No check for whether the container of the start of the Range is of a type that does not allow
            // children of the type of node: the browser's DOM implementation should do this for us when we attempt
            // to add the node

            
var firstNodeInserted insertNodeAtPosition(nodethis.startContainerthis.startOffset);
            
this.setStartBefore(firstNodeInserted);
        },

        
cloneContents: function() {
            
assertRangeValid(this);

            var clone, 
frag;
            if (
this.collapsed) {
                return 
getRangeDocument(this).createDocumentFragment();
            } else {
                if (
this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
                    clone = 
this.startContainer.cloneNode(true);
                    clone.
data = clone.data.slice(this.startOffsetthis.endOffset);
                    
frag getRangeDocument(this).createDocumentFragment();
                    
frag.appendChild(clone);
                    return 
frag;
                } else {
                    var 
iterator = new RangeIterator(thistrue);
                    clone = 
cloneSubtree(iterator);
                    
iterator.detach();
                }
                return clone;
            }
        },

        
canSurroundContents: function() {
            
assertRangeValid(this);
            
assertNodeNotReadOnly(this.startContainer);
            
assertNodeNotReadOnly(this.endContainer);

            
// Check if the contents can be surrounded. Specifically, this means whether the range partially selects
            // no non-text nodes.
            
var iterator = new RangeIterator(thistrue);
            var 
boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._firstthis)) ||
                    (
iterator._last && isNonTextPartiallySelected(iterator._lastthis)));
            
iterator.detach();
            return !
boundariesInvalid;
        },

        
surroundContents: function(node) {
            
assertValidNodeType(nodesurroundNodeTypes);

            if (!
this.canSurroundContents()) {
                throw new 
RangeException("BAD_BOUNDARYPOINTS_ERR");
            }

            
// Extract the contents
            
var content this.extractContents();

            
// Clear the children of the node
            
if (node.hasChildNodes()) {
                while (
node.lastChild) {
                    
node.removeChild(node.lastChild);
                }
            }

            
// Insert the new node and add the extracted contents
            
insertNodeAtPosition(nodethis.startContainerthis.startOffset);
            
node.appendChild(content);

            
this.selectNode(node);
        },

        
cloneRange: function() {
            
assertRangeValid(this);
            var 
range = new Range(getRangeDocument(this));
            var 
rangeProperties.lengthprop;
            while (
i--) {
                
prop rangeProperties[i];
                
range[prop] = this[prop];
            }
            return 
range;
        },

        
toString: function() {
            
assertRangeValid(this);
            var 
sc this.startContainer;
            if (
sc === this.endContainer && dom.isCharacterDataNode(sc)) {
                return (
sc.nodeType == || sc.nodeType == 4) ? sc.data.slice(this.startOffsetthis.endOffset) : "";
            } else {
                var 
textBits = [], iterator = new RangeIterator(thistrue);

                
iterateSubtree(iterator, function(node) {
                    
// Accept only text or CDATA nodes, not comments

                    
if (node.nodeType == || node.nodeType == 4) {
                        
textBits.push(node.data);
                    }
                });
                
iterator.detach();
                return 
textBits.join("");
            }
        },

        
// The methods below are all non-standard. The following batch were introduced by Mozilla but have since
        // been removed from Mozilla.

        
compareNode: function(node) {
            
assertRangeValid(this);

            var 
parent node.parentNode;
            var 
nodeIndex dom.getNodeIndex(node);

            if (!
parent) {
                throw new 
DOMException("NOT_FOUND_ERR");
            }

            var 
startComparison this.comparePoint(parentnodeIndex),
                
endComparison this.comparePoint(parentnodeIndex 1);

            if (
startComparison 0) { // Node starts before
                
return (endComparison 0) ? n_b_a n_b;
            } else {
                return (
endComparison 0) ? n_a n_i;
            }
        },

        
comparePoint: function(nodeoffset) {
            
assertRangeValid(this);
            
assertNode(node"HIERARCHY_REQUEST_ERR");
            
assertSameDocumentOrFragment(nodethis.startContainer);

            if (
dom.comparePoints(nodeoffsetthis.startContainerthis.startOffset) < 0) {
                return -
1;
            } else if (
dom.comparePoints(nodeoffsetthis.endContainerthis.endOffset) > 0) {
                return 
1;
            }
            return 
0;
        },

        
createContextualFragmentcreateContextualFragment,

        
toHtml: function() {
            
assertRangeValid(this);
            var 
container getRangeDocument(this).createElement("div");
            
container.appendChild(this.cloneContents());
            return 
container.innerHTML;
        },

        
// touchingIsIntersecting determines whether this method considers a node that borders a range intersects
        // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
        
intersectsNode: function(nodetouchingIsIntersecting) {
            
assertRangeValid(this);
            
assertNode(node"NOT_FOUND_ERR");
            if (
dom.getDocument(node) !== getRangeDocument(this)) {
                return 
false;
            }

            var 
parent node.parentNodeoffset dom.getNodeIndex(node);
            
assertNode(parent"NOT_FOUND_ERR");

            var 
startComparison dom.comparePoints(parentoffsetthis.endContainerthis.endOffset),
                
endComparison dom.comparePoints(parentoffset 1this.startContainerthis.startOffset);

            return 
touchingIsIntersecting startComparison <= && endComparison >= startComparison && endComparison 0;
        },


        
isPointInRange: function(nodeoffset) {
            
assertRangeValid(this);
            
assertNode(node"HIERARCHY_REQUEST_ERR");
            
assertSameDocumentOrFragment(nodethis.startContainer);

            return (
dom.comparePoints(nodeoffsetthis.startContainerthis.startOffset) >= 0) &&
                   (
dom.comparePoints(nodeoffsetthis.endContainerthis.endOffset) <= 0);
        },

        
// The methods below are non-standard and invented by me.

        // Sharing a boundary start-to-end or end-to-start does not count as intersection.
        
intersectsRange: function(rangetouchingIsIntersecting) {
            
assertRangeValid(this);

            if (
getRangeDocument(range) != getRangeDocument(this)) {
                throw new 
DOMException("WRONG_DOCUMENT_ERR");
            }

            var 
startComparison dom.comparePoints(this.startContainerthis.startOffsetrange.endContainerrange.endOffset),
                
endComparison dom.comparePoints(this.endContainerthis.endOffsetrange.startContainerrange.startOffset);

            return 
touchingIsIntersecting startComparison <= && endComparison >= startComparison && endComparison 0;
        },

        
intersection: function(range) {
            if (
this.intersectsRange(range)) {
                var 
startComparison dom.comparePoints(this.startContainerthis.startOffsetrange.startContainerrange.startOffset),
                    
endComparison dom.comparePoints(this.endContainerthis.endOffsetrange.endContainerrange.endOffset);

                var 
intersectionRange this.cloneRange();

                if (
startComparison == -1) {
                    
intersectionRange.setStart(range.startContainerrange.startOffset);
                }
                if (
endComparison == 1) {
                    
intersectionRange.setEnd(range.endContainerrange.endOffset);
                }
                return 
intersectionRange;
            }
            return 
null;
        },

        
union: function(range) {
            if (
this.intersectsRange(rangetrue)) {
                var 
unionRange this.cloneRange();
                if (
dom.comparePoints(range.startContainerrange.startOffsetthis.startContainerthis.startOffset) == -1) {
                    
unionRange.setStart(range.startContainerrange.startOffset);
                }
                if (
dom.comparePoints(range.endContainerrange.endOffsetthis.endContainerthis.endOffset) == 1) {
                    
unionRange.setEnd(range.endContainerrange.endOffset);
                }
                return 
unionRange;
            } else {
                throw new 
RangeException("Ranges do not intersect");
            }
        },

        
containsNode: function(nodeallowPartial) {
            if (
allowPartial) {
                return 
this.intersectsNode(nodefalse);
            } else {
                return 
this.compareNode(node) == n_i;
            }
        },

        
containsNodeContents: function(node) {
            return 
this.comparePoint(node0) >= && this.comparePoint(nodedom.getNodeLength(node)) <= 0;
        },

        
containsRange: function(range) {
            return 
this.intersection(range).equals(range);
        },

        
containsNodeText: function(node) {
            var 
nodeRange this.cloneRange();
            
nodeRange.selectNode(node);
            var 
textNodes nodeRange.getNodes([3]);
            if (
textNodes.length 0) {
                
nodeRange.setStart(textNodes[0], 0);
                var 
lastTextNode textNodes.pop();
                
nodeRange.setEnd(lastTextNodelastTextNode.length);
                var 
contains this.containsRange(nodeRange);
                
nodeRange.detach();
                return 
contains;
            } else {
                return 
this.containsNodeContents(node);
            }
        },

        
createNodeIterator: function(nodeTypesfilter) {
            
assertRangeValid(this);
            return new 
RangeNodeIterator(thisnodeTypesfilter);
        },

        
getNodes: function(nodeTypesfilter) {
            
assertRangeValid(this);
            return 
getNodesInRange(thisnodeTypesfilter);
        },

        
getDocument: function() {
            return 
getRangeDocument(this);
        },

        
collapseBefore: function(node) {
            
assertNotDetached(this);

            
this.setEndBefore(node);
            
this.collapse(false);
        },

        
collapseAfter: function(node) {
            
assertNotDetached(this);

            
this.setStartAfter(node);
            
this.collapse(true);
        },

        
getName: function() {
            return 
"DomRange";
        },

        
equals: function(range) {
            return 
Range.rangesEqual(thisrange);
        },

        
inspect: function() {
            return 
inspect(this);
        }
    };

    function 
copyComparisonConstantsToObject(obj) {
        
obj.START_TO_START s2s;
        
obj.START_TO_END s2e;
        
obj.END_TO_END e2e;
        
obj.END_TO_START e2s;

        
obj.NODE_BEFORE n_b;
        
obj.NODE_AFTER n_a;
        
obj.NODE_BEFORE_AND_AFTER n_b_a;
        
obj.NODE_INSIDE n_i;
    }

    function 
copyComparisonConstants(constructor) {
        
copyComparisonConstantsToObject(constructor);
        
copyComparisonConstantsToObject(constructor.prototype);
    }

    function 
createRangeContentRemover(removerboundaryUpdater) {
        return function() {
            
assertRangeValid(this);

            var 
sc this.startContainerso this.startOffsetroot this.commonAncestorContainer;

            var 
iterator = new RangeIterator(thistrue);

            
// Work out where to position the range after content removal
            
var nodeboundary;
            if (
sc !== root) {
                
node dom.getClosestAncestorIn(scroottrue);
                
boundary getBoundaryAfterNode(node);
                
sc boundary.node;
                
so boundary.offset;
            }

            
// Check none of the range is read-only
            
iterateSubtree(iteratorassertNodeNotReadOnly);

            
iterator.reset();

            
// Remove the content
            
var returnValue remover(iterator);
            
iterator.detach();

            
// Move to the new position
            
boundaryUpdater(thisscsoscso);

            return 
returnValue;
        };
    }

    function 
createPrototypeRange(constructorboundaryUpdaterdetacher) {
        function 
createBeforeAfterNodeSetter(isBeforeisStart) {
            return function(
node) {
                
assertNotDetached(this);
                
assertValidNodeType(nodebeforeAfterNodeTypes);
                
assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);

                var 
boundary = (isBefore getBoundaryBeforeNode getBoundaryAfterNode)(node);
                (
isStart setRangeStart setRangeEnd)(thisboundary.nodeboundary.offset);
            };
        }

        function 
setRangeStart(rangenodeoffset) {
            var 
ec range.endContainereo range.endOffset;
            if (
node !== range.startContainer || offset !== range.startOffset) {
                
// Check the root containers of the range and the new boundary, and also check whether the new boundary
                // is after the current end. In either case, collapse the range to the new position
                
if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(nodeoffseteceo) == 1) {
                    
ec node;
                    
eo offset;
                }
                
boundaryUpdater(rangenodeoffseteceo);
            }
        }

        function 
setRangeEnd(rangenodeoffset) {
            var 
sc range.startContainerso range.startOffset;
            if (
node !== range.endContainer || offset !== range.endOffset) {
                
// Check the root containers of the range and the new boundary, and also check whether the new boundary
                // is after the current end. In either case, collapse the range to the new position
                
if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(nodeoffsetscso) == -1) {
                    
sc node;
                    
so offset;
                }
                
boundaryUpdater(rangescsonodeoffset);
            }
        }

        function 
setRangeStartAndEnd(rangenodeoffset) {
            if (
node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
                
boundaryUpdater(rangenodeoffsetnodeoffset);
            }
        }

        
constructor.prototype = new RangePrototype();

        
api.util.extend(constructor.prototype, {
            
setStart: function(nodeoffset) {
                
assertNotDetached(this);
                
assertNoDocTypeNotationEntityAncestor(nodetrue);
                
assertValidOffset(nodeoffset);

                
setRangeStart(thisnodeoffset);
            },

            
setEnd: function(nodeoffset) {
                
assertNotDetached(this);
                
assertNoDocTypeNotationEntityAncestor(nodetrue);
                
assertValidOffset(nodeoffset);

                
setRangeEnd(thisnodeoffset);
            },

            
setStartBeforecreateBeforeAfterNodeSetter(truetrue),
            
setStartAftercreateBeforeAfterNodeSetter(falsetrue),
            
setEndBeforecreateBeforeAfterNodeSetter(truefalse),
            
setEndAftercreateBeforeAfterNodeSetter(falsefalse),

            
collapse: function(isStart) {
                
assertRangeValid(this);
                if (
isStart) {
                    
boundaryUpdater(thisthis.startContainerthis.startOffsetthis.startContainerthis.startOffset);
                } else {
                    
boundaryUpdater(thisthis.endContainerthis.endOffsetthis.endContainerthis.endOffset);
                }
            },

            
selectNodeContents: function(node) {
                
// This doesn't seem well specified: the spec talks only about selecting the node's contents, which
                // could be taken to mean only its children. However, browsers implement this the same as selectNode for
                // text nodes, so I shall do likewise
                
assertNotDetached(this);
                
assertNoDocTypeNotationEntityAncestor(nodetrue);

                
boundaryUpdater(thisnode0nodedom.getNodeLength(node));
            },

            
selectNode: function(node) {
                
assertNotDetached(this);
                
assertNoDocTypeNotationEntityAncestor(nodefalse);
                
assertValidNodeType(nodebeforeAfterNodeTypes);

                var 
start getBoundaryBeforeNode(node), end getBoundaryAfterNode(node);
                
boundaryUpdater(thisstart.nodestart.offsetend.nodeend.offset);
            },

            
extractContentscreateRangeContentRemover(extractSubtreeboundaryUpdater),

            
deleteContentscreateRangeContentRemover(deleteSubtreeboundaryUpdater),

            
canSurroundContents: function() {
                
assertRangeValid(this);
                
assertNodeNotReadOnly(this.startContainer);
                
assertNodeNotReadOnly(this.endContainer);

                
// Check if the contents can be surrounded. Specifically, this means whether the range partially selects
                // no non-text nodes.
                
var iterator = new RangeIterator(thistrue);
                var 
boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._firstthis)) ||
                        (
iterator._last && isNonTextPartiallySelected(iterator._lastthis)));
                
iterator.detach();
                return !
boundariesInvalid;
            },

            
detach: function() {
                
detacher(this);
            },

            
splitBoundaries: function() {
                
assertRangeValid(this);


                var 
sc this.startContainerso this.startOffsetec this.endContainereo this.endOffset;
                var 
startEndSame = (sc === ec);

                if (
dom.isCharacterDataNode(ec) && eo && eo ec.length) {
                    
dom.splitDataNode(eceo);

                }

                if (
dom.isCharacterDataNode(sc) && so && so sc.length) {

                    
sc dom.splitDataNode(scso);
                    if (
startEndSame) {
                        
eo -= so;
                        
ec sc;
                    } else if (
ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
                        
eo++;
                    }
                    
so 0;

                }
                
boundaryUpdater(thisscsoeceo);
            },

            
normalizeBoundaries: function() {
                
assertRangeValid(this);

                var 
sc this.startContainerso this.startOffsetec this.endContainereo this.endOffset;

                var 
mergeForward = function(node) {
                    var 
sibling node.nextSibling;
                    if (
sibling && sibling.nodeType == node.nodeType) {
                        
ec node;
                        
eo node.length;
                        
node.appendData(sibling.data);
                        
sibling.parentNode.removeChild(sibling);
                    }
                };

                var 
mergeBackward = function(node) {
                    var 
sibling node.previousSibling;
                    if (
sibling && sibling.nodeType == node.nodeType) {
                        
sc node;
                        var 
nodeLength node.length;
                        
so sibling.length;
                        
node.insertData(0sibling.data);
                        
sibling.parentNode.removeChild(sibling);
                        if (
sc == ec) {
                            
eo += so;
                            
ec sc;
                        } else if (
ec == node.parentNode) {
                            var 
nodeIndex dom.getNodeIndex(node);
                            if (
eo == nodeIndex) {
                                
ec node;
                                
eo nodeLength;
                            } else if (
eo nodeIndex) {
                                
eo--;
                            }
                        }
                    }
                };

                var 
normalizeStart true;

                if (
dom.isCharacterDataNode(ec)) {
                    if (
ec.length == eo) {
                        
mergeForward(ec);
                    }
                } else {
                    if (
eo 0) {
                        var 
endNode ec.childNodes[eo 1];
                        if (
endNode && dom.isCharacterDataNode(endNode)) {
                            
mergeForward(endNode);
                        }
                    }
                    
normalizeStart = !this.collapsed;
                }

                if (
normalizeStart) {
                    if (
dom.isCharacterDataNode(sc)) {
                        if (
so == 0) {
                            
mergeBackward(sc);
                        }
                    } else {
                        if (
so sc.childNodes.length) {
                            var 
startNode sc.childNodes[so];
                            if (
startNode && dom.isCharacterDataNode(startNode)) {
                                
mergeBackward(startNode);
                            }
                        }
                    }
                } else {
                    
sc ec;
                    
so eo;
                }

                
boundaryUpdater(thisscsoeceo);
            },

            
collapseToPoint: function(nodeoffset) {
                
assertNotDetached(this);

                
assertNoDocTypeNotationEntityAncestor(nodetrue);
                
assertValidOffset(nodeoffset);

                
setRangeStartAndEnd(thisnodeoffset);
            }
        });

        
copyComparisonConstants(constructor);
    }

    
/*----------------------------------------------------------------------------------------------------------------*/

    // Updates commonAncestorContainer and collapsed after boundary change
    
function updateCollapsedAndCommonAncestor(range) {
        
range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
        
range.commonAncestorContainer range.collapsed ?
            
range.startContainer dom.getCommonAncestor(range.startContainerrange.endContainer);
    }

    function 
updateBoundaries(rangestartContainerstartOffsetendContainerendOffset) {
        var 
startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
        var 
endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);

        
range.startContainer startContainer;
        
range.startOffset startOffset;
        
range.endContainer endContainer;
        
range.endOffset endOffset;

        
updateCollapsedAndCommonAncestor(range);
        
dispatchEvent(range"boundarychange", {startMovedstartMovedendMovedendMoved});
    }

    function 
detach(range) {
        
assertNotDetached(range);
        
range.startContainer range.startOffset range.endContainer range.endOffset null;
        
range.collapsed range.commonAncestorContainer null;
        
dispatchEvent(range"detach"null);
        
range._listeners null;
    }

    
/**
     * @constructor
     */
    
function Range(doc) {
        
this.startContainer doc;
        
this.startOffset 0;
        
this.endContainer doc;
        
this.endOffset 0;
        
this._listeners = {
            
boundarychange: [],
            
detach: []
        };
        
updateCollapsedAndCommonAncestor(this);
    }

    
createPrototypeRange(RangeupdateBoundariesdetach);

    
api.rangePrototype RangePrototype.prototype;

    
Range.rangeProperties rangeProperties;
    
Range.RangeIterator RangeIterator;
    
Range.copyComparisonConstants copyComparisonConstants;
    
Range.createPrototypeRange createPrototypeRange;
    
Range.inspect inspect;
    
Range.getRangeDocument getRangeDocument;
    
Range.rangesEqual = function(r1r2) {
        return 
r1.startContainer === r2.startContainer &&
               
r1.startOffset === r2.startOffset &&
               
r1.endContainer === r2.endContainer &&
               
r1.endOffset === r2.endOffset;
    };

    
api.DomRange Range;
    
api.RangeException RangeException;
});
rangy.createModule("WrappedRange", function(apimodule) {
    
api.requireModules( ["DomUtil""DomRange"] );

    
/**
     * @constructor
     */
    
var WrappedRange;
    var 
dom api.dom;
    var 
DomPosition dom.DomPosition;
    var 
DomRange api.DomRange;



    
/*----------------------------------------------------------------------------------------------------------------*/

    /*
    This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
    method. For example, in the following (where pipes denote the selection boundaries):

    <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>

    var range = document.selection.createRange();
    alert(range.parentElement().id); // Should alert "ul" but alerts "b"

    This method returns the common ancestor node of the following:
    - the parentElement() of the textRange
    - the parentElement() of the textRange after calling collapse(true)
    - the parentElement() of the textRange after calling collapse(false)
     */
    
function getTextRangeContainerElement(textRange) {
        var 
parentEl textRange.parentElement();

        var 
range textRange.duplicate();
        
range.collapse(true);
        var 
startEl range.parentElement();
        
range textRange.duplicate();
        
range.collapse(false);
        var 
endEl range.parentElement();
        var 
startEndContainer = (startEl == endEl) ? startEl dom.getCommonAncestor(startElendEl);

        return 
startEndContainer == parentEl startEndContainer dom.getCommonAncestor(parentElstartEndContainer);
    }

    function 
textRangeIsCollapsed(textRange) {
        return 
textRange.compareEndPoints("StartToEnd"textRange) == 0;
    }

    
// Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
    // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
    // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
    // for inputs and images, plus optimizations.
    
function getTextRangeBoundaryPosition(textRangewholeRangeContainerElementisStartisCollapsed) {
        var 
workingRange textRange.duplicate();

        
workingRange.collapse(isStart);
        var 
containerElement workingRange.parentElement();

        
// Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
        // check for that
        // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
        
if (!dom.isAncestorOf(wholeRangeContainerElementcontainerElementtrue)) {
            
containerElement wholeRangeContainerElement;

        }



        
// Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
        // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
        
if (!containerElement.canHaveHTML) {
            return new 
DomPosition(containerElement.parentNodedom.getNodeIndex(containerElement));
        }

        var 
workingNode dom.getDocument(containerElement).createElement("span");
        var 
comparisonworkingComparisonType isStart "StartToStart" "StartToEnd";
        var 
previousNodenextNodeboundaryPositionboundaryNode;

        
// Move the working range through the container's children, starting at the end and working backwards, until the
        // working range reaches or goes past the boundary we're interested in
        
do {
            
containerElement.insertBefore(workingNodeworkingNode.previousSibling);
            
workingRange.moveToElementText(workingNode);
        } while ( (
comparison workingRange.compareEndPoints(workingComparisonTypetextRange)) > &&
                
workingNode.previousSibling);

        
// We've now reached or gone past the boundary of the text range we're interested in
        // so have identified the node we want
        
boundaryNode workingNode.nextSibling;

        if (
comparison == -&& boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
            
// This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
            // node containing the text range's boundary, so we move the end of the working range to the boundary point
            // and measure the length of its text to get the boundary's offset within the node.
            
workingRange.setEndPoint(isStart "EndToStart" "EndToEnd"textRange);


            var 
offset;

            if (/[
rn]/.test(boundaryNode.data)) {
                
/*
                For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:

                - Each line break is represented as r in the text node's data/nodeValue properties
                - Each line break is represented as rn in the TextRange's 'text' property
                - The 'text' property of the TextRange does not contain trailing line breaks

                To get round the problem presented by the final fact above, we can use the fact that TextRange's
                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
                the same as the number of characters it was instructed to move. The simplest approach is to use this to
                store the characters moved when moving both the start and end of the range to the start of the document
                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
                However, this is extremely slow when the document is large and the range is near the end of it. Clearly
                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
                problem.

                Another approach that works is to use moveStart() to move the start boundary of the range up to the end
                boundary one character at a time and incrementing a counter with the value returned by the moveStart()
                call. However, the check for whether the start boundary has reached the end boundary is expensive, so
                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
                the range within the document).

                The method below is a hybrid of the two methods above. It uses the fact that a string containing the
                TextRange's 'text' property with each rn converted to a single r character cannot be longer than the
                text of the TextRange, so the start of the range is moved that length initially and then a character at
                a time to make up for any trailing line breaks not contained in the 'text' property. This has good
                performance in most situations compared to the previous two methods.
                */
                
var tempRange workingRange.duplicate();
                var 
rangeLength tempRange.text.replace(/rn/g"r").length;

                
offset tempRange.moveStart("character"rangeLength);
                while ( (
comparison tempRange.compareEndPoints("StartToEnd"tempRange)) == -1) {
                    
offset++;
                    
tempRange.moveStart("character"1);
                }
            } else {
                
offset workingRange.text.length;
            }
            
boundaryPosition = new DomPosition(boundaryNodeoffset);
        } else {


            
// If the boundary immediately follows a character data node and this is the end boundary, we should favour
            // a position within that, and likewise for a start boundary preceding a character data node
            
previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
            
nextNode = (isCollapsed || isStart) && workingNode.nextSibling;



            if (
nextNode && dom.isCharacterDataNode(nextNode)) {
                
boundaryPosition = new DomPosition(nextNode0);
            } else if (
previousNode && dom.isCharacterDataNode(previousNode)) {
                
boundaryPosition = new DomPosition(previousNodepreviousNode.length);
            } else {
                
boundaryPosition = new DomPosition(containerElementdom.getNodeIndex(workingNode));
            }
        }

        
// Clean up
        
workingNode.parentNode.removeChild(workingNode);

        return 
boundaryPosition;
    }

    
// Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
    // (http://code.google.com/p/ierange/)
    
function createBoundaryTextRange(boundaryPositionisStart) {
        var 
boundaryNodeboundaryParentboundaryOffset boundaryPosition.offset;
        var 
doc dom.getDocument(boundaryPosition.node);
        var 
workingNodechildNodesworkingRange doc.body.createTextRange();
        var 
nodeIsDataNode dom.isCharacterDataNode(boundaryPosition.node);

        if (
nodeIsDataNode) {
            
boundaryNode boundaryPosition.node;
            
boundaryParent boundaryNode.parentNode;
        } else {
            
childNodes boundaryPosition.node.childNodes;
            
boundaryNode = (boundaryOffset childNodes.length) ? childNodes[boundaryOffset] : null;
            
boundaryParent boundaryPosition.node;
        }

        
// Position the range immediately before the node containing the boundary
        
workingNode doc.createElement("span");

        
// Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
        // element rather than immediately before or after it, which is what we want
        
workingNode.innerHTML "&#feff;";

        
// insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
        
if (boundaryNode) {
            
boundaryParent.insertBefore(workingNodeboundaryNode);
        } else {
            
boundaryParent.appendChild(workingNode);
        }

        
workingRange.moveToElementText(workingNode);
        
workingRange.collapse(!isStart);

        
// Clean up
        
boundaryParent.removeChild(workingNode);

        
// Move the working range to the text offset, if required
        
if (nodeIsDataNode) {
            
workingRange[isStart "moveStart" "moveEnd"]("character"boundaryOffset);
        }

        return 
workingRange;
    }

    
/*----------------------------------------------------------------------------------------------------------------*/

    
if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
        
// This is a wrapper around the browser's native DOM Range. It has two aims:
        // - Provide workarounds for specific browser bugs
        // - provide convenient extensions, which are inherited from Rangy's DomRange

        
(function() {
            var 
rangeProto;
            var 
rangeProperties DomRange.rangeProperties;
            var 
canSetRangeStartAfterEnd;

            function 
updateRangeProperties(range) {
                var 
rangeProperties.lengthprop;
                while (
i--) {
                    
prop rangeProperties[i];
                    
range[prop] = range.nativeRange[prop];
                }
            }

            function 
updateNativeRange(rangestartContainerstartOffsetendContainer,endOffset) {
                var 
startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
                var 
endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);

                
// Always set both boundaries for the benefit of IE9 (see issue 35)
                
if (startMoved || endMoved) {
                    
range.setEnd(endContainerendOffset);
                    
range.setStart(startContainerstartOffset);
                }
            }

            function 
detach(range) {
                
range.nativeRange.detach();
                
range.detached true;
                var 
rangeProperties.lengthprop;
                while (
i--) {
                    
prop rangeProperties[i];
                    
range[prop] = null;
                }
            }

            var 
createBeforeAfterNodeSetter;

            
WrappedRange = function(range) {
                if (!
range) {
                    throw new 
Error("Range must be specified");
                }
                
this.nativeRange range;
                
updateRangeProperties(this);
            };

            
DomRange.createPrototypeRange(WrappedRangeupdateNativeRangedetach);

            
rangeProto WrappedRange.prototype;

            
rangeProto.selectNode = function(node) {
                
this.nativeRange.selectNode(node);
                
updateRangeProperties(this);
            };

            
rangeProto.deleteContents = function() {
                
this.nativeRange.deleteContents();
                
updateRangeProperties(this);
            };

            
rangeProto.extractContents = function() {
                var 
frag this.nativeRange.extractContents();
                
updateRangeProperties(this);
                return 
frag;
            };

            
rangeProto.cloneContents = function() {
                return 
this.nativeRange.cloneContents();
            };

            
// TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
            // insertNode, which works but is almost certainly slower than the native implementation.
/*
            rangeProto.insertNode = function(node) {
                this.nativeRange.insertNode(node);
                updateRangeProperties(this);
            };
*/

            
rangeProto.surroundContents = function(node) {
                
this.nativeRange.surroundContents(node);
                
updateRangeProperties(this);
            };

            
rangeProto.collapse = function(isStart) {
                
this.nativeRange.collapse(isStart);
                
updateRangeProperties(this);
            };

            
rangeProto.cloneRange = function() {
                return new 
WrappedRange(this.nativeRange.cloneRange());
            };

            
rangeProto.refresh = function() {
                
updateRangeProperties(this);
            };

            
rangeProto.toString = function() {
                return 
this.nativeRange.toString();
            };

            
// Create test range and node for feature detection

            
var testTextNode document.createTextNode("test");
            
dom.getBody(document).appendChild(testTextNode);
            var 
range document.createRange();

            
/*--------------------------------------------------------------------------------------------------------*/

            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
            // correct for it

            
range.setStart(testTextNode0);
            
range.setEnd(testTextNode0);

            try {
                
range.setStart(testTextNode1);
                
canSetRangeStartAfterEnd true;

                
rangeProto.setStart = function(nodeoffset) {
                    
this.nativeRange.setStart(nodeoffset);
                    
updateRangeProperties(this);
                };

                
rangeProto.setEnd = function(nodeoffset) {
                    
this.nativeRange.setEnd(nodeoffset);
                    
updateRangeProperties(this);
                };

                
createBeforeAfterNodeSetter = function(name) {
                    return function(
node) {
                        
this.nativeRange[name](node);
                        
updateRangeProperties(this);
                    };
                };

            } catch(
ex) {


                
canSetRangeStartAfterEnd false;

                
rangeProto.setStart = function(nodeoffset) {
                    try {
                        
this.nativeRange.setStart(nodeoffset);
                    } catch (
ex) {
                        
this.nativeRange.setEnd(nodeoffset);
                        
this.nativeRange.setStart(nodeoffset);
                    }
                    
updateRangeProperties(this);
                };

                
rangeProto.setEnd = function(nodeoffset) {
                    try {
                        
this.nativeRange.setEnd(nodeoffset);
                    } catch (
ex) {
                        
this.nativeRange.setStart(nodeoffset);
                        
this.nativeRange.setEnd(nodeoffset);
                    }
                    
updateRangeProperties(this);
                };

                
createBeforeAfterNodeSetter = function(nameoppositeName) {
                    return function(
node) {
                        try {
                            
this.nativeRange[name](node);
                        } catch (
ex) {
                            
this.nativeRange[oppositeName](node);
                            
this.nativeRange[name](node);
                        }
                        
updateRangeProperties(this);
                    };
                };
            }

            
rangeProto.setStartBefore createBeforeAfterNodeSetter("setStartBefore""setEndBefore");
            
rangeProto.setStartAfter createBeforeAfterNodeSetter("setStartAfter""setEndAfter");
            
rangeProto.setEndBefore createBeforeAfterNodeSetter("setEndBefore""setStartBefore");
            
rangeProto.setEndAfter createBeforeAfterNodeSetter("setEndAfter""setStartAfter");

            
/*--------------------------------------------------------------------------------------------------------*/

            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
            // the 0th character of the text node
            
range.selectNodeContents(testTextNode);
            if (
range.startContainer == testTextNode && range.endContainer == testTextNode &&
                    
range.startOffset == && range.endOffset == testTextNode.length) {
                
rangeProto.selectNodeContents = function(node) {
                    
this.nativeRange.selectNodeContents(node);
                    
updateRangeProperties(this);
                };
            } else {
                
rangeProto.selectNodeContents = function(node) {
                    
this.setStart(node0);
                    
this.setEnd(nodeDomRange.getEndOffset(node));
                };
            }

            
/*--------------------------------------------------------------------------------------------------------*/

            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738

            
range.selectNodeContents(testTextNode);
            
range.setEnd(testTextNode3);

            var 
range2 document.createRange();
            
range2.selectNodeContents(testTextNode);
            
range2.setEnd(testTextNode4);
            
range2.setStart(testTextNode2);

            if (
range.compareBoundaryPoints(range.START_TO_ENDrange2) == -&
                    
range.compareBoundaryPoints(range.END_TO_STARTrange2) == 1) {
                
// This is the wrong way round, so correct for it


                
rangeProto.compareBoundaryPoints = function(typerange) {
                    
range range.nativeRange || range;
                    if (
type == range.START_TO_END) {
                        
type range.END_TO_START;
                    } else if (
type == range.END_TO_START) {
                        
type range.START_TO_END;
                    }
                    return 
this.nativeRange.compareBoundaryPoints(typerange);
                };
            } else {
                
rangeProto.compareBoundaryPoints = function(typerange) {
                    return 
this.nativeRange.compareBoundaryPoints(typerange.nativeRange || range);
                };
            }

            
/*--------------------------------------------------------------------------------------------------------*/

            // Test for existence of createContextualFragment and delegate to it if it exists
            
if (api.util.isHostMethod(range"createContextualFragment")) {
                
rangeProto.createContextualFragment = function(fragmentStr) {
                    return 
this.nativeRange.createContextualFragment(fragmentStr);
                };
            }

            
/*--------------------------------------------------------------------------------------------------------*/

            // Clean up
            
dom.getBody(document).removeChild(testTextNode);
            
range.detach();
            
range2.detach();
        })();

        
api.createNativeRange = function(doc) {
            
doc doc || document;
            return 
doc.createRange();
        };
    } else if (
api.features.implementsTextRange) {
        
// This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
        // prototype

        
WrappedRange = function(textRange) {
            
this.textRange textRange;
            
this.refresh();
        };

        
WrappedRange.prototype = new DomRange(document);

        
WrappedRange.prototype.refresh = function() {
            var 
startend;

            
// TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
            
var rangeContainerElement getTextRangeContainerElement(this.textRange);

            if (
textRangeIsCollapsed(this.textRange)) {
                
end start getTextRangeBoundaryPosition(this.textRangerangeContainerElementtruetrue);
            } else {

                
start getTextRangeBoundaryPosition(this.textRangerangeContainerElementtruefalse);
                
end getTextRangeBoundaryPosition(this.textRangerangeContainerElementfalsefalse);
            }

            
this.setStart(start.nodestart.offset);
            
this.setEnd(end.nodeend.offset);
        };

        
DomRange.copyComparisonConstants(WrappedRange);

        
// Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
        
var globalObj = (function() { return this; })();
        if (
typeof globalObj.Range == "undefined") {
            
globalObj.Range WrappedRange;
        }

        
api.createNativeRange = function(doc) {
            
doc doc || document;
            return 
doc.body.createTextRange();
        };
    }

    if (
api.features.implementsTextRange) {
        
WrappedRange.rangeToTextRange = function(range) {
            if (
range.collapsed) {
                var 
tr createBoundaryTextRange(new DomPosition(range.startContainerrange.startOffset), true);



                return 
tr;

                
//return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
            
} else {
                var 
startRange createBoundaryTextRange(new DomPosition(range.startContainerrange.startOffset), true);
                var 
endRange createBoundaryTextRange(new DomPosition(range.endContainerrange.endOffset), false);
                var 
textRange dom.getDocument(range.startContainer).body.createTextRange();
                
textRange.setEndPoint("StartToStart"startRange);
                
textRange.setEndPoint("EndToEnd"endRange);
                return 
textRange;
            }
        };
    }

    
WrappedRange.prototype.getName = function() {
        return 
"WrappedRange";
    };

    
api.WrappedRange WrappedRange;

    
api.createRange = function(doc) {
        
doc doc || document;
        return new 
WrappedRange(api.createNativeRange(doc));
    };

    
api.createRangyRange = function(doc) {
        
doc doc || document;
        return new 
DomRange(doc);
    };

    
api.createIframeRange = function(iframeEl) {
        return 
api.createRange(dom.getIframeDocument(iframeEl));
    };

    
api.createIframeRangyRange = function(iframeEl) {
        return 
api.createRangyRange(dom.getIframeDocument(iframeEl));
    };

    
api.addCreateMissingNativeApiListener(function(win) {
        var 
doc win.document;
        if (
typeof doc.createRange == "undefined") {
            
doc.createRange = function() {
                return 
api.createRange(this);
            };
        }
        
doc win null;
    });
});
rangy.createModule("WrappedSelection", function(apimodule) {
    
// This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
    // spec (http://html5.org/specs/dom-range.html)

    
api.requireModules( ["DomUtil""DomRange""WrappedRange"] );

    
api.config.checkSelectionRanges true;

    var 
BOOLEAN "boolean",
        
windowPropertyName "_rangySelection",
        
dom api.dom,
        
util api.util,
        
DomRange api.DomRange,
        
WrappedRange api.WrappedRange,
        
DOMException api.DOMException,
        
DomPosition dom.DomPosition,
        
getSelection,
        
selectionIsCollapsed,
        
CONTROL "Control";



    function 
getWinSelection(winParam) {
        return (
winParam || window).getSelection();
    }

    function 
getDocSelection(winParam) {
        return (
winParam || window).document.selection;
    }

    
// Test for the Range/TextRange and Selection features required
    // Test for ability to retrieve selection
    
var implementsWinGetSelection api.util.isHostMethod(window"getSelection"),
        
implementsDocSelection api.util.isHostObject(document"selection");

    var 
useDocumentSelection implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);

    if (
useDocumentSelection) {
        
getSelection getDocSelection;
        
api.isSelectionValid = function(winParam) {
            var 
doc = (winParam || window).documentnativeSel doc.selection;

            
// Check whether the selection TextRange is actually contained within the correct document
            
return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
        };
    } else if (
implementsWinGetSelection) {
        
getSelection getWinSelection;
        
api.isSelectionValid = function() {
            return 
true;
        };
    } else {
        
module.fail("Neither document.selection or window.getSelection() detected.");
    }

    
api.getNativeSelection getSelection;

    var 
testSelection getSelection();
    var 
testRange api.createNativeRange(document);
    var 
body dom.getBody(document);

    
// Obtaining a range from a selection
    
var selectionHasAnchorAndFocus util.areHostObjects(testSelection, ["anchorNode""focusNode"] &&
                                     
util.areHostProperties(testSelection, ["anchorOffset""focusOffset"]));
    
api.features.selectionHasAnchorAndFocus selectionHasAnchorAndFocus;

    
// Test for existence of native selection extend() method
    
var selectionHasExtend util.isHostMethod(testSelection"extend");
    
api.features.selectionHasExtend selectionHasExtend;

    
// Test if rangeCount exists
    
var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
    
api.features.selectionHasRangeCount selectionHasRangeCount;

    var 
selectionSupportsMultipleRanges false;
    var 
collapsedNonEditableSelectionsSupported true;

    if (
util.areHostMethods(testSelection, ["addRange""getRangeAt""removeAllRanges"]) &&
            
typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {

        (function() {
            var 
iframe document.createElement("iframe");
            
body.appendChild(iframe);

            var 
iframeDoc dom.getIframeDocument(iframe);
            
iframeDoc.open();
            
iframeDoc.write("<html><head></head><body>12</body></html>");
            
iframeDoc.close();

            var 
sel dom.getIframeWindow(iframe).getSelection();
            var 
docEl iframeDoc.documentElement;
            var 
iframeBody docEl.lastChildtextNode iframeBody.firstChild;

            
// Test whether the native selection will allow a collapsed selection within a non-editable element
            
var r1 iframeDoc.createRange();
            
r1.setStart(textNode1);
            
r1.collapse(true);
            
sel.addRange(r1);
            
collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
            
sel.removeAllRanges();

            
// Test whether the native selection is capable of supporting multiple ranges
            
var r2 r1.cloneRange();
            
r1.setStart(textNode0);
            
r2.setEnd(textNode2);
            
sel.addRange(r1);
            
sel.addRange(r2);

            
selectionSupportsMultipleRanges = (sel.rangeCount == 2);

            
// Clean up
            
r1.detach();
            
r2.detach();

            
body.removeChild(iframe);
        })();
    }

    
api.features.selectionSupportsMultipleRanges selectionSupportsMultipleRanges;
    
api.features.collapsedNonEditableSelectionsSupported collapsedNonEditableSelectionsSupported;

    
// ControlRanges
    
var implementsControlRange falsetestControlRange;

    if (
body && util.isHostMethod(body"createControlRange")) {
        
testControlRange body.createControlRange();
        if (
util.areHostProperties(testControlRange, ["item""add"])) {
            
implementsControlRange true;
        }
    }
    
api.features.implementsControlRange implementsControlRange;

    
// Selection collapsedness
    
if (selectionHasAnchorAndFocus) {
        
selectionIsCollapsed = function(sel) {
            return 
sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
        };
    } else {
        
selectionIsCollapsed = function(sel) {
            return 
sel.rangeCount sel.getRangeAt(sel.rangeCount 1).collapsed false;
        };
    }

    function 
updateAnchorAndFocusFromRange(selrangebackwards) {
        var 
anchorPrefix backwards "end" "start"focusPrefix backwards "start" "end";
        
sel.anchorNode range[anchorPrefix "Container"];
        
sel.anchorOffset range[anchorPrefix "Offset"];
        
sel.focusNode range[focusPrefix "Container"];
        
sel.focusOffset range[focusPrefix "Offset"];
    }

    function 
updateAnchorAndFocusFromNativeSelection(sel) {
        var 
nativeSel sel.nativeSelection;
        
sel.anchorNode nativeSel.anchorNode;
        
sel.anchorOffset nativeSel.anchorOffset;
        
sel.focusNode nativeSel.focusNode;
        
sel.focusOffset nativeSel.focusOffset;
    }

    function 
updateEmptySelection(sel) {
        
sel.anchorNode sel.focusNode null;
        
sel.anchorOffset sel.focusOffset 0;
        
sel.rangeCount 0;
        
sel.isCollapsed true;
        
sel._ranges.length 0;
    }

    function 
getNativeRange(range) {
        var 
nativeRange;
        if (
range instanceof DomRange) {
            
nativeRange range._selectionNativeRange;
            if (!
nativeRange) {
                
nativeRange api.createNativeRange(dom.getDocument(range.startContainer));
                
nativeRange.setEnd(range.endContainerrange.endOffset);
                
nativeRange.setStart(range.startContainerrange.startOffset);
                
range._selectionNativeRange nativeRange;
                
range.attachListener("detach", function() {

                    
this._selectionNativeRange null;
                });
            }
        } else if (
range instanceof WrappedRange) {
            
nativeRange range.nativeRange;
        } else if (
api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
            
nativeRange range;
        }
        return 
nativeRange;
    }

    function 
rangeContainsSingleElement(rangeNodes) {
        if (!
rangeNodes.length || rangeNodes[0].nodeType != 1) {
            return 
false;
        }
        for (var 
1len rangeNodes.lengthlen; ++i) {
            if (!
dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
                return 
false;
            }
        }
        return 
true;
    }

    function 
getSingleElementFromRange(range) {
        var 
nodes range.getNodes();
        if (!
rangeContainsSingleElement(nodes)) {
            throw new 
Error("getSingleElementFromRange: range " range.inspect() + " did not consist of a single element");
        }
        return 
nodes[0];
    }

    function 
isTextRange(range) {
        return !!
range && typeof range.text != "undefined";
    }

    function 
updateFromTextRange(selrange) {
        
// Create a Range from the selected TextRange
        
var wrappedRange = new WrappedRange(range);
        
sel._ranges = [wrappedRange];

        
updateAnchorAndFocusFromRange(selwrappedRangefalse);
        
sel.rangeCount 1;
        
sel.isCollapsed wrappedRange.collapsed;
    }

    function 
updateControlSelection(sel) {
        
// Update the wrapped selection based on what's now in the native selection
        
sel._ranges.length 0;
        if (
sel.docSelection.type == "None") {
            
updateEmptySelection(sel);
        } else {
            var 
controlRange sel.docSelection.createRange();
            if (
isTextRange(controlRange)) {
                
// This case (where the selection type is "Control" and calling createRange() on the selection returns
                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
                // ControlRange have been removed from the ControlRange and removed from the document.
                
updateFromTextRange(selcontrolRange);
            } else {
                
sel.rangeCount controlRange.length;
                var 
rangedoc dom.getDocument(controlRange.item(0));
                for (var 
0sel.rangeCount; ++i) {
                    
range api.createRange(doc);
                    
range.selectNode(controlRange.item(i));
                    
sel._ranges.push(range);
                }
                
sel.isCollapsed sel.rangeCount == && sel._ranges[0].collapsed;
                
updateAnchorAndFocusFromRange(selsel._ranges[sel.rangeCount 1], false);
            }
        }
    }

    function 
addRangeToControlSelection(selrange) {
        var 
controlRange sel.docSelection.createRange();
        var 
rangeElement getSingleElementFromRange(range);

        
// Create a new ControlRange containing all the elements in the selected ControlRange plus the element
        // contained by the supplied range
        
var doc dom.getDocument(controlRange.item(0));
        var 
newControlRange dom.getBody(doc).createControlRange();
        for (var 
0len controlRange.lengthlen; ++i) {
            
newControlRange.add(controlRange.item(i));
        }
        try {
            
newControlRange.add(rangeElement);
        } catch (
ex) {
            throw new 
Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
        }
        
newControlRange.select();

        
// Update the wrapped selection based on what's now in the native selection
        
updateControlSelection(sel);
    }

    var 
getSelectionRangeAt;

    if (
util.isHostMethod(testSelection,  "getRangeAt")) {
        
getSelectionRangeAt = function(selindex) {
            try {
                return 
sel.getRangeAt(index);
            } catch(
ex) {
                return 
null;
            }
        };
    } else if (
selectionHasAnchorAndFocus) {
        
getSelectionRangeAt = function(sel) {
            var 
doc dom.getDocument(sel.anchorNode);
            var 
range api.createRange(doc);
            
range.setStart(sel.anchorNodesel.anchorOffset);
            
range.setEnd(sel.focusNodesel.focusOffset);

            
// Handle the case when the selection was selected backwards (from the end to the start in the
            // document)
            
if (range.collapsed !== this.isCollapsed) {
                
range.setStart(sel.focusNodesel.focusOffset);
                
range.setEnd(sel.anchorNodesel.anchorOffset);
            }

            return 
range;
        };
    }

    
/**
     * @constructor
     */
    
function WrappedSelection(selectiondocSelectionwin) {
        
this.nativeSelection selection;
        
this.docSelection docSelection;
        
this._ranges = [];
        
this.win win;
        
this.refresh();
    }

    
api.getSelection = function(win) {
        
win win || window;
        var 
sel win[windowPropertyName];
        var 
nativeSel getSelection(win), docSel implementsDocSelection getDocSelection(win) : null;
        if (
sel) {
            
sel.nativeSelection nativeSel;
            
sel.docSelection docSel;
            
sel.refresh(win);
        } else {
            
sel = new WrappedSelection(nativeSeldocSelwin);
            
win[windowPropertyName] = sel;
        }
        return 
sel;
    };

    
api.getIframeSelection = function(iframeEl) {
        return 
api.getSelection(dom.getIframeWindow(iframeEl));
    };

    var 
selProto WrappedSelection.prototype;

    function 
createControlSelection(selranges) {
        
// Ensure that the selection becomes of type "Control"
        
var doc dom.getDocument(ranges[0].startContainer);
        var 
controlRange dom.getBody(doc).createControlRange();
        for (var 
0elrangeCount; ++i) {
            
el getSingleElementFromRange(ranges[i]);
            try {
                
controlRange.add(el);
            } catch (
ex) {
                throw new 
Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
            }
        }
        
controlRange.select();

        
// Update the wrapped selection based on what's now in the native selection
        
updateControlSelection(sel);
    }

    
// Selecting a range
    
if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges""addRange"])) {
        
selProto.removeAllRanges = function() {
            
this.nativeSelection.removeAllRanges();
            
updateEmptySelection(this);
        };

        var 
addRangeBackwards = function(selrange) {
            var 
doc DomRange.getRangeDocument(range);
            var 
endRange api.createRange(doc);
            
endRange.collapseToPoint(range.endContainerrange.endOffset);
            
sel.nativeSelection.addRange(getNativeRange(endRange));
            
sel.nativeSelection.extend(range.startContainerrange.startOffset);
            
sel.refresh();
        };

        if (
selectionHasRangeCount) {
            
selProto.addRange = function(rangebackwards) {
                if (
implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
                    
addRangeToControlSelection(thisrange);
                } else {
                    if (
backwards && selectionHasExtend) {
                        
addRangeBackwards(thisrange);
                    } else {
                        var 
previousRangeCount;
                        if (
selectionSupportsMultipleRanges) {
                            
previousRangeCount this.rangeCount;
                        } else {
                            
this.removeAllRanges();
                            
previousRangeCount 0;
                        }
                        
this.nativeSelection.addRange(getNativeRange(range));

                        
// Check whether adding the range was successful
                        
this.rangeCount this.nativeSelection.rangeCount;

                        if (
this.rangeCount == previousRangeCount 1) {
                            
// The range was added successfully

                            // Check whether the range that we added to the selection is reflected in the last range extracted from
                            // the selection
                            
if (api.config.checkSelectionRanges) {
                                var 
nativeRange getSelectionRangeAt(this.nativeSelectionthis.rangeCount 1);
                                if (
nativeRange && !DomRange.rangesEqual(nativeRangerange)) {
                                    
// Happens in WebKit with, for example, a selection placed at the start of a text node
                                    
range = new WrappedRange(nativeRange);
                                }
                            }
                            
this._ranges[this.rangeCount 1] = range;
                            
updateAnchorAndFocusFromRange(thisrangeselectionIsBackwards(this.nativeSelection));
                            
this.isCollapsed selectionIsCollapsed(this);
                        } else {
                            
// The range was not added successfully. The simplest thing is to refresh
                            
this.refresh();
                        }
                    }
                }
            };
        } else {
            
selProto.addRange = function(rangebackwards) {
                if (
backwards && selectionHasExtend) {
                    
addRangeBackwards(thisrange);
                } else {
                    
this.nativeSelection.addRange(getNativeRange(range));
                    
this.refresh();
                }
            };
        }

        
selProto.setRanges = function(ranges) {
            if (
implementsControlRange && ranges.length 1) {
                
createControlSelection(thisranges);
            } else {
                
this.removeAllRanges();
                for (var 
0len ranges.lengthlen; ++i) {
                    
this.addRange(ranges[i]);
                }
            }
        };
    } else if (
util.isHostMethod(testSelection"empty") && util.isHostMethod(testRange"select") &&
               
implementsControlRange && useDocumentSelection) {

        
selProto.removeAllRanges = function() {
            
// Added try/catch as fix for issue #21
            
try {
                
this.docSelection.empty();

                
// Check for empty() not working (issue #24)
                
if (this.docSelection.type != "None") {
                    
// Work around failure to empty a control selection by instead selecting a TextRange and then
                    // calling empty()
                    
var doc;
                    if (
this.anchorNode) {
                        
doc dom.getDocument(this.anchorNode);
                    } else if (
this.docSelection.type == CONTROL) {
                        var 
controlRange this.docSelection.createRange();
                        if (
controlRange.length) {
                            
doc dom.getDocument(controlRange.item(0)).body.createTextRange();
                        }
                    }
                    if (
doc) {
                        var 
textRange doc.body.createTextRange();
                        
textRange.select();
                        
this.docSelection.empty();
                    }
                }
            } catch(
ex) {}
            
updateEmptySelection(this);
        };

        
selProto.addRange = function(range) {
            if (
this.docSelection.type == CONTROL) {
                
addRangeToControlSelection(thisrange);
            } else {
                
WrappedRange.rangeToTextRange(range).select();
                
this._ranges[0] = range;
                
this.rangeCount 1;
                
this.isCollapsed this._ranges[0].collapsed;
                
updateAnchorAndFocusFromRange(thisrangefalse);
            }
        };

        
selProto.setRanges = function(ranges) {
            
this.removeAllRanges();
            var 
rangeCount ranges.length;
            if (
rangeCount 1) {
                
createControlSelection(thisranges);
            } else if (
rangeCount) {
                
this.addRange(ranges[0]);
            }
        };
    } else {
        
module.fail("No means of selecting a Range or TextRange was found");
        return 
false;
    }

    
selProto.getRangeAt = function(index) {
        if (
index || index >= this.rangeCount) {
            throw new 
DOMException("INDEX_SIZE_ERR");
        } else {
            return 
this._ranges[index];
        }
    };

    var 
refreshSelection;

    if (
useDocumentSelection) {
        
refreshSelection = function(sel) {
            var 
range;
            if (
api.isSelectionValid(sel.win)) {
                
range sel.docSelection.createRange();
            } else {
                
range dom.getBody(sel.win.document).createTextRange();
                
range.collapse(true);
            }


            if (
sel.docSelection.type == CONTROL) {
                
updateControlSelection(sel);
            } else if (
isTextRange(range)) {
                
updateFromTextRange(selrange);
            } else {
                
updateEmptySelection(sel);
            }
        };
    } else if (
util.isHostMethod(testSelection"getRangeAt") && typeof testSelection.rangeCount == "number") {
        
refreshSelection = function(sel) {
            if (
implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
                
updateControlSelection(sel);
            } else {
                
sel._ranges.length sel.rangeCount sel.nativeSelection.rangeCount;
                if (
sel.rangeCount) {
                    for (var 
0len sel.rangeCountlen; ++i) {
                        
sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
                    }
                    
updateAnchorAndFocusFromRange(selsel._ranges[sel.rangeCount 1], selectionIsBackwards(sel.nativeSelection));
                    
sel.isCollapsed selectionIsCollapsed(sel);
                } else {
                    
updateEmptySelection(sel);
                }
            }
        };
    } else if (
selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
        
refreshSelection = function(sel) {
            var 
rangenativeSel sel.nativeSelection;
            if (
nativeSel.anchorNode) {
                
range getSelectionRangeAt(nativeSel0);
                
sel._ranges = [range];
                
sel.rangeCount 1;
                
updateAnchorAndFocusFromNativeSelection(sel);
                
sel.isCollapsed selectionIsCollapsed(sel);
            } else {
                
updateEmptySelection(sel);
            }
        };
    } else {
        
module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
        return 
false;
    }

    
selProto.refresh = function(checkForChanges) {
        var 
oldRanges checkForChanges this._ranges.slice(0) : null;
        
refreshSelection(this);
        if (
checkForChanges) {
            var 
oldRanges.length;
            if (
!= this._ranges.length) {
                return 
false;
            }
            while (
i--) {
                if (!
DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
                    return 
false;
                }
            }
            return 
true;
        }
    };

    
// Removal of a single range
    
var removeRangeManually = function(selrange) {
        var 
ranges sel.getAllRanges(), removed false;
        
sel.removeAllRanges();
        for (var 
0len ranges.lengthlen; ++i) {
            if (
removed || range !== ranges[i]) {
                
sel.addRange(ranges[i]);
            } else {
                
// According to the draft WHATWG Range spec, the same range may be added to the selection multiple
                // times. removeRange should only remove the first instance, so the following ensures only the first
                // instance is removed
                
removed true;
            }
        }
        if (!
sel.rangeCount) {
            
updateEmptySelection(sel);
        }
    };

    if (
implementsControlRange) {
        
selProto.removeRange = function(range) {
            if (
this.docSelection.type == CONTROL) {
                var 
controlRange this.docSelection.createRange();
                var 
rangeElement getSingleElementFromRange(range);

                
// Create a new ControlRange containing all the elements in the selected ControlRange minus the
                // element contained by the supplied range
                
var doc dom.getDocument(controlRange.item(0));
                var 
newControlRange dom.getBody(doc).createControlRange();
                var 
elremoved false;
                for (var 
0len controlRange.lengthlen; ++i) {
                    
el controlRange.item(i);
                    if (
el !== rangeElement || removed) {
                        
newControlRange.add(controlRange.item(i));
                    } else {
                        
removed true;
                    }
                }
                
newControlRange.select();

                
// Update the wrapped selection based on what's now in the native selection
                
updateControlSelection(this);
            } else {
                
removeRangeManually(thisrange);
            }
        };
    } else {
        
selProto.removeRange = function(range) {
            
removeRangeManually(thisrange);
        };
    }

    
// Detecting if a selection is backwards
    
var selectionIsBackwards;
    if (!
useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
        
selectionIsBackwards = function(sel) {
            var 
backwards false;
            if (
sel.anchorNode) {
                
backwards = (dom.comparePoints(sel.anchorNodesel.anchorOffsetsel.focusNodesel.focusOffset) == 1);
            }
            return 
backwards;
        };

        
selProto.isBackwards = function() {
            return 
selectionIsBackwards(this);
        };
    } else {
        
selectionIsBackwards selProto.isBackwards = function() {
            return 
false;
        };
    }

    
// Selection text
    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
    
selProto.toString = function() {

        var 
rangeTexts = [];
        for (var 
0len this.rangeCountlen; ++i) {
            
rangeTexts[i] = "" this._ranges[i];
        }
        return 
rangeTexts.join("");
    };

    function 
assertNodeInSameDocument(selnode) {
        if (
sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
            throw new 
DOMException("WRONG_DOCUMENT_ERR");
        }
    }

    
// No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
    
selProto.collapse = function(nodeoffset) {
        
assertNodeInSameDocument(thisnode);
        var 
range api.createRange(dom.getDocument(node));
        
range.collapseToPoint(nodeoffset);
        
this.removeAllRanges();
        
this.addRange(range);
        
this.isCollapsed true;
    };

    
selProto.collapseToStart = function() {
        if (
this.rangeCount) {
            var 
range this._ranges[0];
            
this.collapse(range.startContainerrange.startOffset);
        } else {
            throw new 
DOMException("INVALID_STATE_ERR");
        }
    };

    
selProto.collapseToEnd = function() {
        if (
this.rangeCount) {
            var 
range this._ranges[this.rangeCount 1];
            
this.collapse(range.endContainerrange.endOffset);
        } else {
            throw new 
DOMException("INVALID_STATE_ERR");
        }
    };

    
// The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
    // never used by Rangy.
    
selProto.selectAllChildren = function(node) {
        
assertNodeInSameDocument(thisnode);
        var 
range api.createRange(dom.getDocument(node));
        
range.selectNodeContents(node);
        
this.removeAllRanges();
        
this.addRange(range);
    };

    
selProto.deleteFromDocument = function() {
        
// Sepcial behaviour required for Control selections
        
if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
            var 
controlRange this.docSelection.createRange();
            var 
element;
            while (
controlRange.length) {
                
element controlRange.item(0);
                
controlRange.remove(element);
                
element.parentNode.removeChild(element);
            }
            
this.refresh();
        } else if (
this.rangeCount) {
            var 
ranges this.getAllRanges();
            
this.removeAllRanges();
            for (var 
0len ranges.lengthlen; ++i) {
                
ranges[i].deleteContents();
            }
            
// The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
            // range. Firefox moves the selection to where the final selected range was, so we emulate that
            
this.addRange(ranges[len 1]);
        }
    };

    
// The following are non-standard extensions
    
selProto.getAllRanges = function() {
        return 
this._ranges.slice(0);
    };

    
selProto.setSingleRange = function(range) {
        
this.setRanges( [range] );
    };

    
selProto.containsNode = function(nodeallowPartial) {
        for (var 
0len this._ranges.lengthlen; ++i) {
            if (
this._ranges[i].containsNode(nodeallowPartial)) {
                return 
true;
            }
        }
        return 
false;
    };

    
selProto.toHtml = function() {
        var 
html "";
        if (
this.rangeCount) {
            var 
container DomRange.getRangeDocument(this._ranges[0]).createElement("div");
            for (var 
0len this._ranges.lengthlen; ++i) {
                
container.appendChild(this._ranges[i].cloneContents());
            }
            
html container.innerHTML;
        }
        return 
html;
    };

    function 
inspect(sel) {
        var 
rangeInspects = [];
        var 
anchor = new DomPosition(sel.anchorNodesel.anchorOffset);
        var 
focus = new DomPosition(sel.focusNodesel.focusOffset);
        var 
name = (typeof sel.getName == "function") ? sel.getName() : "Selection";

        if (
typeof sel.rangeCount != "undefined") {
            for (var 
0len sel.rangeCountlen; ++i) {
                
rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
            }
        }
        return 
"[" name "(Ranges: " rangeInspects.join(", ") +
                
")(anchor: " anchor.inspect() + ", focus: " focus.inspect() + "]";

    }

    
selProto.getName = function() {
        return 
"WrappedSelection";
    };

    
selProto.inspect = function() {
        return 
inspect(this);
    };

    
selProto.detach = function() {
        
this.win[windowPropertyName] = null;
        
this.win this.anchorNode this.focusNode null;
    };

    
WrappedSelection.inspect inspect;

    
api.Selection WrappedSelection;

    
api.selectionPrototype selProto;

    
api.addCreateMissingNativeApiListener(function(win) {
        if (
typeof win.getSelection == "undefined") {
            
win.getSelection = function() {
                return 
api.getSelection(this);
            };
        }
        
win null;
    });
});
/*
    Base.js, version 1.1a
    Copyright 2006-2010, Dean Edwards
    License: http://www.opensource.org/licenses/mit-license.php
*/

var Base = function() {
    
// dummy
};

Base.extend = function(_instance_static) { // subclass
    
var extend Base.prototype.extend;
    
    
// build the prototype
    
Base._prototyping true;
    var 
proto = new this;
    
extend.call(proto_instance);
  
proto.base = function() {
    
// call this method from any other method to invoke that method's ancestor
  
};
    
delete Base._prototyping;
    
    
// create the wrapper for the constructor function
    //var constructor = proto.constructor.valueOf(); //-dean
    
var constructor proto.constructor;
    var 
klass proto.constructor = function() {
        if (!
Base._prototyping) {
            if (
this._constructing || this.constructor == klass) { // instantiation
                
this._constructing true;
                
constructor.apply(thisarguments);
                
delete this._constructing;
            } else if (
arguments[0] != null) { // casting
                
return (arguments[0].extend || extend).call(arguments[0], proto);
            }
        }
    };
    
    
// build the class interface
    
klass.ancestor this;
    
klass.extend this.extend;
    
klass.forEach = this.forEach;
    
klass.implement this.implement;
    
klass.prototype proto;
    
klass.toString this.toString;
    
klass.valueOf = function(type) {
        
//return (type == "object") ? klass : constructor; //-dean
        
return (type == "object") ? klass constructor.valueOf();
    };
    
extend.call(klass_static);
    
// class initialisation
    
if (typeof klass.init == "function"klass.init();
    return 
klass;
};

Base.prototype = {    
    
extend: function(sourcevalue) {
        if (
arguments.length 1) { // extending with a name/value pair
            
var ancestor this[source];
            if (
ancestor && (typeof value == "function") && // overriding a method?
                // the valueOf() comparison is to avoid circular references
                
(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
                /
bbaseb/.test(value)) {
                
// get the underlying method
                
var method value.valueOf();
                
// override
                
value = function() {
                    var 
previous this.base || Base.prototype.base;
                    
this.base ancestor;
                    var 
returnValue method.apply(thisarguments);
                    
this.base previous;
                    return 
returnValue;
                };
                
// point to the underlying method
                
value.valueOf = function(type) {
                    return (
type == "object") ? value method;
                };
                
value.toString Base.toString;
            }
            
this[source] = value;
        } else if (
source) { // extending with an object literal
            
var extend Base.prototype.extend;
            
// if this object has a customised extend method then use it
            
if (!Base._prototyping && typeof this != "function") {
                
extend this.extend || extend;
            }
            var 
proto = {toSourcenull};
            
// do the "toString" and other methods manually
            
var hidden = ["constructor""toString""valueOf"];
            
// if we are prototyping then include the constructor
            
var Base._prototyping 1;
            while (
key hidden[i++]) {
                if (
source[key] != proto[key]) {
                    
extend.call(thiskeysource[key]);

                }
            }
            
// copy each of the source object's properties to this object
            
for (var key in source) {
                if (!
proto[key]) extend.call(thiskeysource[key]);
            }
        }
        return 
this;
    }
};

// initialise
Base Base.extend({
    
constructor: function() {
        
this.extend(arguments[0]);
    }
}, {
    
ancestorObject,
    
version"1.1",
    
    forEach: function(
objectblockcontext) {
        for (var 
key in object) {
            if (
this.prototype[key] === undefined) {
                
block.call(contextobject[key], keyobject);
            }
        }
    },
        
    
implement: function() {
        for (var 
0arguments.lengthi++) {
            if (
typeof arguments[i] == "function") {
                
// if it's a function, call it
                
arguments[i](this.prototype);
            } else {
                
// add the interface using the extend method
                
this.prototype.extend(arguments[i]);
            }
        }
        return 
this;
    },
    
    
toString: function() {
        return 
String(this.valueOf());
    }
});
/**
 * Detect browser support for specific features
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 */
wysihtml5.browser = (function() {
  var 
userAgent   navigator.userAgent,
      
testElement document.createElement("div"),
      
// Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
      
isIE        userAgent.indexOf("MSIE")         !== -&& userAgent.indexOf("Opera") === -1,
      
isGecko     userAgent.indexOf("Gecko")        !== -&& userAgent.indexOf("KHTML") === -1,
      
isWebKit    userAgent.indexOf("AppleWebKit/") !== -1,
      
isOpera     userAgent.indexOf("Opera/")       !== -1;
  
  return {
    
// Static variable needed, publicly accessible, to be able override it in unit tests
    
USER_AGENTuserAgent,
    
    
/**
     * Exclude browsers that are not capable of displaying and handling
     * contentEditable as desired:
     *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
     *    - IE < 8 create invalid markup and crash randomly from time to time
     *
     * @return {Boolean}
     */
    
supported: function() {
      var 
userAgent                   this.USER_AGENT.toLowerCase(),
          
// Essential for making html elements editable
          
hasContentEditableSupport   "contentEditable" in testElement,
          
// Following methods are needed in order to interact with the contentEditable area
          
hasEditingApiSupport        document.execCommand && document.queryCommandSupported && document.queryCommandState,
          
// document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
          
hasQuerySelectorSupport     document.querySelector && document.querySelectorAll,
          
// contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile)
          
isIncompatibleMobileBrowser = (userAgent.indexOf("webkit") !== -&& userAgent.indexOf("mobile") !== -1) || userAgent.indexOf("opera mobi") !== -1;

      return 
hasContentEditableSupport
        
&& hasEditingApiSupport
        
&& hasQuerySelectorSupport
        
&& !isIncompatibleMobileBrowser;
    },

    
/**
     * Whether the browser supports sandboxed iframes
     * Currently only IE 6+ offers such feature <iframe security="restricted">
     *
     * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
     * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
     *
     * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
     */
    
supportsSandboxedIframes: function() {
      return 
isIE;
    },

    
/**
     * IE6+7 throw a mixed content warning when the src of an iframe
     * is empty/unset or about:blank
     * window.querySelector is implemented as of IE8
     */
    
throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
      return !(
"querySelector" in document);
    },

    
/**
     * Whether the caret is correctly displayed in contentEditable elements
     * Firefox sometimes shows a huge caret in the beginning after focusing
     */
    
displaysCaretInEmptyContentEditableCorrectly: function() {
      return !
isGecko;
    },

    
/**
     * Opera and IE are the only browsers who offer the css value
     * in the original unit, thx to the currentStyle object
     * All other browsers provide the computed style in px via window.getComputedStyle
     */
    
hasCurrentStyleProperty: function() {
      return 
"currentStyle" in testElement;
    },

    
/**
     * Whether the browser inserts a <br> when pressing enter in a contentEditable element
     */
    
insertsLineBreaksOnReturn: function() {
      return 
isGecko;
    },

    
supportsPlaceholderAttributeOn: function(element) {
      return 
"placeholder" in element;
    },

    
supportsEvent: function(eventName) {
      return 
"on" eventName in testElement || (function() {
        
testElement.setAttribute("on" eventName"return;");
        return 
typeof(testElement["on" eventName]) === "function";
      })();
    },

    
/**
     * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
     */
    
supportsEventsInIframeCorrectly: function() {
      return !
isOpera;
    },

    
/**
     * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled
     * with event.preventDefault
     * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs
     * to be cancelled
     */
    
firesOnDropOnlyWhenOnDragOverIsCancelled: function() {
      return 
isWebKit || isGecko;
    },
    
    
/**
     * Whether the browser supports the event.dataTransfer property in a proper way
     */
    
supportsDataTransfer: function() {
      try {
        
// Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)
        
return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;
      } catch(
e) {
        return 
false;
      }
    },

    
/**
     * Everything below IE9 doesn't know how to treat HTML5 tags
     *
     * @param {Object} context The document object on which to check HTML5 support
     *
     * @example
     *    wysihtml5.browser.supportsHTML5Tags(document);
     */
    
supportsHTML5Tags: function(context) {
      var 
element context.createElement("div"),
          
html5   "<article>foo</article>";
      
element.innerHTML html5;
      return 
element.innerHTML.toLowerCase() === html5;
    },

    
/**
     * Checks whether a document supports a certain queryCommand
     * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
     * in oder to report correct results
     *
     * @param {Object} doc Document object on which to check for a query command
     * @param {String} command The query command to check for
     * @return {Boolean}
     *
     * @example
     *    wysihtml5.browser.supportsCommand(document, "bold");
     */
    
supportsCommand: (function() {
      
// Following commands are supported but contain bugs in some browsers
      
var buggyCommands = {
        
// formatBlock fails with some tags (eg. <blockquote>)
        
"formatBlock":          isIE,
         
// When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
         // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
         // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
        
"insertUnorderedList":  isIE || isOpera,
        
"insertOrderedList":    isIE || isOpera
      
};

      return function(
doccommand) {
        var 
isBuggy buggyCommands[command];
        if (!
isBuggy) {
          
// Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
          
try {
            return 
doc.queryCommandSupported(command);
          } catch(
e1) {}

          try {
            return 
doc.queryCommandEnabled(command);
          } catch(
e2) {}
        }
        return 
false;
      };
    })(),

    
/**
     * IE: URLs starting with:
     *    www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
     *    nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
     * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
     * space bar when the caret is directly after such an url.
     * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
     * (related blog post on msdn
     * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
     */
    
doesAutoLinkingInContentEditable: function() {
      return 
isIE;
    },

    
/**
     * As stated above, IE auto links urls typed into contentEditable elements
     * Since IE9 it's possible to prevent this behavior
     */
    
canDisableAutoLinking: function() {
      return 
this.supportsCommand(document"AutoUrlDetect");
    },

    
/**
     * IE leaves an empty paragraph in the contentEditable element after clearing it
     * Chrome/Safari sometimes an empty <div>
     */
    
clearsContentEditableCorrectly: function() {
      return 
isGecko || isOpera || isWebKit;
    },

    
/**
     * IE gives wrong results for getAttribute
     */
    
supportsGetAttributeCorrectly: function() {
      var 
td document.createElement("td");
      return 
td.getAttribute("rowspan") != "1";
    },

    
/**
     * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
     * Chrome and Safari both don't support this
     */
    
canSelectImagesInContentEditable: function() {
      return 
isGecko || isIE || isOpera;
    },

    
/**
     * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container
     * pressing backspace doesn't remove the entire list as done in other browsers
     */
    
clearsListsInContentEditableCorrectly: function() {
      return 
isGecko || isIE || isWebKit;
    },

    
/**
     * All browsers except Safari and Chrome automatically scroll the range/caret position into view
     */
    
autoScrollsToCaret: function() {
      return !
isWebKit;
    },

    
/**
     * Check whether the browser automatically closes tags that don't need to be opened
     */
    
autoClosesUnclosedTags: function() {
      var 
clonedTestElement testElement.cloneNode(false),
          
returnValue,
          
innerHTML;

      
clonedTestElement.innerHTML "<p><div></div>";
      
innerHTML                   clonedTestElement.innerHTML.toLowerCase();
      
returnValue                 innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";

      
// Cache result by overwriting current function
      
this.autoClosesUnclosedTags = function() { return returnValue; };

      return 
returnValue;
    },

    
/**
     * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
     */
    
supportsNativeGetElementsByClassName: function() {
      return 
String(document.getElementsByClassName).indexOf("[native code]") !== -1;
    },

    
/**
     * As of now (19.04.2011) only supported by Firefox 4 and Chrome
     * See https://developer.mozilla.org/en/DOM/Selection/modify
     */
    
supportsSelectionModify: function() {
      return 
"getSelection" in window && "modify" in window.getSelection();
    },
    
    
/**
     * Whether the browser supports the classList object for fast className manipulation
     * See https://developer.mozilla.org/en/DOM/element.classList
     */
    
supportsClassList: function() {
      return 
"classList" in testElement;
    },
    
    
/**
     * Opera needs a white space after a <br> in order to position the caret correctly
     */
    
needsSpaceAfterLineBreak: function() {
      return 
isOpera;
    },
    
    
/**
     * Whether the browser supports the speech api on the given element
     * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
     *
     * @example
     *    var input = document.createElement("input");
     *    if (wysihtml5.browser.supportsSpeechApiOn(input)) {
     *      // ...
     *    }
     */
    
supportsSpeechApiOn: function(input) {
      var 
chromeVersion userAgent.match(/Chrome/(d+)/) || [, 0];
      return 
chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
    },
    
    
/**
     * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
     * See https://connect.microsoft.com/ie/feedback/details/650112
     * or try the POC http://tifftiff.de/ie9_crash/
     */
    
crashesWhenDefineProperty: function(property) {
      return 
isIE && (property === "XMLHttpRequest" || property === "XDomainRequest");
    }
  };
})();
wysihtml5.lang.array = function(arr) {
  return {
    
/**
     * Check whether a given object exists in an array
     *
     * @example
     *    wysihtml5.lang.array([1, 2]).contains(1);
     *    // => true
     */
    
contains: function(needle) {
      if (
arr.indexOf) {
        return 
arr.indexOf(needle) !== -1;
      } else {
        for (var 
i=0length=arr.lengthi<lengthi++) {
          if (
arr[i] === needle) { return true; }
        }
        return 
false;
      }
    },
    
    
/**
     * Substract one array from another
     *
     * @example
     *    wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
     *    // => [1, 2]
     */
    
without: function(arrayToSubstract) {
      
arrayToSubstract wysihtml5.lang.array(arrayToSubstract);
      var 
newArr  = [],
          
i       0,
          
length  arr.length;
      for (; 
i<lengthi++) {
        if (!
arrayToSubstract.contains(arr[i])) {
          
newArr.push(arr[i]);
        }
      }
      return 
newArr;
    },
    
    
/**
     * Return a clean native array
     * 
     * Following will convert a Live NodeList to a proper Array
     * @example
     *    var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
     */
    
get: function() {
      var 
i        0,
          
length   arr.length,
          
newArray = [];
      for (; 
i<lengthi++) {
        
newArray.push(arr[i]);
      }
      return 
newArray;
    }
  };
};
wysihtml5.lang.Dispatcher Base.extend(
  
/** @scope wysihtml5.lang.Dialog.prototype */ {
  
observe: function(eventNamehandler) {
    
this.events this.events || {};
    
this.events[eventName] = this.events[eventName] || [];
    
this.events[eventName].push(handler);
    return 
this;
  },

  
fire: function(eventNamepayload) {
    
this.events this.events || {};
    var 
handlers this.events[eventName] || [],
        
i        0;
    for (; 
i<handlers.lengthi++) {
      
handlers[i].call(thispayload);
    }
    return 
this;
  },

  
stopObserving: function(eventNamehandler) {
    
this.events this.events || {};
    var 
0,
        
handlers,
        
newHandlers;
    if (
eventName) {
      
handlers    this.events[eventName] || [],
      
newHandlers = [];
      for (; 
i<handlers.lengthi++) {
        if (
handlers[i] !== handler) {
          
newHandlers.push(handlers[i]);
        }
      }
      
this.events[eventName] = newHandlers;
    } else {
      
// Clean up all events
      
this.events = {};
    }
    return 
this;
  }
});
wysihtml5.lang.object = function(obj) {
  return {
    
/**
     * @example
     *    wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
     *    // => { foo: 1, bar: 2, baz: 3 }
     */
    
merge: function(otherObj) {
      for (var 
i in otherObj) {
        
obj[i] = otherObj[i];
      }
      return 
this;
    },
    
    
get: function() {
      return 
obj;
    },
    
    
/**
     * @example
     *    wysihtml5.lang.object({ foo: 1 }).clone();
     *    // => { foo: 1 }
     */
    
clone: function() {
      var 
newObj = {},
          
i;
      for (
i in obj) {
        
newObj[i] = obj[i];
      }
      return 
newObj;
    },
    
    
/**
     * @example
     *    wysihtml5.lang.object([]).isArray();
     *    // => true
     */
    
isArray: function() {
      return 
Object.prototype.toString.call(obj) === "[object Array]";
    }
  };
};(function() {
  var 
WHITE_SPACE_START = /^s+/,
      
WHITE_SPACE_END   = /s+$/;
  
wysihtml5.lang.string = function(str) {
    
str String(str);
    return {
      
/**
       * @example
       *    wysihtml5.lang.string("   foo   ").trim();
       *    // => "foo"
       */
      
trim: function() {
        return 
str.replace(WHITE_SPACE_START"").replace(WHITE_SPACE_END"");
      },
      
      
/**
       * @example
       *    wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
       *    // => "Hello Christopher"
       */
      
interpolate: function(vars) {
        for (var 
i in vars) {
          
str this.replace("#{" "}").by(vars[i]);
        }
        return 
str;
      },
      
      
/**
       * @example
       *    wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
       *    // => "Hello Hans"
       */
      
replace: function(search) {
        return {
          
by: function(replace) {
            return 
str.split(search).join(replace);
          }
        }
      }
    };
  };
})();
/**
 * Find urls in descendant text nodes of an element and auto-links them
 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
 *
 * @param {Element} element Container element in which to search for urls
 * @author Christopher Blum <christopher.blum@xing.com>
 * @example
 *    <div id="text-container">Please click here: www.google.com</div>
 *    <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
 */
(function(wysihtml5) {
  var 
/**
       * Don't auto-link urls that are contained in the following elements:
       */
      
IGNORE_URLS_IN        wysihtml5.lang.array(["CODE""PRE""A""SCRIPT""HEAD""TITLE""STYLE"]),
      
/**
       * revision 1:
       *    /(S+.{1}[^s,.!]+)/g
       *
       * revision 2:
       *    /(b(((https?|ftp)://)|(www.))[-A-Z0-9+&@#/%?=~_|!:,.;[]]*[-A-Z0-9+&@#/%=~_|])/gim
       *
       * put this in the beginning if you don't wan't to match within a word
       *    (^|[>({[s>])
       */
      
URL_REG_EXP           = /((https?://|www.)[^s<]{3,})/gi,
      
TRAILING_CHAR_REG_EXP = /([^w/-](,?))$/i,
      
MAX_DISPLAY_LENGTH    100,
      
BRACKETS              = { ")""(""]""[""}""{" };
  
  function 
autoLink(element) {
    if (
_hasParentThatShouldBeIgnored(element)) {
      return 
element;
    }

    if (
element === element.ownerDocument.documentElement) {
      
element element.ownerDocument.body;
    }

    return 
_parseNode(element);
  }
  
  
/**
   * This is basically a rebuild of
   * the rails auto_link_urls text helper
   */
  
function _convertUrlsToLinks(str) {
    return 
str.replace(URL_REG_EXP, function(matchurl) {
      var 
punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
          
opening     BRACKETS[punctuation];
      
url url.replace(TRAILING_CHAR_REG_EXP"");

      if (
url.split(opening).length url.split(punctuation).length) {
        
url url punctuation;
        
punctuation "";
      }
      var 
realUrl    url,
          
displayUrl url;
      if (
url.length MAX_DISPLAY_LENGTH) {
        
displayUrl displayUrl.substr(0MAX_DISPLAY_LENGTH) + "...";
      }
      
// Add http prefix if necessary
      
if (realUrl.substr(04) === "www.") {
        
realUrl "http://" realUrl;
      }
      
      return 
'<a href="' realUrl '">' displayUrl '</a>' punctuation;
    });
  }
  
  
/**
   * Creates or (if already cached) returns a temp element
   * for the given document object
   */
  
function _getTempElement(context) {
    var 
tempElement context._wysihtml5_tempElement;
    if (!
tempElement) {
      
tempElement context._wysihtml5_tempElement context.createElement("div");
    }
    return 
tempElement;
  }
  
  
/**
   * Replaces the original text nodes with the newly auto-linked dom tree
   */
  
function _wrapMatchesInNode(textNode) {
    var 
parentNode  textNode.parentNode,
        
tempElement _getTempElement(parentNode.ownerDocument);
    
    
// We need to insert an empty/temporary <span /> to fix IE quirks
    // Elsewise IE would strip white space in the beginning
    
tempElement.innerHTML "<span></span>" _convertUrlsToLinks(textNode.data);
    
tempElement.removeChild(tempElement.firstChild);
    
    while (
tempElement.firstChild) {
      
// inserts tempElement.firstChild before textNode
      
parentNode.insertBefore(tempElement.firstChildtextNode);
    }
    
parentNode.removeChild(textNode);
  }
  
  function 
_hasParentThatShouldBeIgnored(node) {
    var 
nodeName;
    while (
node.parentNode) {
      
node node.parentNode;
      
nodeName node.nodeName;
      if (
IGNORE_URLS_IN.contains(nodeName)) {
        return 
true;
      } else if (
nodeName === "body") {
        return 
false;
      }
    }
    return 
false;
  }
  
  function 
_parseNode(element) {
    if (
IGNORE_URLS_IN.contains(element.nodeName)) {
      return;
    }
    
    if (
element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
      
_wrapMatchesInNode(element);
      return;
    }
    
    var 
childNodes        wysihtml5.lang.array(element.childNodes).get(),
        
childNodesLength  childNodes.length,
        
i                 0;
    
    for (; 
i<childNodesLengthi++) {
      
_parseNode(childNodes[i]);
    }
    
    return 
element;
  }
  
  
wysihtml5.dom.autoLink autoLink;
  
  
// Reveal url reg exp to the outside
  
wysihtml5.dom.autoLink.URL_REG_EXP URL_REG_EXP;
})(
wysihtml5);(function(wysihtml5) {
  var 
supportsClassList wysihtml5.browser.supportsClassList(),
      
api               wysihtml5.dom;
  
  
api.addClass = function(elementclassName) {
    if (
supportsClassList) {
      return 
element.classList.add(className);
    }
    if (
api.hasClass(elementclassName)) {
      return;
    }
    
element.className += " " className;
  };
  
  
api.removeClass = function(elementclassName) {
    if (
supportsClassList) {
      return 
element.classList.remove(className);
    }
    
    
element.className element.className.replace(new RegExp("(^|\s+)" className "(\s+|$)"), " ");
  };
  
  
api.hasClass = function(elementclassName) {
    if (
supportsClassList) {
      return 
element.classList.contains(className);
    }
    
    var 
elementClassName element.className;
    return (
elementClassName.length && (elementClassName == className || new RegExp("(^|\s)" className "(\s|$)").test(elementClassName)));
  };
})(
wysihtml5);
wysihtml5.dom.contains = (function() {
  var 
documentElement document.documentElement;
  if (
documentElement.contains) {
    return function(
containerelement) {
      if (
element.nodeType !== wysihtml5.ELEMENT_NODE) {
        
element element.parentNode;
      }
      return 
container !== element && container.contains(element);
    };
  } else if (
documentElement.compareDocumentPosition) {
    return function(
containerelement) {
      
// https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
      
return !!(container.compareDocumentPosition(element) & 16);
    };
  }
})();
/**
 * Converts an HTML fragment/element into a unordered/ordered list
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} element The element which should be turned into a list
 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
 * @return {Element} The created list
 *
 * @example
 *    <!-- Assume the following dom: -->
 *    <span id="pseudo-list">
 *      eminem<br>
 *      dr. dre
 *      <div>50 Cent</div>
 *    </span>
 *
 *    <script>
 *      wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
 *    </script>
 *
 *    <!-- Will result in: -->
 *    <ul>
 *      <li>eminem</li>
 *      <li>dr. dre</li>
 *      <li>50 Cent</li>
 *    </ul>
 */
wysihtml5.dom.convertToList = (function() {
  function 
_createListItem(doc, list) {
    var 
listItem doc.createElement("li");
    list.
appendChild(listItem);
    return 
listItem;
  }
  
  function 
_createList(doctype) {
    return 
doc.createElement(type);
  }
  
  function 
convertToList(elementlistType) {
    if (
element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
      
// Already a list
      
return element;
    }
    
    var 
doc               element.ownerDocument,
        list              = 
_createList(doclistType),
        
childNodes        wysihtml5.lang.array(element.childNodes).get(),
        
childNodesLength  childNodes.length,
        
childNode,
        
isBlockElement,
        
isLineBreak,
        
currentListItem,
        
i                 0;
    for (; 
i<childNodesLengthi++) {
      
currentListItem currentListItem || _createListItem(doc, list);
      
childNode       childNodes[i];
      
isBlockElement  wysihtml5.dom.getStyle("display").from(childNode) === "block";
      
isLineBreak     childNode.nodeName === "BR";
      
      if (
isBlockElement) {
        
// Append blockElement to current <li> if empty, otherwise create a new one
        
currentListItem currentListItem.firstChild _createListItem(doc, list) : currentListItem;
        
currentListItem.appendChild(childNode);
        
currentListItem null;
        continue;
      }
      
      if (
isLineBreak) {
        
// Only create a new list item in the next iteration when the current one has already content
        
currentListItem currentListItem.firstChild null currentListItem;
        continue;
      }
      
      
currentListItem.appendChild(childNode);
    }
    
    
element.parentNode.replaceChild(list, element);
    return list;
  }
  
  return 
convertToList;
})();
/**
 * Copy a set of attributes from one element to another
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Array} attributesToCopy List of attributes which should be copied
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
 *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 
 *    with the element where to copy the attributes to (see example)
 *
 * @example
 *    var textarea    = document.querySelector("textarea"),
 *        div         = document.querySelector("div[contenteditable=true]"),
 *        anotherDiv  = document.querySelector("div.preview");
 *    wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
 *
 */
wysihtml5.dom.copyAttributes = function(attributesToCopy) {
  return {
    
from: function(elementToCopyFrom) {
      return {
        
to: function(elementToCopyTo) {
          var 
attribute,
              
i         0,
              
length    attributesToCopy.length;
          for (; 
i<lengthi++) {
            
attribute attributesToCopy[i];
            if (
elementToCopyFrom[attribute]) {
              
elementToCopyTo[attribute] = elementToCopyFrom[attribute];
            }
          }
          return { 
andToarguments.callee };
        }
      };
    }
  };
};
/**
 * Copy a set of styles from one element to another
 * Please note that this only works properly across browsers when the element from which to copy the styles
 * is in the dom
 *
 * Interesting article on how to copy styles
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Array} stylesToCopy List of styles which should be copied
 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
 *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked 
 *    with the element where to copy the styles to (see example)
 *
 * @example
 *    var textarea    = document.querySelector("textarea"),
 *        div         = document.querySelector("div[contenteditable=true]"),
 *        anotherDiv  = document.querySelector("div.preview");
 *    wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
 *
 */
(function(dom) {
  
  
/**
   * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
   * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 
   * its computed css width will be 198px
   */
  
var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing""-moz-box-sizing""-ms-box-sizing""box-sizing"];
  
  var 
shouldIgnoreBoxSizingBorderBox = function(element) {
    if (
hasBoxSizingBorderBox(element)) {
       return 
parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
    }
    return 
false;
  };
  
  var 
hasBoxSizingBorderBox = function(element) {
    var 
i       0,
        
length  BOX_SIZING_PROPERTIES.length;
    for (; 
i<lengthi++) {
      if (
dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
        return 
BOX_SIZING_PROPERTIES[i];
      }
    }
  };
  
  
dom.copyStyles = function(stylesToCopy) {
    return {
      
from: function(element) {
        if (
shouldIgnoreBoxSizingBorderBox(element)) {
          
stylesToCopy wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
        }
        
        var 
cssText "",
            
length  stylesToCopy.length,
            
i       0,
            
property;
        for (; 
i<lengthi++) {
          
property stylesToCopy[i];
          
cssText += property ":" dom.getStyle(property).from(element) + ";";
        }
        
        return {
          
to: function(element) {
            
dom.setStyles(cssText).on(element);
            return { 
andToarguments.callee };
          }
        };
      }
    };
  };
})(
wysihtml5.dom);/**
 * Event Delegation
 *
 * @example
 *    wysihtml5.dom.delegate(document.body, "a", "click", function() {
 *      // foo
 *    });
 */
(function(wysihtml5) {
  
  
wysihtml5.dom.delegate = function(containerselectoreventNamehandler) {
    return 
wysihtml5.dom.observe(containereventName, function(event) {
      var 
target    event.target,
          
match     wysihtml5.lang.array(container.querySelectorAll(selector));
      
      while (
target && target !== container) {
        if (
match.contains(target)) {
          
handler.call(targetevent);
          break;
        }
        
target target.parentNode;
      }
    });
  };
  
})(
wysihtml5);/**
 * Returns the given html wrapped in a div element
 *
 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
 * when inserted via innerHTML
 * 
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {String} html The html which should be wrapped in a dom element
 * @param {Obejct} [context] Document object of the context the html belongs to
 *
 * @example
 *    wysihtml5.dom.getAsDom("<article>foo</article>");
 */
wysihtml5.dom.getAsDom = (function() {
  
  var 
_innerHTMLShiv = function(htmlcontext) {
    var 
tempElement context.createElement("div");
    
tempElement.style.display "none";
    
context.body.appendChild(tempElement);
    
// IE throws an exception when trying to insert <frameset></frameset> via innerHTML
    
try { tempElement.innerHTML html; } catch(e) {}
    
context.body.removeChild(tempElement);
    return 
tempElement;
  };
  
  
/**
   * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
   */
  
var _ensureHTML5Compatibility = function(context) {
    if (
context._wysihtml5_supportsHTML5Tags) {
      return;
    }
    for (var 
i=0length=HTML5_ELEMENTS.lengthi<lengthi++) {
      
context.createElement(HTML5_ELEMENTS[i]);
    }
    
context._wysihtml5_supportsHTML5Tags true;
  };
  
  
  
/**
   * List of html5 tags
   * taken from http://simon.html5.org/html5-elements
   */
  
var HTML5_ELEMENTS = [
    
"abbr""article""aside""audio""bdi""canvas""command""datalist""details""figcaption",
    
"figure""footer""header""hgroup""keygen""mark""meter""nav""output""progress",
    
"rp""rt""ruby""svg""section""source""summary""time""track""video""wbr"
  
];
  
  return function(
htmlcontext) {
    
context context || document;
    var 
tempElement;
    if (
typeof(html) === "object" && html.nodeType) {
      
tempElement context.createElement("div");
      
tempElement.appendChild(html);
    } else if (
wysihtml5.browser.supportsHTML5Tags(context)) {
      
tempElement context.createElement("div");
      
tempElement.innerHTML html;
    } else {
      
_ensureHTML5Compatibility(context);
      
tempElement _innerHTMLShiv(htmlcontext);
    }
    return 
tempElement;
  };
})();
/**
 * Walks the dom tree from the given node up until it finds a match
 * Designed for optimal performance.
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} node The from which to check the parent nodes
 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
 * @example
 *    var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
 *    // ... or ...
 *    var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
 *    // ... or ...
 *    var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
 */
wysihtml5.dom.getParentElement = (function() {
  
  function 
_isSameNodeName(nodeNamedesiredNodeNames) {
    if (!
desiredNodeNames || !desiredNodeNames.length) {
      return 
true;
    }
    
    if (
typeof(desiredNodeNames) === "string") {
      return 
nodeName === desiredNodeNames;
    } else {
      return 
wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
    }
  }
  
  function 
_isElement(node) {
    return 
node.nodeType === wysihtml5.ELEMENT_NODE;
  }
  
  function 
_hasClassName(elementclassNameclassRegExp) {
    var 
classNames = (element.className || "").match(classRegExp) || [];
    if (!
className) {
      return !!
classNames.length;
    }
    return 
classNames[classNames.length 1] === className;
  }
  
  function 
_getParentElementWithNodeName(nodenodeNamelevels) {
    while (
levels-- && node && node.nodeName !== "BODY") {
      if (
_isSameNodeName(node.nodeNamenodeName)) {
        return 
node;
      }
      
node node.parentNode;
    }
    return 
null;
  }
  
  function 
_getParentElementWithNodeNameAndClassName(nodenodeNameclassNameclassRegExplevels) {
    while (
levels-- && node && node.nodeName !== "BODY") {
      if (
_isElement(node) &&
          
_isSameNodeName(node.nodeNamenodeName) &&
          
_hasClassName(nodeclassNameclassRegExp)) {
        return 
node;
      }
      
node node.parentNode;
    }
    return 
null;
  }
  
  return function(
nodematchingSetlevels) {
    
levels levels || 50// Go max 50 nodes upwards from current node
    
if (matchingSet.className || matchingSet.classRegExp) {
      return 
_getParentElementWithNodeNameAndClassName(
        
nodematchingSet.nodeNamematchingSet.classNamematchingSet.classRegExplevels
      
);
    } else {
      return 
_getParentElementWithNodeName(
        
nodematchingSet.nodeNamelevels
      
);
    }
  };
})();
/**
 * Get element's style for a specific css property
 *
 * @param {Element} element The element on which to retrieve the style
 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
 * @author Christopher Blum <christopher.blum@xing.com>
 * @example
 *    wysihtml5.dom.getStyle("display").from(document.body);
 *    // => "block"
 */
wysihtml5.dom.getStyle = (function() {
  var 
stylePropertyMapping = {
        
"float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" "cssFloat"
      
},
      
REG_EXP_CAMELIZE = /-[a-z]/g;
  
  function 
camelize(str) {
    return 
str.replace(REG_EXP_CAMELIZE, function(match) {
      return 
match.charAt(1).toUpperCase();
    });
  }
  
  return function(
property) {
    return {
      
from: function(element) {
        if (
element.nodeType !== wysihtml5.ELEMENT_NODE) {
          return;
        }
        
        var 
doc               element.ownerDocument,
            
camelizedProperty stylePropertyMapping[property] || camelize(property),
            
style             element.style,
            
currentStyle      element.currentStyle,
            
styleValue        style[camelizedProperty];
        if (
styleValue) {
          return 
styleValue;
        }
        
        
// currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
        // window.getComputedStyle, since it returns css property values in their original unit:
        // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
        // gives you the original "50%".
        // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
        
if (currentStyle) {
          return 
currentStyle[camelizedProperty];
        }

        var 
win                 doc.defaultView || doc.parentWindow,
            
needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
            
originalOverflow,
            
returnValue;

        if (
win.getComputedStyle) {
          
// Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
          // therfore we remove and restore the scrollbar and calculate the value in between
          
if (needsOverflowReset) {
            
originalOverflow style.overflow;
            
style.overflow "hidden";
          }
          
returnValue win.getComputedStyle(elementnull).getPropertyValue(property);
          if (
needsOverflowReset) {
            
style.overflow originalOverflow || "";
          }
          return 
returnValue;
        }
      }
    };
  };
})();
/**
 * High performant way to check whether an element with a specific tag name is in the given document
 * Optimized for being heavily executed
 * Unleashes the power of live node lists
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Object} doc The document object of the context where to check
 * @param {String} tagName Upper cased tag name
 * @example
 *    wysihtml5.dom.hasElementWithTagName(document, "IMG");
 */
wysihtml5.dom.hasElementWithTagName = (function() {
  var 
LIVE_CACHE          = {},
      
DOCUMENT_IDENTIFIER 1;
  
  function 
_getDocumentIdentifier(doc) {
    return 
doc._wysihtml5_identifier || (doc._wysihtml5_identifier DOCUMENT_IDENTIFIER++);
  }
  
  return function(
doctagName) {
    var 
key         _getDocumentIdentifier(doc) + ":" tagName,
        
cacheEntry  LIVE_CACHE[key];
    if (!
cacheEntry) {
      
cacheEntry LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
    }
    
    return 
cacheEntry.length 0;
  };
})();
/**
 * High performant way to check whether an element with a specific class name is in the given document
 * Optimized for being heavily executed
 * Unleashes the power of live node lists
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Object} doc The document object of the context where to check
 * @param {String} tagName Upper cased tag name
 * @example
 *    wysihtml5.dom.hasElementWithClassName(document, "foobar");
 */
(function(wysihtml5) {
  var 
LIVE_CACHE          = {},
      
DOCUMENT_IDENTIFIER 1;

  function 
_getDocumentIdentifier(doc) {
    return 
doc._wysihtml5_identifier || (doc._wysihtml5_identifier DOCUMENT_IDENTIFIER++);
  }
  
  
wysihtml5.dom.hasElementWithClassName = function(docclassName) {
    
// getElementsByClassName is not supported by IE<9
    // but is sometimes mocked via library code (which then doesn't return live node lists)
    
if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
      return !!
doc.querySelector("." className);
    }

    var 
key         _getDocumentIdentifier(doc) + ":" className,
        
cacheEntry  LIVE_CACHE[key];
    if (!
cacheEntry) {
      
cacheEntry LIVE_CACHE[key] = doc.getElementsByClassName(className);
    }

    return 
cacheEntry.length 0;
  };
})(
wysihtml5);
wysihtml5.dom.insert = function(elementToInsert) {
  return {
    
after: function(element) {
      
element.parentNode.insertBefore(elementToInsertelement.nextSibling);
    },
    
    
before: function(element) {
      
element.parentNode.insertBefore(elementToInsertelement);
    },
    
    
into: function(element) {
      
element.appendChild(elementToInsert);
    }
  };
};
wysihtml5.dom.insertCSS = function(rules) {
  
rules rules.join("n");
  
  return {
    
into: function(doc) {
      var 
head         doc.head || doc.getElementsByTagName("head")[0],
          
styleElement doc.createElement("style");

      
styleElement.type "text/css";

      if (
styleElement.styleSheet) {
        
styleElement.styleSheet.cssText rules;
      } else {
        
styleElement.appendChild(doc.createTextNode(rules));
      }

      if (
head) {
        
head.appendChild(styleElement);
      }
    }
  };
};
/**
 * Method to set dom events
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @example
 *    wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
 */
wysihtml5.dom.observe = function(elementeventNameshandler) {
  
eventNames typeof(eventNames) === "string" ? [eventNames] : eventNames;
  
  var 
handlerWrapper,
      
eventName,
      
i       0,
      
length  eventNames.length;
  
  for (; 
i<lengthi++) {
    
eventName eventNames[i];
    if (
element.addEventListener) {
      
element.addEventListener(eventNamehandlerfalse);
    } else {
      
handlerWrapper = function(event) {
        if (!(
"target" in event)) {
          
event.target event.srcElement;
        }
        
event.preventDefault event.preventDefault || function() {
          
this.returnValue false;
        };
        
event.stopPropagation event.stopPropagation || function() {
          
this.cancelBubble true;
        };
        
handler.call(elementevent);
      };
      
element.attachEvent("on" eventNamehandlerWrapper);
    }
  }
  
  return {
    
stop: function() {
      var 
eventName,
          
i       0,
          
length  eventNames.length;
      for (; 
i<lengthi++) {
        
eventName eventNames[i];
        if (
element.removeEventListener) {
          
element.removeEventListener(eventNamehandlerfalse);
        } else {
          
element.detachEvent("on" eventNamehandlerWrapper);
        }
      }
    }
  };
};
/**
 * HTML Sanitizer
 * Rewrites the HTML based on given rules
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
 *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
 *    desired substitution.
 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
 *
 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
 *
 * @example
 *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
 *    wysihtml5.dom.parse(userHTML, {
 *      tags {
 *        p:      "div",      // Rename p tags to div tags
 *        font:   "span"      // Rename font tags to span tags
 *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
 *        script: undefined   // Remove script elements
 *      }
 *    });
 *    // => <div><div><span>foo bar</span></div></div>
 *
 *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
 *    wysihtml5.dom.parse(userHTML);
 *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
 *
 *    var userHTML = '<div>foobar<br>foobar</div>';
 *    wysihtml5.dom.parse(userHTML, {
 *      tags: {
 *        div: undefined,
 *        br:  true
 *      }
 *    });
 *    // => ''
 *
 *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
 *    wysihtml5.dom.parse(userHTML, {
 *      classes: {
 *        red:    1,
 *        green:  1
 *      },
 *      tags: {
 *        div: {
 *          rename_tag:     "p"
 *        }
 *      }
 *    });
 *    // => '<p class="red">foo</p><p>bar</p>'
 */
wysihtml5.dom.parse = (function() {
  
  
/**
   * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
   * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
   * node isn't closed
   *
   * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
   */
  
var NODE_TYPE_MAPPING = {
        
"1"_handleElement,
        
"3"_handleText
      
},
      
// Rename unknown tags to this
      
DEFAULT_NODE_NAME   "span",
      
WHITE_SPACE_REG_EXP = /s+/,
      
defaultRules        = { tags: {}, classes: {} },
      
currentRules        = {};
  
  
/**
   * Iterates over all childs of the element, recreates them, appends them into a document fragment
   * which later replaces the entire body content
   */
  
function parse(elementOrHtmlrulescontextcleanUp) {
    
wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();
    
    
context           context || elementOrHtml.ownerDocument || document;
    var 
fragment      context.createDocumentFragment(),
        
isString      typeof(elementOrHtml) === "string",
        
element,
        
newNode,
        
firstChild;
    
    if (
isString) {
      
element wysihtml5.dom.getAsDom(elementOrHtmlcontext);
    } else {
      
element elementOrHtml;
    }
    
    while (
element.firstChild) {
      
firstChild  element.firstChild;
      
element.removeChild(firstChild);
      
newNode _convert(firstChildcleanUp);
      if (
newNode) {
        
fragment.appendChild(newNode);
      }
    }
    
    
// Clear element contents
    
element.innerHTML "";
    
    
// Insert new DOM tree
    
element.appendChild(fragment);
    
    return 
isString wysihtml5.quirks.getCorrectInnerHTML(element) : element;
  }
  
  function 
_convert(oldNodecleanUp) {
    var 
oldNodeType     oldNode.nodeType,
        
oldChilds       oldNode.childNodes,
        
oldChildsLength oldChilds.length,
        
newNode,
        
method          NODE_TYPE_MAPPING[oldNodeType],
        
i               0;
    
    
newNode method && method(oldNode);
    
    if (!
newNode) {
      return 
null;
    }
    
    for (
i=0i<oldChildsLengthi++) {
      
newChild _convert(oldChilds[i], cleanUp);
      if (
newChild) {
        
newNode.appendChild(newChild);
      }
    }
    
    
// Cleanup senseless <span> elements
    
if (cleanUp &&
        
newNode.childNodes.length <= &&
        
newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
        
newNode.innerHTML !== wysihtml5.selection.PLACEHOLDER_TEXT && 
        !
newNode.attributes.length) {
      return 
newNode.firstChild;
    }
    
    return 
newNode;
  }
  
  function 
_handleElement(oldNode) {
    var 
rule,
        
newNode,
        
endTag,
        
tagRules    currentRules.tags,
        
nodeName    oldNode.nodeName.toLowerCase(),
        
scopeName   oldNode.scopeName;
    
    
/**
     * We already parsed that element
     * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
     */
    
if (oldNode._wysihtml5) {
      return 
null;
    }
    
oldNode._wysihtml5 1;
    
    if (
oldNode.className === "wysihtml5-temp") {
      return 
null;
    }
    
    
/**
     * IE is the only browser who doesn't include the namespace in the
     * nodeName, that's why we have to prepend it by ourselves
     * scopeName is a proprietary IE feature
     * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
     */
    
if (scopeName && scopeName != "HTML") {
      
nodeName scopeName ":" nodeName;
    }
    
    
/**
     * Repair node
     * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
     * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout
     */
    
if ("outerHTML" in oldNode) {
      if (!
wysihtml5.browser.autoClosesUnclosedTags() &&
          
oldNode.nodeName === "P" &&
          
oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
        
nodeName "div";
      }
    }
    
    if (
nodeName in tagRules) {
      
rule tagRules[nodeName];
      if (!
rule || rule.remove) {
        return 
null;
      }
      
      
rule typeof(rule) === "string" ? { rename_tagrule } : rule;
    } else if (
oldNode.firstChild) {
      
rule = { rename_tagDEFAULT_NODE_NAME };
    } else {
      
// Remove empty unknown elements
      
return null;
    }
    
    
newNode oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);
    
_handleAttributes(oldNodenewNoderule);
    
    
oldNode null;
    return 
newNode;
  }
  
  function 
_handleAttributes(oldNodenewNoderule) {
    var 
attributes          = {},                         // fresh new set of attributes to set on newNode
        
setClass            rule.set_class,             // classes to set
        
addClass            rule.add_class,             // add classes based on existing attributes
        
setAttributes       rule.set_attributes,        // attributes to set on the current node
        
checkAttributes     rule.check_attributes,      // check/convert values of attributes
        
allowedClasses      currentRules.classes,
        
i                   0,
        
classes             = [],
        
newClasses          = [],
        
newUniqueClasses    = [],
        
oldClasses          = [],
        
classesLength,
        
newClassesLength,
        
currentClass,
        
newClass,
        
attributeName,
        
newAttributeValue,
        
method;
    
    if (
setAttributes) {
      
attributes wysihtml5.lang.object(setAttributes).clone();
    }
    
    if (
checkAttributes) {
      for (
attributeName in checkAttributes) {
        
method attributeCheckMethods[checkAttributes[attributeName]];
        if (!
method) {
          continue;
        }
        
newAttributeValue method(_getAttribute(oldNodeattributeName));
        if (
typeof(newAttributeValue) === "string") {
          
attributes[attributeName] = newAttributeValue;
        }
      }
    }
    
    if (
setClass) {
      
classes.push(setClass);
    }
    
    if (
addClass) {
      for (
attributeName in addClass) {
        
method addClassMethods[addClass[attributeName]];
        if (!
method) {
          continue;
        }
        
newClass method(_getAttribute(oldNodeattributeName));
        if (
typeof(newClass) === "string") {
          
classes.push(newClass);
        }
      }
    }
    
    
// add old classes last
    
oldClasses oldNode.getAttribute("class");
    if (
oldClasses) {
      
classes classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
    }
    
classesLength classes.length;
    for (; 
i<classesLengthi++) {
      
currentClass classes[i];
      if (
allowedClasses[currentClass]) {
        
newClasses.push(currentClass);
      }
    }
    
    
// remove duplicate entries and preserve class specificity
    
newClassesLength newClasses.length;
    while (
newClassesLength--) {
      
currentClass newClasses[newClassesLength];
      if (!
wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
        
newUniqueClasses.unshift(currentClass);
      }
    }
    
    if (
newUniqueClasses.length) {
      
attributes["class"] = newUniqueClasses.join(" ");
    }
    
    
// set attributes on newNode
    
for (attributeName in attributes) {
      
// Setting attributes can cause a js error in IE under certain circumstances
      // eg. on a <img> under https when it's new attribute value is non-https
      // TODO: Investigate this further and check for smarter handling
      
try {
        
newNode.setAttribute(attributeNameattributes[attributeName]);
      } catch(
e) {}
    }
    
    
// IE8 sometimes loses the width/height attributes when those are set before the "src"
    // so we make sure to set them again
    
if (attributes.src) {
      if (
typeof(attributes.width) !== "undefined") {
        
newNode.setAttribute("width"attributes.width);
      }
      if (
typeof(attributes.height) !== "undefined") {
        
newNode.setAttribute("height"attributes.height);
      }
    }
  }
  
  
/**
   * IE gives wrong results for hasAttribute/getAttribute, for example:
   *    var td = document.createElement("td");
   *    td.getAttribute("rowspan"); // => "1" in IE
   *
   * Therefore we have to check the element's outerHTML for the attribute
   */
  
var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
  function 
_getAttribute(nodeattributeName) {
    
attributeName attributeName.toLowerCase();
    var 
nodeName node.nodeName;
    if (
nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {
      
// Get 'src' attribute value via object property since this will always contain the
      // full absolute url (http://...)
      // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
      // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
      
return node.src;
    } else if (
HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
      
// Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
      
var outerHTML      node.outerHTML.toLowerCase(),
          
// TODO: This might not work for attributes without value: <input disabled>
          
hasAttribute   outerHTML.indexOf(" " attributeName +  "=") != -1;
      
      return 
hasAttribute node.getAttribute(attributeName) : null;
    } else{
      return 
node.getAttribute(attributeName);
    }
  }
  
  
/**
   * Check whether the given node is a proper loaded image
   * FIXME: Returns undefined when unknown (Chrome, Safari)
   */
  
function _isLoadedImage(node) {
    try {
      return 
node.complete && !node.mozMatchesSelector(":-moz-broken");
    } catch(
e) {
      if (
node.complete && node.readyState === "complete") {
        return 
true;
      }
    }
  }
  
  function 
_handleText(oldNode) {
    return 
oldNode.ownerDocument.createTextNode(oldNode.data);
  }
  
  
  
// ------------ attribute checks ------------ \
  
var attributeCheckMethods = {
    
url: (function() {
      var 
REG_EXP = /^https?:///i;
      
return function(attributeValue) {
        if (!
attributeValue || !attributeValue.match(REG_EXP)) {
          return 
null;
        }
        return 
attributeValue.replace(REG_EXP, function(match) {
          return 
match.toLowerCase();
        });
      };
    })(),
    
    
alt: (function() {
      var 
REG_EXP = /[^ a-z0-9_-]/gi;
      return function(
attributeValue) {
        if (!
attributeValue) {
          return 
"";
        }
        return 
attributeValue.replace(REG_EXP"");
      };
    })(),
    
    
numbers: (function() {
      var 
REG_EXP = /D/g;
      return function(
attributeValue) {
        
attributeValue = (attributeValue || "").replace(REG_EXP"");
        return 
attributeValue || null;
      };
    })()
  };
  
  
// ------------ class converter (converts an html attribute to a class name) ------------ \
  
var addClassMethods = {
    
align_img: (function() {
      var 
mapping = {
        
left:   "wysiwyg-float-left",
        
right:  "wysiwyg-float-right"
      
};
      return function(
attributeValue) {
        return 
mapping[String(attributeValue).toLowerCase()];
      };
    })(),
    
    
align_text: (function() {
      var 
mapping = {
        
left:     "wysiwyg-text-align-left",
        
right:    "wysiwyg-text-align-right",
        
center:   "wysiwyg-text-align-center",
        
justify:  "wysiwyg-text-align-justify"
      
};
      return function(
attributeValue) {
        return 
mapping[String(attributeValue).toLowerCase()];
      };
    })(),
    
    
clear_br: (function() {
      var 
mapping = {
        
left:   "wysiwyg-clear-left",
        
right:  "wysiwyg-clear-right",
        
both:   "wysiwyg-clear-both",
        
all:    "wysiwyg-clear-both"
      
};
      return function(
attributeValue) {
        return 
mapping[String(attributeValue).toLowerCase()];
      };
    })(),
    
    
size_font: (function() {
      var 
mapping = {
        
"1""wysiwyg-font-size-xx-small",
        
"2""wysiwyg-font-size-small",
        
"3""wysiwyg-font-size-medium",
        
"4""wysiwyg-font-size-large",
        
"5""wysiwyg-font-size-x-large",
        
"6""wysiwyg-font-size-xx-large",
        
"7""wysiwyg-font-size-xx-large",
        
"-""wysiwyg-font-size-smaller",
        
"+""wysiwyg-font-size-larger"
      
};
      return function(
attributeValue) {
        return 
mapping[String(attributeValue).charAt(0)];
      };
    })()
  };
  
  return 
parse;
})();
/**
 * Checks for empty text node childs and removes them
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} node The element in which to cleanup
 * @example
 *    wysihtml5.dom.removeEmptyTextNodes(element);
 */
wysihtml5.dom.removeEmptyTextNodes = function(node) {
  var 
childNode,
      
childNodes        wysihtml5.lang.array(node.childNodes).get(),
      
childNodesLength  childNodes.length,
      
i                 0;
  for (; 
i<childNodesLengthi++) {
    
childNode childNodes[i];
    if (
childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
      
childNode.parentNode.removeChild(childNode);
    }
  }
};
/**
 * Renames an element (eg. a <div> to a <p>) and keeps its childs
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} element The list element which should be renamed
 * @param {Element} newNodeName The desired tag name
 *
 * @example
 *    <!-- Assume the following dom: -->
 *    <ul id="list">
 *      <li>eminem</li>
 *      <li>dr. dre</li>
 *      <li>50 Cent</li>
 *    </ul>
 *
 *    <script>
 *      wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
 *    </script>
 *
 *    <!-- Will result in: -->
 *    <ol>
 *      <li>eminem</li>
 *      <li>dr. dre</li>
 *      <li>50 Cent</li>
 *    </ol>
 */
wysihtml5.dom.renameElement = function(elementnewNodeName) {
  var 
newElement element.ownerDocument.createElement(newNodeName),
      
firstChild;
  while (
firstChild element.firstChild) {
    
newElement.appendChild(firstChild);
  }
  
wysihtml5.dom.copyAttributes(["align""className"]).from(element).to(newElement);
  
element.parentNode.replaceChild(newElementelement);
  return 
newElement;
};
/**
 * Takes an element, removes it and replaces it with it's childs
 * 
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Object} node The node which to replace with it's child nodes
 * @example
 *    <div id="foo">
 *      <span>hello</span>
 *    </div>
 *    <script>
 *      // Remove #foo and replace with it's children
 *      wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
 *    </script>
 */
wysihtml5.dom.replaceWithChildNodes = function(node) {
  if (!
node.parentNode) {
    return;
  }
  
  if (!
node.firstChild) {
    
node.parentNode.removeChild(node);
    return;
  }
  
  var 
fragment node.ownerDocument.createDocumentFragment();
  while (
node.firstChild) {
    
fragment.appendChild(node.firstChild);
  }
  
node.parentNode.replaceChild(fragmentnode);
  
node fragment null;
};
/**
 * Unwraps an unordered/ordered list
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} element The list element which should be unwrapped
 *
 * @example
 *    <!-- Assume the following dom: -->
 *    <ul id="list">
 *      <li>eminem</li>
 *      <li>dr. dre</li>
 *      <li>50 Cent</li>
 *    </ul>
 *
 *    <script>
 *      wysihtml5.dom.resolveList(document.getElementById("list"));
 *    </script>
 *
 *    <!-- Will result in: -->
 *    eminem<br>
 *    dr. dre<br>
 *    50 Cent<br>
 */
(function(dom) {
  function 
_isBlockElement(node) {
    return 
dom.getStyle("display").from(node) === "block";
  }
  
  function 
_isLineBreak(node) {
    return 
node.nodeName === "BR";
  }
  
  function 
_appendLineBreak(element) {
    var 
lineBreak element.ownerDocument.createElement("br");
    
element.appendChild(lineBreak);
  }
  
  function 
resolveList(list) {
    if (list.
nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {
      return;
    }
    
    var 
doc             = list.ownerDocument,
        
fragment        doc.createDocumentFragment(),
        
previousSibling = list.previousSibling,
        
firstChild,
        
lastChild,
        
isLastChild,
        
shouldAppendLineBreak,
        
listItem;
    
    if (
previousSibling && !_isBlockElement(previousSibling)) {
      
_appendLineBreak(fragment);
    }
    
    while (
listItem = list.firstChild) {
      
lastChild listItem.lastChild;
      while (
firstChild listItem.firstChild) {
        
isLastChild           firstChild === lastChild;
        
// This needs to be done before appending it to the fragment, as it otherwise will loose style information
        
shouldAppendLineBreak isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
        
fragment.appendChild(firstChild);
        if (
shouldAppendLineBreak) {
          
_appendLineBreak(fragment);
        }
      }
      
      
listItem.parentNode.removeChild(listItem);
    }
    list.
parentNode.replaceChild(fragment, list);
  }
  
  
dom.resolveList resolveList;
})(
wysihtml5.dom);/**
 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
 *
 * Browser Compatibility:
 *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
 *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
 *
 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
 *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
 *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
 *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
 *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
 *      can do anything as if the sandbox attribute wasn't set
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
 * @param {Object} [config] Optional parameters, see defaultConfig property for more info
 *
 * @example
 *    new wysihtml5.dom.Sandbox(function(sandbox) {
 *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
 *    });
 */
(function(wysihtml5) {
  var 
/**
       * Default configuration
       */
      
defaultConfig       = {
        
uaCompatible"IE=Edge"   // X-UA-Compatible meta tag value (Document compatibility mode)
      
},
      
doc                 document,
      
/**
       * Properties to unset/protect on the window object
       */
      
windowProperties    = [
        
"parent""top""opener""frameElement""frames",
        
"localStorage""globalStorage""sessionStorage""indexedDB"
      
],
      
/**
       * Properties on the window object which are set to an empty function
       */
      
windowProperties2   = [
        
"open""close""openDialog""showModalDialog",
        
"alert""confirm""prompt",
        
"openDatabase""postMessage",
        
"XMLHttpRequest""XDomainRequest"
      
],
      
/**
       * Properties to unset/proetect on the document object
       */
      
documentProperties  = [
        
"referrer",
        
"write""open""close"
      
];
  
  
wysihtml5.dom.Sandbox Base.extend(
    
/** @scope wysihtml5.dom.Sandbox.prototype */ {

    
constructor: function(readyCallbackconfig) {
      
this.callback readyCallback || wysihtml5.EMPTY_FUNCTION;
      
this.config   wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
      
this.iframe   this._createIframe();
    },
    
    
insertInto: function(element) {
      if (
typeof(element) === "string") {
        
element doc.getElementById(element);
      }
      
      
element.appendChild(this.iframe);
    },

    
getIframe: function() {
      return 
this.iframe;
    },

    
getWindow: function() {
      
this._readyError();
    },

    
getDocument: function() {
      
this._readyError();
    },

    
destroy: function() {
      var 
iframe this.getIframe();
      
iframe.parentNode.removeChild(iframe);
    },

    
_readyError: function() {
      throw new 
Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
    },

    
/**
     * Creates the sandbox iframe
     *
     * Some important notes:
     *  - We can't use HTML5 sandbox for now:
     *    setting it causes that the iframe's dom can't be accessed from the outside
     *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
     *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
     *    In order to make this happen we need to set the "allow-scripts" flag.
     *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
     *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
     *  - IE needs to have the security="restricted" attribute set before the iframe is 
     *    inserted into the dom tree
     *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
     *    though it supports it
     *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
     *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
     *    on the onreadystatechange event
     */
    
_createIframe: function() {
      var 
that   this,
          
iframe doc.createElement("iframe");
      
iframe.className "wysihtml5-sandbox";
      
wysihtml5.dom.setAttributes({
        
"security":           "restricted",
        
"allowtransparency":  "true",
        
"frameborder":        0,
        
"width":              0,
        
"height":             0,
        
"marginwidth":        0,
        
"marginheight":       0
      
}).on(iframe);

      
// Setting the src like this prevents ssl warnings in IE6
      
if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
        
iframe.src "javascript:'<html></html>'";
      }

      
iframe.onload = function() {
        
iframe.onreadystatechange iframe.onload null;
        
that._onLoadIframe(iframe);
      };

      
iframe.onreadystatechange = function() {
        if (/
loaded|complete/.test(iframe.readyState)) {
          
iframe.onreadystatechange iframe.onload null;
          
that._onLoadIframe(iframe);
        }
      };

      return 
iframe;
    },

    
/**
     * Callback for when the iframe has finished loading
     */
    
_onLoadIframe: function(iframe) {
      
// don't resume when the iframe got unloaded (eg. by removing it from the dom)
      
if (!wysihtml5.dom.contains(doc.documentElementiframe)) {
        return;
      }

      var 
that           this,
          
iframeWindow   iframe.contentWindow,
          
iframeDocument iframe.contentDocument || iframe.contentWindow.document,
          
charset        doc.characterSet || doc.charset || "utf-8",
          
sandboxHtml    this._getHtml({
            
charset:      charset,
            
stylesheets:  this.config.stylesheets,
            
uaCompatiblethis.config.uaCompatible
          
});

      
// Create the basic dom tree including proper DOCTYPE and charset
      
iframeDocument.open("text/html""replace");
      
iframeDocument.write(sandboxHtml);
      
iframeDocument.close();

      
this.getWindow = function() { return iframeWindow; };
      
this.getDocument = function() { return iframeDocument; };

      
// Catch js errors and pass them to the parent's onerror event
      // addEventListener("error") doesn't work properly in some browsers
      // TODO: apparently this doesn't work in IE9!
      
iframeWindow.onerror = function(errorMessagefileNamelineNumber) {
        throw new 
Error("wysihtml5.Sandbox: " errorMessagefileNamelineNumber);
      };

      if (!
wysihtml5.browser.supportsSandboxedIframes()) {
        
// Unset a bunch of sensitive variables
        // Please note: This isn't hack safe!  
        // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
        // IE is secure though, which is the most important thing, since IE is the only browser, who
        // takes over scripts & styles into contentEditable elements when copied from external websites
        // or applications (Microsoft Word, ...)
        
var ilength;
        for (
i=0length=windowProperties.lengthi<lengthi++) {
          
this._unset(iframeWindowwindowProperties[i]);
        }
        for (
i=0length=windowProperties2.lengthi<lengthi++) {
          
this._unset(iframeWindowwindowProperties2[i], wysihtml5.EMPTY_FUNCTION);
        }
        for (
i=0length=documentProperties.lengthi<lengthi++) {
          
this._unset(iframeDocumentdocumentProperties[i]);
        }
        
// This doesn't work in Safari 5 
        // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
        
this._unset(iframeDocument"cookie"""true);
      }

      
this.loaded true;

      
// Trigger the callback
      
setTimeout(function() { that.callback(that); }, 0);
    },

    
_getHtml: function(templateVars) {
      var 
stylesheets templateVars.stylesheets,
          
html        "",
          
i           0,
          
length;
      
stylesheets typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
      if (
stylesheets) {
        
length stylesheets.length;
        for (; 
i<lengthi++) {
          
html += '<link rel="stylesheet" href="' stylesheets[i] + '">';
        }
      }
      
templateVars.stylesheets html;

      return 
wysihtml5.lang.string(
        
'<!DOCTYPE html><html><head>'
        
'<meta http-equiv="X-UA-Compatible" content="#{uaCompatible}"><meta charset="#{charset}">#{stylesheets}</head>'
        
'<body></body></html>'
      
).interpolate(templateVars);
    },

    
/**
     * Method to unset/override existing variables
     * @example
     *    // Make cookie unreadable and unwritable
     *    this._unset(document, "cookie", "", true);
     */
    
_unset: function(objectpropertyvaluesetter) {
      try { 
object[property] = value; } catch(e) {}

      try { 
object.__defineGetter__(property, function() { return value; }); } catch(e) {}
      if (
setter) {
        try { 
object.__defineSetter__(property, function() {}); } catch(e) {}
      }

      if (!
wysihtml5.browser.crashesWhenDefineProperty(property)) {
        try {
          var 
config = {
            
get: function() { return value; }
          };
          if (
setter) {
            
config.set = function() {};
          }
          
Object.defineProperty(objectpropertyconfig);
        } catch(
e) {}
      }
    }
  });
})(
wysihtml5);
(function() {
  var 
mapping = {
    
"className""class"
  
};
  
wysihtml5.dom.setAttributes = function(attributes) {
    return {
      
on: function(element) {
        for (var 
i in attributes) {
          
element.setAttribute(mapping[i] || iattributes[i]);
        }
      }
    }
  };
})();
wysihtml5.dom.setStyles = function(styles) {
  return {
    
on: function(element) {
      var 
style element.style;
      if (
typeof(styles) === "string") {
        
style.cssText += ";" styles;
        return;
      }
      for (var 
i in styles) {
        if (
=== "float") {
          
style.cssFloat styles[i];
          
style.styleFloat styles[i];
        } else {
          
style[i] = styles[i];
        }
      }
    }
  };
};
/**
 * Simulate HTML5 placeholder attribute
 *
 * Needed since
 *    - div[contentEditable] elements don't support it
 *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Object} parent Instance of main wysihtml5.Editor class
 * @param {Element} view Instance of wysihtml5.views.* class
 * @param {String} placeholderText
 *
 * @example
 *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
 */
(function(dom) {
  
dom.simulatePlaceholder = function(editorviewplaceholderText) {
    var 
CLASS_NAME "placeholder",
        unset = function() {
          if (
view.hasPlaceholderSet()) {
            
view.clear();
          }
          
dom.removeClass(view.elementCLASS_NAME);
        },
        
set = function() {
          if (
view.isEmpty()) {
            
view.setValue(placeholderText);
            
dom.addClass(view.elementCLASS_NAME);
          }
        };

    
editor
      
.observe("set_placeholder"set)
      .
observe("unset_placeholder", unset)
      .
observe("focus:composer", unset)
      .
observe("paste:composer", unset)
      .
observe("blur:composer"set);

    
set();
  };
})(
wysihtml5.dom);
(function(
dom) {
  var 
documentElement document.documentElement;
  if (
"textContent" in documentElement) {
    
dom.setTextContent = function(elementtext) {
      
element.textContent text;
    };

    
dom.getTextContent = function(element) {
      return 
element.textContent;
    };
  } else if (
"innerText" in documentElement) {
    
dom.setTextContent = function(elementtext) {
      
element.innerText text;
    };

    
dom.getTextContent = function(element) {
      return 
element.innerText;
    };
  } else {
    
dom.setTextContent = function(elementtext) {
      
element.nodeValue text;
    };

    
dom.getTextContent = function(element) {
      return 
element.nodeValue;
    };
  }
})(
wysihtml5.dom);

/**
 * Fix most common html formatting misbehaviors of browsers implementation when inserting
 * content via copy & paste contentEditable
 *
 * @author Christopher Blum
 */
wysihtml5.quirks.cleanPastedHTML = (function() {
  
// TODO: We probably need more rules here
  
var defaultRules = {
    
// When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling
    
"a u"wysihtml5.dom.replaceWithChildNodes
  
};
  
  function 
cleanPastedHTML(elementOrHtmlrulescontext) {
    
rules   rules || defaultRules;
    
context context || elementOrHtml.ownerDocument || document;
    
    var 
element,
        
isString typeof(elementOrHtml) === "string",
        
method,
        
matches,
        
matchesLength,
        
i,
        
0;
    if (
isString) {
      
element wysihtml5.dom.getAsDom(elementOrHtmlcontext);
    } else {
      
element elementOrHtml;
    }
    
    for (
i in rules) {
      
matches       element.querySelectorAll(i);
      
method        rules[i];
      
matchesLength matches.length;
      for (; 
j<matchesLengthj++) {
        
method(matches[j]);
      }
    }
    
    
matches elementOrHtml rules null;
    
    return 
isString element.innerHTML element;
  }
  
  return 
cleanPastedHTML;
})();
/**
 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
 * @exaple
 *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
 */
(function(wysihtml5) {
  var 
dom wysihtml5.dom;
  
  
wysihtml5.quirks.ensureProperClearing = (function() {
    var 
clearIfNecessary = function(event) {
      var 
element this;
      
setTimeout(function() {
        var 
innerHTML element.innerHTML.toLowerCase();
        if (
innerHTML == "<p>&nbsp;</p>" ||
            
innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
          
element.innerHTML "";
        }
      }, 
0);
    };

    return function(
contentEditableElement) {
      
dom.observe(contentEditableElement, ["cut""keydown"], clearIfNecessary);
    };
  })();



  
/**
   * In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace
   *
   * @author Christopher Blum <christopher.blum@xing.com>
   * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
   * @exaple
   *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
   */
  
wysihtml5.quirks.ensureProperClearingOfLists = (function() {
    var 
ELEMENTS_THAT_CONTAIN_LI = ["OL""UL""MENU"];

    var 
clearIfNecessary = function(elementcontentEditableElement) {
      if (!
contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {
        return;
      }

      var list = 
dom.getParentElement(element, { nodeNameELEMENTS_THAT_CONTAIN_LI });
      if (!list) {
        return;
      }

      var 
listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
      if (!
listIsFirstChildOfContentEditable) {
        return;
      }

      var 
hasOnlyOneListItem = list.childNodes.length <= 1;
      if (!
hasOnlyOneListItem) {
        return;
      }

      var 
onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" true;
      if (!
onlyListItemIsEmpty) {
        return;
      }

      list.
parentNode.removeChild(list);
    };

    return function(
contentEditableElement) {
      
dom.observe(contentEditableElement"keydown", function(event) {
        if (
event.keyCode !== wysihtml5.BACKSPACE_KEY) {
          return;
        }

        var 
element wysihtml5.selection.getSelectedNode(contentEditableElement.ownerDocument);
        
clearIfNecessary(elementcontentEditableElement);
      });
    };
  })();

})(
wysihtml5);
// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
//
// In Firefox this:
//      var d = document.createElement("div");
//      d.innerHTML ='<a href="~"></a>';
//      d.innerHTML;
// will result in:
//      <a href="%7E"></a>
// which is wrong
(function(wysihtml5) {
  var 
TILDE_ESCAPED "%7E";
  
wysihtml5.quirks.getCorrectInnerHTML = function(element) {
    var 
innerHTML element.innerHTML;
    if (
innerHTML.indexOf(TILDE_ESCAPED) === -1) {
      return 
innerHTML;
    }
    
    var 
elementsWithTilde element.querySelectorAll("[href*='~'], [src*='~']"),
        
url,
        
urlToSearch,
        
length,
        
i;
    for (
i=0length=elementsWithTilde.lengthi<lengthi++) {
      
url         elementsWithTilde[i].href || elementsWithTilde[i].src;
      
urlToSearch wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
      
innerHTML   wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
    }
    return 
innerHTML;
  };
})(
wysihtml5);/**
 * Some browsers don't insert line breaks when hitting return in a contentEditable element
 *    - Opera & IE insert new <p> on return
 *    - Chrome & Safari insert new <div> on return
 *    - Firefox inserts <br> on return (yippie!)
 
 * @author Christopher Blum <christopher.blum@xing.com>
 *
 * @param {Element} element
 *
 * @example
 *    wysihtml5.quirks.insertLineBreakOnReturn(element);
 */
(function(wysihtml5) {
  var 
dom                                           wysihtml5.dom,
      
USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS  = ["LI""P""H1""H2""H3""H4""H5""H6"],
      
LIST_TAGS                                     = ["UL""OL""MENU"];
  
  function 
keyDown(event) {
    var 
keyCode event.keyCode;
    
    if (
event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
      return;
    }
    
    var 
element         event.target,
        
selectedNode    wysihtml5.selection.getSelectedNode(element.ownerDocument),
        
blockElement    dom.getParentElement(selectedNode, { nodeNameUSE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);
    
    if (
blockElement) {
      
// IE and Opera create <p> elements after leaving a list
      // check after keypress of backspace and return whether a <p> got inserted and unwrap it
      
if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
        
setTimeout(function() {
          var 
selectedNode wysihtml5.selection.getSelectedNode(element.ownerDocument),
              list,
              
invisibleSpace,
              
div;
          if (!
selectedNode) {
            return;
          }
          
          list = 
dom.getParentElement(selectedNode, {
            
nodeNameLIST_TAGS
          
}, 2);
          
          if (list) {
            return;
          }
          
          var 
parentElement dom.getParentElement(selectedNode, { nodeName: ["P""DIV"] }, 2);
          if (!
parentElement) {
            return;
          }
          
          
invisibleSpace document.createTextNode(wysihtml5.INVISIBLE_SPACE);
          
dom.insert(invisibleSpace).before(parentElement);
          
dom.replaceWithChildNodes(parentElement);
          
wysihtml5.selection.selectNode(invisibleSpace);
        }, 
0);
      }
      return;
    }
    
    if (
keyCode === wysihtml5.ENTER_KEY) {
      
wysihtml5.commands.exec(element"insertLineBreak");
      
event.preventDefault();
    }
  }
  
  
wysihtml5.quirks.insertLineBreakOnReturn = function(element) {
    
// keypress doesn't fire when you hit backspace
    
dom.observe(element.ownerDocument"keydown"keyDown);
  };
})(
wysihtml5);/**
 * Force rerendering of a given element
 * Needed to fix display misbehaviors of IE
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} element The element object which needs to be rerendered
 * @example
 *    wysihtml5.quirks.redraw(document.body);
 */
(function(wysihtml5) {
  var 
CLASS_NAME "wysihtml5-quirks-redraw";
  
  
wysihtml5.quirks.redraw = function(element) {
    
wysihtml5.dom.addClass(elementCLASS_NAME);
    
wysihtml5.dom.removeClass(elementCLASS_NAME);
    
    
// Following hack is needed for firefox to make sure that image resize handles are properly removed
    
try {
      var 
doc element.ownerDocument;
      
doc.execCommand("italic"falsenull);
      
doc.execCommand("italic"falsenull);
    } catch(
e) {}
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
PLACEHOLDER_TEXT "--_CARET_--",
      
dom              wysihtml5.dom;
  
      
// ----------------- private -------------- \
  
function _createPlaceholderNode(doc) {
    
// Important: placeholder element needs to be an inline element
    // otherwise chrome will cause trouble when interacting with the text
    
var element       doc._wysihtml5CaretPlaceholder = (doc._wysihtml5CaretPlaceholder || doc.createElement("span"));
    
element.innerHTML PLACEHOLDER_TEXT;
    return 
element;
  }

  function 
_findPlaceholderNode(doc) {
    var 
placeholderElement      _createPlaceholderNode(doc),
        
// Using placeholderElement.innerHTML causes problems in firefox who sometimes mangles the innerHTML
        
placeholderElementText  dom.getTextContent(placeholderElement),
        
placeholderElementById  doc.getElementById(placeholderElement.id),
        
i                       0,
        
element,
        
elements,
        
elementsLength;
    if (
dom.contains(doc.bodyplaceholderElement)) {
      return 
placeholderElement;
    } else if (
placeholderElementById) {
      return 
placeholderElementById;
    } else {
      
elements doc.getElementsByTagName("*");
      
elementsLength elements.length;
      for (; 
i<elementsLengthi++) {
        
element elements[i];
        if (
element.innerHTML === placeholderElementText) {
          return 
element;
        }
      }
      return 
null;
    }
  }
  
  function 
_getCumulativeOffsetTop(element) {
    var 
top 0;
    if (
element.parentNode) {
      do {
        
top += element.offsetTop || 0;
        
element element.offsetParent;
      } while (
element);
    }
    return 
top;
  }
  
  
wysihtml5.selection = {
    
PLACEHOLDER_TEXTPLACEHOLDER_TEXT,

    
/**
     * Get the current selection as a bookmark to be able to later restore it
     *
     * @param {Object} doc Document object of the context
     * @return {Object} An object that represents the current selection
     */
    
getBookmark: function(doc) {
      var 
range this.getRange(doc);
      return 
range && range.cloneRange();
    },

    
/**
     * Restore a selection retrieved via wysihtml5.selection.getBookmark
     *
     * @param {Object} bookmark An object that represents the current selection
     */
    
setBookmark: function(bookmark) {
      if (!
bookmark) {
        return;
      }

      
this.setSelection(bookmark);
    },

    
/**
     * Set the caret in front of the given node
     *
     * @param {Object} node The element or text node where to position the caret in front of
     * @example
     *    wysihtml5.selection.setBefore(myElement);
     */
    
setBefore: function(node) {
      var 
range rangy.createRange(node.ownerDocument);
      
range.setStartBefore(node);
      
range.setEndBefore(node);
      return 
this.setSelection(range);
    },

    
/**
     * Set the caret after the given node
     *
     * @param {Object} node The element or text node where to position the caret in front of
     * @example
     *    wysihtml5.selection.setBefore(myElement);
     */
    
setAfter: function(node) {
      var 
range rangy.createRange(node.ownerDocument);
      
range.setStartAfter(node);
      
range.setEndAfter(node);
      return 
this.setSelection(range);
    },

    
/**
     * Ability to select/mark nodes
     *
     * @param {Element} node The node/element to select
     * @example
     *    wysihtml5.selection.selectNode(document.getElementById("my-image"));
     */
    
selectNode: function(node) {
      var 
range           rangy.createRange(node.ownerDocument),
          
isElement       node.nodeType === wysihtml5.ELEMENT_NODE,
          
canHaveHTML     "canHaveHTML" in node node.canHaveHTML : (node.nodeName !== "IMG"),
          
content         isElement node.innerHTML node.data,
          
isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
          
displayStyle    dom.getStyle("display").from(node),
          
isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");

      if (
isEmpty && isElement && canHaveHTML) {
        
// Make sure that caret is visible in node by inserting a zero width no breaking space
        
try { node.innerHTML wysihtml5.INVISIBLE_SPACE; } catch(e) {}
      }

      if (
canHaveHTML) {
        
range.selectNodeContents(node);
      } else {
        
range.selectNode(node);
      }

      if (
canHaveHTML && isEmpty && isElement) {
        
range.collapse(isBlockElement);
      } else if (
canHaveHTML && isEmpty) {
        
range.setStartAfter(node);
        
range.setEndAfter(node);
      }

      
this.setSelection(range);
    },

    
/**
     * Get the node which contains the selection
     *
     * @param {Object} document Document object of the context where to select
     * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
     * @return {Object} The node that contains the caret
     * @example
     *    var nodeThatContainsCaret = wysihtml5.selection.getSelectedNode(document);
     */
    
getSelectedNode: function(doccontrolRange) {
      var 
selection,
          
range;

      if (
controlRange && doc.selection && doc.selection.type === "Control") {
        
range doc.selection.createRange();
        if (
range && range.length) {
          return 
range.item(0);
        }
      }

      
selection this.getSelection(doc);
      if (
selection.focusNode === selection.anchorNode) {
        return 
selection.focusNode;
      } else {
        
range this.getRange(doc);
        return 
range range.commonAncestorContainer doc.body;
      }
    },

    
executeAndRestore: function(docmethodrestoreScrollPosition) {
      if (!
window.getSelection) {
        return 
this.executeAndRestoreSimple(docmethod);
      }

      var 
body                doc.body,
          
oldScrollTop        body.scrollTop,
          
oldScrollLeft       body.scrollLeft,
          
range               this.getRange(doc),
          
caretPlaceholder    _createPlaceholderNode(doc),
          
newCaretPlaceholder,
          
newRange;

      
// Nothing selected, execute and say goodbye
      
if (!range) {
        
method(bodybody);
        return;
      }

      
range.insertNode(caretPlaceholder);

      
// Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
      
try {
        
method(range.startContainerrange.endContainer);
      } catch(
e1) {
        
setTimeout(function() { throw e1; }, 0);
      }

      
// range.detach();

      
newCaretPlaceholder _findPlaceholderNode(doc);

      if (
newCaretPlaceholder) {
        
newRange rangy.createRange(doc);
        
newRange.selectNode(newCaretPlaceholder);
        
newRange.deleteContents();
        
this.setSelection(newRange);
      }

      if (
restoreScrollPosition) {
        
body.scrollTop  oldScrollTop;
        
body.scrollLeft oldScrollLeft;
      }

      
// Remove it again, just to make sure that the placeholder is definitely out of the dom tree
      
try {
        
newCaretPlaceholder.parentNode.removeChild(newCaretPlaceholder);
      } catch(
e2) {}
    },

    
/**
     * Different approach of preserving the selection (doesn't modify the dom)
     * Takes all text nodes in the selection and saves the selection position in the first and last one
     */
    
executeAndRestoreSimple: function(docmethod) {
      var 
range this.getRange(doc),
          
newRange,
          
firstNode,
          
lastNode,
          
textNodes,
          
rangeBackup;

      
// Nothing selected, execute and say goodbye
      
if (!range) {
        
method(doc.bodydoc.body);
        return;
      }

      
textNodes range.getNodes([3]);
      
firstNode textNodes[0] || range.startContainer;
      
lastNode  textNodes[textNodes.length 1] || range.endContainer;

      
rangeBackup = {
        
collapsed:      range.collapsed,
        
startContainerfirstNode,
        
startOffset:    firstNode === range.startContainer range.startOffset 0,
        
endContainer:   lastNode,
        
endOffset:      lastNode === range.endContainer range.endOffset lastNode.length
      
};

      try {
        
method(range.startContainerrange.endContainer);
      } catch(
e) {
        
setTimeout(function() { throw e; }, 0);
      }

      
newRange rangy.createRange(doc);
      try { 
newRange.setStart(rangeBackup.startContainerrangeBackup.startOffset); } catch(e1) {}
      try { 
newRange.setEnd(rangeBackup.endContainerrangeBackup.endOffset); } catch(e2) {}
      try { 
this.setSelection(newRange); } catch(e3) {}
    },

    
/**
     * Insert html at the caret position and move the cursor after the inserted html
     *
     * @param {Object} doc Document object of the context where to insert the html
     * @param {String} html HTML string to insert
     * @example
     *    wysihtml5.selection.insertHTML(document, "<p>foobar</p>");
     */
    
insertHTML: function(dochtml) {
      var 
range     rangy.createRange(doc),
          
node      range.createContextualFragment(html),
          
lastChild node.lastChild;
      
this.insertNode(node);
      if (
lastChild) {
        
this.setAfter(lastChild);
      }
    },

    
/**
     * Insert a node at the caret position and move the cursor after it
     *
     * @param {Object} node HTML string to insert
     * @example
     *    wysihtml5.selection.insertNode(document.createTextNode("foobar"));
     */
    
insertNode: function(node) {
      var 
range this.getRange(node.ownerDocument);
      if (
range) {
        
range.insertNode(node);
      } else {
        return 
false;
      }
    },

    
/**
     * Wraps current selection with the given node
     *
     * @param {Object} node The node to surround the selected elements with
     */
    
surround: function(node) {
      var 
range this.getRange(node.ownerDocument);
      if (!
range) {
        return;
      }

      try {
        
// This only works when the range boundaries are not overlapping other elements
        
range.surroundContents(node);
        
this.selectNode(node);
      } catch(
e) {
        
// fallback
        
node.appendChild(range.extractContents());
        
range.insertNode(node);
      }
    },

    
/**
     * Scroll the current caret position into the view
     * FIXME: This is a bit hacky, there might be a smarter way of doing this
     *
     * @param {Object} element A scrollable element in which the caret is currently positioned
     * @example
     *    wysihtml5.selection.scrollIntoView(element);
     */
    
scrollIntoView: function(element) {
      var 
doc           element.ownerDocument,
          
hasScrollBars doc.documentElement.scrollHeight doc.documentElement.offsetHeight,
          
tempElement   doc._wysihtml5ScrollIntoViewElement doc._wysihtml5ScrollIntoViewElement || (function() {
            var 
element doc.createElement("span");
            
// The element needs content in order to be able to calculate it's position properly
            
element.innerHTML wysihtml5.INVISIBLE_SPACE;
            return 
element;
          })(),
          
offsetTop;

      if (
hasScrollBars) {
        
this.insertNode(tempElement);
        
offsetTop _getCumulativeOffsetTop(tempElement);
        
tempElement.parentNode.removeChild(tempElement);
        if (
offsetTop element.scrollTop) {
          
element.scrollTop offsetTop;
        }
      }
    },

    
/**
     * Select line where the caret is in
     */
    
selectLine: function(doc) {
      if (
wysihtml5.browser.supportsSelectionModify()) {
        
this._selectLine_W3C(doc);
      } else if (
doc.selection) {
        
this._selectLine_MSIE(doc);
      }
    },

    
/**
     * See https://developer.mozilla.org/en/DOM/Selection/modify
     */
    
_selectLine_W3C: function(doc) {
      var 
win doc.defaultView,
          
selection win.getSelection();
      
selection.modify("extend""left""lineboundary");
      
selection.modify("extend""right""lineboundary");
    },

    
_selectLine_MSIE: function(doc) {
      var 
range       doc.selection.createRange(),
          
rangeTop    range.boundingTop,
          
rangeHeight range.boundingHeight,
          
scrollWidth doc.body.scrollWidth,
          
rangeBottom,
          
rangeEnd,
          
measureNode,
          
i,
          
j;

      if (!
range.moveToPoint) {
        return;
      }

      if (
rangeTop === 0) {
        
// Don't know why, but when the selection ends at the end of a line
        // range.boundingTop is 0
        
measureNode doc.createElement("span");
        
this.insertNode(measureNode);
        
rangeTop measureNode.offsetTop;
        
measureNode.parentNode.removeChild(measureNode);
      }

      
rangeTop += 1;

      for (
i=-10i<scrollWidthi+=2) {
        try {
          
range.moveToPoint(irangeTop);
          break;
        } catch(
e1) {}
      }

      
// Investigate the following in order to handle multi line selections
      // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
      
rangeBottom rangeTop;
      
rangeEnd doc.selection.createRange();
      for (
j=scrollWidthj>=0j--) {
        try {
          
rangeEnd.moveToPoint(jrangeBottom);
          break;
        } catch(
e2) {}
      }

      
range.setEndPoint("EndToEnd"rangeEnd);
      
range.select();
    },

    
getText: function(doc) {
      var 
selection this.getSelection(doc);
      return 
selection selection.toString() : "";
    },

    
getNodes: function(docnodeTypefilter) {
      var 
range this.getRange(doc);
      if (
range) {
        return 
range.getNodes([nodeType], filter);
      } else {
        return [];
      }
    },

    
getRange: function(doc) {
      var 
selection this.getSelection(doc);
      return 
selection && selection.rangeCount && selection.getRangeAt(0);
    },

    
getSelection: function(doc) {
      return 
rangy.getSelection(doc.defaultView || doc.parentWindow);
    },

    
setSelection: function(range) {
      var 
doc       = (range.startContainer || range.endContainer).ownerDocument,
          
win       doc.defaultView || doc.parentWindow,
          
selection rangy.getSelection(win);
      return 
selection.setSingleRange(range);
    }
  };
  
})(
wysihtml5);

/**
 * @author Christopher Blum <christopher.blum@xing.com> to match WYSIHTML5 logic
 * in order to be able ...
 *    - to use custom tags
 *    - to detect and replace similar css classes via reg exp
 *
 * Inspired by the rangy CSS Applier module written by Tim Down and  licensed under the MIT license.
 * http://code.google.com/p/rangy/
 */
(function(wysihtml5rangy) {
  var 
defaultTagName "span";
  
  var 
REG_EXP_WHITE_SPACE = /s+/g;
  
  function 
hasClass(elcssClassregExp) {
    if (!
el.className) {
      return 
false;
    }
    
    var 
matchingClassNames el.className.match(regExp) || [];
    return 
matchingClassNames[matchingClassNames.length 1] === cssClass;
  }

  function 
addClass(elcssClassregExp) {
    if (
el.className) {
      
removeClass(elregExp);
      
el.className += " " cssClass;
    } else {
      
el.className cssClass;
    }
  }

  function 
removeClass(elregExp) {
    if (
el.className) {
      
el.className el.className.replace(regExp"");
    }
  }
  
  function 
hasSameClasses(el1el2) {
    return 
el1.className.replace(REG_EXP_WHITE_SPACE" ") == el2.className.replace(REG_EXP_WHITE_SPACE" ");
  }

  function 
replaceWithOwnChildren(el) {
    var 
parent el.parentNode;
    while (
el.firstChild) {
      
parent.insertBefore(el.firstChildel);
    }
    
parent.removeChild(el);
  }

  function 
elementsHaveSameNonClassAttributes(el1el2) {
    if (
el1.attributes.length != el2.attributes.length) {
      return 
false;
    }
    for (var 
0len el1.attributes.lengthattr1attr2namelen; ++i) {
      
attr1 el1.attributes[i];
      
name attr1.name;
      if (
name != "class") {
        
attr2 el2.attributes.getNamedItem(name);
        if (
attr1.specified != attr2.specified) {
          return 
false;
        }
        if (
attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
          return 
false;
        }
      }
    }
    return 
true;
  }

  function 
isSplitPoint(nodeoffset) {
    if (
rangy.dom.isCharacterDataNode(node)) {
      if (
offset == 0) {
        return !!
node.previousSibling;
      } else if (
offset == node.length) {
        return !!
node.nextSibling;
      } else {
        return 
true;
      }
    }

    return 
offset && offset node.childNodes.length;
  }

  function 
splitNodeAt(nodedescendantNodedescendantOffset) {
    var 
newNode;
    if (
rangy.dom.isCharacterDataNode(descendantNode)) {
      if (
descendantOffset == 0) {
        
descendantOffset rangy.dom.getNodeIndex(descendantNode);
        
descendantNode descendantNode.parentNode;
      } else if (
descendantOffset == descendantNode.length) {
        
descendantOffset rangy.dom.getNodeIndex(descendantNode) + 1;
        
descendantNode descendantNode.parentNode;
      } else {
        
newNode rangy.dom.splitDataNode(descendantNodedescendantOffset);
      }
    }
    if (!
newNode) {
      
newNode descendantNode.cloneNode(false);
      if (
newNode.id) {
        
newNode.removeAttribute("id");
      }
      var 
child;
      while ((
child descendantNode.childNodes[descendantOffset])) {
        
newNode.appendChild(child);
      }
      
rangy.dom.insertAfter(newNodedescendantNode);
    }
    return (
descendantNode == node) ? newNode splitNodeAt(nodenewNode.parentNoderangy.dom.getNodeIndex(newNode));
  }
  
  function 
Merge(firstNode) {
    
this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
    
this.firstTextNode this.isElementMerge firstNode.lastChild firstNode;
    
this.textNodes = [this.firstTextNode];
  }

  
Merge.prototype = {
    
doMerge: function() {
      var 
textBits = [], textNodeparenttext;
      for (var 
0len this.textNodes.lengthlen; ++i) {
        
textNode this.textNodes[i];
        
parent textNode.parentNode;
        
textBits[i] = textNode.data;
        if (
i) {
          
parent.removeChild(textNode);
          if (!
parent.hasChildNodes()) {
            
parent.parentNode.removeChild(parent);
          }
        }
      }
      
this.firstTextNode.data text textBits.join("");
      return 
text;
    },

    
getLength: function() {
      var 
this.textNodes.lengthlen 0;
      while (
i--) {
        
len += this.textNodes[i].length;
      }
      return 
len;
    },

    
toString: function() {
      var 
textBits = [];
      for (var 
0len this.textNodes.lengthlen; ++i) {
        
textBits[i] = "'" this.textNodes[i].data "'";
      }
      return 
"[Merge(" textBits.join(",") + ")]";
    }
  };

  function 
HTMLApplier(tagNamescssClasssimilarClassRegExpnormalize) {
    
this.tagNames tagNames || [defaultTagName];
    
this.cssClass cssClass || "";
    
this.similarClassRegExp similarClassRegExp;
    
this.normalize normalize;
    
this.applyToAnyTagName false;
  }

  
HTMLApplier.prototype = {
    
getAncestorWithClass: function(node) {
      var 
cssClassMatch;
      while (
node) {
        
cssClassMatch this.cssClass hasClass(nodethis.cssClassthis.similarClassRegExp) : true;
        if (
node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNamesnode.tagName.toLowerCase()) && cssClassMatch) {
          return 
node;
        }
        
node node.parentNode;
      }
      return 
false;
    },

    
// Normalizes nodes after applying a CSS class to a Range.
    
postApply: function(textNodesrange) {
      var 
firstNode textNodes[0], lastNode textNodes[textNodes.length 1];

      var 
merges = [], currentMerge;

      var 
rangeStartNode firstNoderangeEndNode lastNode;
      var 
rangeStartOffset 0rangeEndOffset lastNode.length;

      var 
textNodeprecedingTextNode;

      for (var 
0len textNodes.lengthlen; ++i) {
        
textNode textNodes[i];
        
precedingTextNode this.getAdjacentMergeableTextNode(textNode.parentNodefalse);
        if (
precedingTextNode) {
          if (!
currentMerge) {
            
currentMerge = new Merge(precedingTextNode);
            
merges.push(currentMerge);
          }
          
currentMerge.textNodes.push(textNode);
          if (
textNode === firstNode) {
            
rangeStartNode currentMerge.firstTextNode;
            
rangeStartOffset rangeStartNode.length;
          }
          if (
textNode === lastNode) {
            
rangeEndNode currentMerge.firstTextNode;
            
rangeEndOffset currentMerge.getLength();
          }
        } else {
          
currentMerge null;
        }
      }

      
// Test whether the first node after the range needs merging
      
var nextTextNode this.getAdjacentMergeableTextNode(lastNode.parentNodetrue);
      if (
nextTextNode) {
        if (!
currentMerge) {
          
currentMerge = new Merge(lastNode);
          
merges.push(currentMerge);
        }
        
currentMerge.textNodes.push(nextTextNode);
      }

      
// Do the merges
      
if (merges.length) {
        for (
0len merges.lengthlen; ++i) {
          
merges[i].doMerge();
        }
        
// Set the range boundaries
        
range.setStart(rangeStartNoderangeStartOffset);
        
range.setEnd(rangeEndNoderangeEndOffset);
      }
    },
    
    
getAdjacentMergeableTextNode: function(nodeforward) {
        var 
isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
        var 
el isTextNode node.parentNode node;
        var 
adjacentNode;
        var 
propName forward "nextSibling" "previousSibling";
        if (
isTextNode) {
          
// Can merge if the node's previous/next sibling is a text node
          
adjacentNode node[propName];
          if (
adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
            return 
adjacentNode;
          }
        } else {
          
// Compare element with its sibling
          
adjacentNode el[propName];
          if (
adjacentNode && this.areElementsMergeable(nodeadjacentNode)) {
            return 
adjacentNode[forward "firstChild" "lastChild"];
          }
        }
        return 
null;
    },
    
    
areElementsMergeable: function(el1el2) {
      return 
rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
        && 
rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
        && 
hasSameClasses(el1el2)
        && 
elementsHaveSameNonClassAttributes(el1el2);
    },

    
createContainer: function(doc) {
      var 
el doc.createElement(this.tagNames[0]);
      if (
this.cssClass) {
        
el.className this.cssClass;
      }
      return 
el;
    },

    
applyToTextNode: function(textNode) {
      var 
parent textNode.parentNode;
      if (
parent.childNodes.length == && rangy.dom.arrayContains(this.tagNamesparent.tagName.toLowerCase())) {
        if (
this.cssClass) {
          
addClass(parentthis.cssClassthis.similarClassRegExp);
        }
      } else {
        var 
el this.createContainer(rangy.dom.getDocument(textNode));
        
textNode.parentNode.insertBefore(eltextNode);
        
el.appendChild(textNode);
      }
    },

    
isRemovable: function(el) {
      return 
rangy.dom.arrayContains(this.tagNamesel.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
    },

    
undoToTextNode: function(textNoderangeancestorWithClass) {
      if (!
range.containsNode(ancestorWithClass)) {
        
// Split out the portion of the ancestor from which we can remove the CSS class
        
var ancestorRange range.cloneRange();
        
ancestorRange.selectNode(ancestorWithClass);

        if (
ancestorRange.isPointInRange(range.endContainerrange.endOffset) && isSplitPoint(range.endContainerrange.endOffset)) {
          
splitNodeAt(ancestorWithClassrange.endContainerrange.endOffset);
          
range.setEndAfter(ancestorWithClass);
        }
        if (
ancestorRange.isPointInRange(range.startContainerrange.startOffset) && isSplitPoint(range.startContainerrange.startOffset)) {
          
ancestorWithClass splitNodeAt(ancestorWithClassrange.startContainerrange.startOffset);
        }
      }
      
      if (
this.similarClassRegExp) {
        
removeClass(ancestorWithClassthis.similarClassRegExp);
      }
      if (
this.isRemovable(ancestorWithClass)) {
        
replaceWithOwnChildren(ancestorWithClass);
      }
    },

    
applyToRange: function(range) {
        var 
textNodes range.getNodes([wysihtml5.TEXT_NODE]);
        if (!
textNodes.length) {
          try {
            var 
node this.createContainer(range.endContainer.ownerDocument);
            
range.surroundContents(node);
            
this.selectNode(rangenode);
            return;
          } catch(
e) {}
        }
        
        
range.splitBoundaries();
        
textNodes range.getNodes([wysihtml5.TEXT_NODE]);
        
        if (
textNodes.length) {
          var 
textNode;

          for (var 
0len textNodes.lengthlen; ++i) {
            
textNode textNodes[i];
            if (!
this.getAncestorWithClass(textNode)) {
              
this.applyToTextNode(textNode);
            }
          }
          
          
range.setStart(textNodes[0], 0);
          
textNode textNodes[textNodes.length 1];
          
range.setEnd(textNodetextNode.length);
          
          if (
this.normalize) {
            
this.postApply(textNodesrange);
          }
        }
    },

    
undoToRange: function(range) {
      var 
textNodes range.getNodes([wysihtml5.TEXT_NODE]), textNodeancestorWithClass;
      if (
textNodes.length) {
        
range.splitBoundaries();
        
textNodes range.getNodes([wysihtml5.TEXT_NODE]);
      } else {
        var 
doc range.endContainer.ownerDocument,
            
node doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
        
range.insertNode(node);
        
range.selectNode(node);
        
textNodes = [node];
      }
      
      for (var 
0len textNodes.lengthlen; ++i) {
        
textNode textNodes[i];
        
ancestorWithClass this.getAncestorWithClass(textNode);
        if (
ancestorWithClass) {
          
this.undoToTextNode(textNoderangeancestorWithClass);
        }
      }
      
      if (
len == 1) {
        
this.selectNode(rangetextNodes[0]);
      } else {
        
range.setStart(textNodes[0], 0);
        
textNode textNodes[textNodes.length 1];
        
range.setEnd(textNodetextNode.length);

        if (
this.normalize) {
          
this.postApply(textNodesrange);
        }
      }
    },
    
    
selectNode: function(rangenode) {
      var 
isElement       node.nodeType === wysihtml5.ELEMENT_NODE,
          
canHaveHTML     "canHaveHTML" in node node.canHaveHTML true,
          
content         isElement node.innerHTML node.data,
          
isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);

      if (
isEmpty && isElement && canHaveHTML) {
        
// Make sure that caret is visible in node by inserting a zero width no breaking space
        
try { node.innerHTML wysihtml5.INVISIBLE_SPACE; } catch(e) {}
      }
      
range.selectNodeContents(node);
      if (
isEmpty && isElement) {
        
range.collapse(false);
      } else if (
isEmpty) {
        
range.setStartAfter(node);
        
range.setEndAfter(node);
      }
    },
    
    
getTextSelectedByRange: function(textNoderange) {
      var 
textRange range.cloneRange();
      
textRange.selectNodeContents(textNode);

      var 
intersectionRange textRange.intersection(range);
      var 
text intersectionRange intersectionRange.toString() : "";
      
textRange.detach();

      return 
text;
    },

    
isAppliedToRange: function(range) {
      var 
ancestors = [],
          
ancestor,
          
textNodes range.getNodes([wysihtml5.TEXT_NODE]);
      if (!
textNodes.length) {
        
ancestor this.getAncestorWithClass(range.startContainer);
        return 
ancestor ? [ancestor] : false;
      }
      
      for (var 
0len textNodes.lengthselectedTextlen; ++i) {
        
selectedText this.getTextSelectedByRange(textNodes[i], range);
        
ancestor this.getAncestorWithClass(textNodes[i]);
        if (
selectedText != "" && !ancestor) {
          return 
false;
        } else {
          
ancestors.push(ancestor);
        }
      }
      return 
ancestors;
    },

    
toggleRange: function(range) {
      if (
this.isAppliedToRange(range)) {
        
this.undoToRange(range);
      } else {
        
this.applyToRange(range);
      }
    }
  };

  
wysihtml5.selection.HTMLApplier HTMLApplier;
  
})(
wysihtml5rangy);/**
 * TODO: the following methods still need unit test coverage
 */
wysihtml5.views.View Base.extend(
  
/** @scope wysihtml5.views.View.prototype */ {
  
constructor: function(parenttextareaElementconfig) {
    
this.parent   parent;
    
this.element  textareaElement;
    
this.config   config;
    
    
this._observeViewChange();
  },
  
  
_observeViewChange: function() {
    var 
that this;
    
this.parent.observe("beforeload", function() {
      
that.parent.observe("change_view", function(view) {
        if (
view === that.name) {
          
that.parent.currentView that;
          
that.show();
          
// Using tiny delay here to make sure that the placeholder is set before focusing
          
setTimeout(function() { that.focus(); }, 0);
        } else {
          
that.hide();
        }
      });
    });
  },
  
  
focus: function() {
    if (
this.element.ownerDocument.querySelector(":focus") === this.element) {
      return;
    }
    
    try { 
this.element.focus(); } catch(e) {}
  },
  
  
hide: function() {
    
this.element.style.display "none";
  },
  
  
show: function() {
    
this.element.style.display "";
  },
  
  
disable: function() {
    
this.element.setAttribute("disabled""disabled");
  },
  
  
enable: function() {
    
this.element.removeAttribute("disabled");
  }
});(function(
wysihtml5) {
  var 
dom       wysihtml5.dom,
      
browser   wysihtml5.browser,
      
selection wysihtml5.selection;
  
  
wysihtml5.views.Composer wysihtml5.views.View.extend(
    
/** @scope wysihtml5.views.Composer.prototype */ {
    
name"composer",

    
// Needed for firefox in order to display a proper caret in an empty contentEditable
    
CARET_HACK"<br>",

    
constructor: function(parenttextareaElementconfig) {
      
this.base(parenttextareaElementconfig);
      
this.textarea this.parent.textarea;
      
this._initSandbox();
    },

    
clear: function() {
      
this.element.innerHTML browser.displaysCaretInEmptyContentEditableCorrectly() ? "" this.CARET_HACK;
    },

    
getValue: function(parse) {
      var 
value this.isEmpty() ? "" wysihtml5.quirks.getCorrectInnerHTML(this.element);
      
      if (
parse) {
        
value this.parent.parse(value);
      }

      
// Replace all "zero width no breaking space" chars
      // which are used as hacks to enable some functionalities
      // Also remove all CARET hacks that somehow got left
      
value wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
      
value wysihtml5.lang.string(value).replace(selection.PLACEHOLDER_TEXT).by("");

      return 
value;
    },

    
setValue: function(htmlparse) {
      if (
parse) {
        
html this.parent.parse(html);
      }
      
this.element.innerHTML html;
    },

    
show: function() {
      
this.iframe.style.display this._displayStyle || "";

      
// Firefox needs this, otherwise contentEditable becomes uneditable
      
this.disable();
      
this.enable();
    },

    
hide: function() {
      
this._displayStyle dom.getStyle("display").from(this.iframe);
      if (
this._displayStyle === "none") {
        
this._displayStyle null;
      }
      
this.iframe.style.display "none";
    },

    
disable: function() {
      
this.element.removeAttribute("contentEditable");
      
this.base();
    },

    
enable: function() {
      
this.element.setAttribute("contentEditable""true");
      
this.base();
    },

    
getTextContent: function() {
      return 
dom.getTextContent(this.element);
    },

    
hasPlaceholderSet: function() {
      return 
this.getTextContent() == this.textarea.element.getAttribute("placeholder");
    },

    
isEmpty: function() {
      var 
innerHTML               this.element.innerHTML,
          
elementsWithVisualValue "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
      return 
innerHTML === ""              || 
             
innerHTML === this.CARET_HACK ||
             
this.hasPlaceholderSet()      ||
             (
this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
    },

    
_initSandbox: function() {
      var 
that this;
      
      
this.sandbox = new dom.Sandbox(function() {
        
that._create();
      }, {
        
stylesheets:  this.config.stylesheets,
        
uaCompatible"IE=7"
      
});
      
this.iframe  this.sandbox.getIframe();

      
// Create hidden field which tells the server after submit, that the user used an wysiwyg editor
      
var hiddenField document.createElement("input");
      
hiddenField.type   "hidden";
      
hiddenField.name   "_wysihtml5_mode";
      
hiddenField.value  1;

      
// Store reference to current wysihtml5 instance on the textarea element
      
var textareaElement this.textarea.element;
      
dom.insert(this.iframe).after(textareaElement);
      
dom.insert(hiddenField).after(textareaElement);
    },

    
_create: function() {
      var 
that this;
      
      
this.element            this.sandbox.getDocument().body;
      
this.textarea           this.parent.textarea;
      
this.element.innerHTML  this.textarea.getValue(true);
      
this.enable();

      
// Make sure that our external range library is initialized
      
window.rangy.init();

      
dom.copyAttributes([
        
"className""spellcheck""title""lang""dir""accessKey"
      
]).from(this.textarea.element).to(this.element);
      
      
dom.addClass(this.elementthis.config.composerClassName);

      
// Make the editor look like the original textarea, by syncing styles
      
if (this.config.style) {
        
this.style();
      }

      
this.observe();

      var 
name this.config.name;
      if (
name) {
        
dom.addClass(this.elementname);
        
dom.addClass(this.iframename);
      }

      
// Simulate html5 placeholder attribute on contentEditable element
      
var placeholderText typeof(this.config.placeholder) === "string"
        
this.config.placeholder
        
this.textarea.element.getAttribute("placeholder");
      if (
placeholderText) {
        
dom.simulatePlaceholder(this.parentthisplaceholderText);
      }

      
// Make sure that the browser avoids using inline styles whenever possible
      
wysihtml5.commands.exec(this.element"styleWithCSS"false);

      
this._initAutoLinking();
      
this._initObjectResizing();
      
this._initUndoManager();

      
// Simulate html5 autofocus on contentEditable element
      
if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
        
setTimeout(function() { that.focus(); }, 100);
      }

      
// IE and Opera insert paragraphs on return instead of line breaks
      
if (!browser.insertsLineBreaksOnReturn()) {
        
wysihtml5.quirks.insertLineBreakOnReturn(this.element);
      }

      
// IE sometimes leaves a single paragraph, which can't be removed by the user
      
if (!browser.clearsContentEditableCorrectly()) {
        
wysihtml5.quirks.ensureProperClearing(this.element);
      }

      if (!
browser.clearsListsInContentEditableCorrectly()) {
        
wysihtml5.quirks.ensureProperClearingOfLists(this.element);
      }

      
// Set up a sync that makes sure that textarea and editor have the same content
      
if (this.initSync && this.config.sync) {
        
this.initSync();
      }

      
// Okay hide the textarea, we are ready to go
      
this.textarea.hide();

      
// Fire global (before-)load event
      
this.parent.fire("beforeload").fire("load");
    },

    
_initAutoLinking: function() {
      var 
supportsDisablingOfAutoLinking browser.canDisableAutoLinking(),
          
supportsAutoLinking            browser.doesAutoLinkingInContentEditable();
      if (
supportsDisablingOfAutoLinking) {
        
wysihtml5.commands.exec(this.element"autoUrlDetect"false);
      }

      if (!
this.config.autoLink) {
        return;
      }

      var 
sandboxDoc this.sandbox.getDocument();

      
// Only do the auto linking by ourselves when the browser doesn't support auto linking
      // OR when he supports auto linking but we were able to turn it off (IE9+)
      
if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
        
this.parent.observe("newword:composer", function() {
          
selection.executeAndRestore(sandboxDoc, function(startContainerendContainer) {
            
dom.autoLink(endContainer.parentNode);
          });
        });
      }

      
// Assuming we have the following:
      //  <a href="http://www.google.de">http://www.google.de</a>
      // If a user now changes the url in the innerHTML we want to make sure that
      // it's synchronized with the href attribute (as long as the innerHTML is still a url)
      
var // Use a live NodeList to check whether there are any links in the document
          
links           sandboxDoc.getElementsByTagName("a"),
          
// The autoLink helper method reveals a reg exp to detect correct urls
          
urlRegExp       dom.autoLink.URL_REG_EXP,
          
getTextContent  = function(element) {
            var 
textContent wysihtml5.lang.string(dom.getTextContent(element)).trim();
            if (
textContent.substr(04) === "www.") {
              
textContent "http://" textContent;
            }
            return 
textContent;
          };

      
dom.observe(this.element"keydown", function(event) {
        if (!
links.length) {
          return;
        }

        var 
selectedNode selection.getSelectedNode(event.target.ownerDocument),
            
link         dom.getParentElement(selectedNode, { nodeName"A" }, 4),
            
textContent;

        if (!
link) {
          return;
        }

        
textContent getTextContent(link);
        
// keydown is fired before the actual content is changed
        // therefore we set a timeout to change the href
        
setTimeout(function() {
          var 
newTextContent getTextContent(link);
          if (
newTextContent === textContent) {
            return;
          }

          
// Only set href when new href looks like a valid url
          
if (newTextContent.match(urlRegExp)) {
            
link.setAttribute("href"newTextContent);
          }
        }, 
0);
      });
    },

    
_initObjectResizing: function() {
      var 
properties        = ["width""height"],
          
propertiesLength  properties.length,
          
element           this.element;
      
      
wysihtml5.commands.exec(element"enableObjectResizing"this.config.allowObjectResizing);

      if (
this.config.allowObjectResizing) {
         
// IE sets inline styles after resizing objects
         // The following lines make sure that the width/height css properties
         // are copied over to the width/height attributes
        
if (browser.supportsEvent("resizeend")) {
          
dom.observe(element"resizeend", function(event) {
            var 
target event.target || event.srcElement,
                
style  target.style,
                
i      0,
                
property;
            for(; 
i<propertiesLengthi++) {
              
property properties[i];
              if (
style[property]) {
                
target.setAttribute(propertyparseInt(style[property], 10));
                
style[property] = "";
              }
            }
            
// After resizing IE sometimes forgets to remove the old resize handles
            
wysihtml5.quirks.redraw(element);
          });
        }
      } else {
        if (
browser.supportsEvent("resizestart")) {
          
dom.observe(element"resizestart", function(event) { event.preventDefault(); });
        }
      }
    },
    
    
_initUndoManager: function() {
      new 
wysihtml5.UndoManager(this.parent);
    }
  });
})(
wysihtml5);(function(wysihtml5) {
  var 
dom             wysihtml5.dom,
      
doc             document,
      
win             window,
      
HOST_TEMPLATE   doc.createElement("div"),
      
/**
       * Styles to copy from textarea to the composer element
       */
      
TEXT_FORMATTING = [
        
"background-color",
        
"color""cursor",
        
"font-family""font-size""font-style""font-variant""font-weight",
        
"line-height""letter-spacing",
        
"text-align""text-decoration""text-indent""text-rendering",
        
"word-break""word-wrap""word-spacing"
      
],
      
/**
       * Styles to copy from textarea to the iframe
       */
      
BOX_FORMATTING = [
        
"background-color",
        
"border-collapse",
        
"border-bottom-color""border-bottom-style""border-bottom-width",
        
"border-left-color""border-left-style""border-left-width",
        
"border-right-color""border-right-style""border-right-width",
        
"border-top-color""border-top-style""border-top-width",
        
"clear""display""float",
        
"margin-bottom""margin-left""margin-right""margin-top",
        
"outline-color""outline-offset""outline-width""outline-style",
        
"padding-left""padding-right""padding-top""padding-bottom",
        
"position""top""left""right""bottom""z-index",
        
"vertical-align""text-align",
        
"-webkit-box-sizing""-moz-box-sizing""-ms-box-sizing""box-sizing",
        
"-webkit-box-shadow""-moz-box-shadow""-ms-box-shadow","box-shadow",
        
"width""height"
      
],
      
/**
       * Styles to sync while the window gets resized
       */
      
RESIZE_STYLE = [
        
"width""height",
        
"top""left""right""bottom"
      
],
      
ADDITIONAL_CSS_RULES = [
        
"html             { height: 100%; }",
        
"body             { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
        
"._wysihtml5-temp { display: none; }",
        
wysihtml5.browser.isGecko ?
          
"body.placeholder { color: graytext !important; }" 
          
"body.placeholder { color: #a9a9a9 !important; }",
        
"body[disabled]   { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
        
// Ensure that user see's broken images and can delete them
        
"img:-moz-broken  { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
      
];
  
  
/**
   * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
   * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
   *
   * Other browsers need a more hacky way: (pssst don't tell my mama)
   * In order to prevent the element being scrolled into view when focusing it, we simply
   * move it out of the scrollable area, focus it, and reset it's position
   */
  
var focusWithoutScrolling = function(element) {
    if (
element.setActive) {
      
// Following line could cause a js error when the textarea is invisible
      // See https://github.com/xing/wysihtml5/issues/9
      
try { element.setActive(); } catch(e) {}
    } else {
      var 
elementStyle element.style,
          
originalScrollTop doc.documentElement.scrollTop || doc.body.scrollTop,
          
originalScrollLeft doc.documentElement.scrollLeft || doc.body.scrollLeft,
          
originalStyles = {
            
position:         elementStyle.position,
            
top:              elementStyle.top,
            
left:             elementStyle.left,
            
WebkitUserSelectelementStyle.WebkitUserSelect
          
};
      
      
dom.setStyles({
        
position:         "absolute",
        
top:              "-99999px",
        
left:             "-99999px",
        
// Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
        
WebkitUserSelect"none"
      
}).on(element);
      
      
element.focus();
      
      
dom.setStyles(originalStyles).on(element);
      
      if (
win.scrollTo) {
        
// Some browser extensions unset this method to prevent annoyances
        // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
        // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
        
win.scrollTo(originalScrollLeftoriginalScrollTop);
      }
    }
  };
  
  
  
wysihtml5.views.Composer.prototype.style = function() {
    var 
that                  this,
        
originalActiveElement doc.querySelector(":focus"),
        
textareaElement       this.textarea.element,
        
hasPlaceholder        textareaElement.hasAttribute("placeholder"),
        
originalPlaceholder   hasPlaceholder && textareaElement.getAttribute("placeholder");
    
this.focusStylesHost      this.focusStylesHost  || HOST_TEMPLATE.cloneNode(false);
    
this.blurStylesHost       this.blurStylesHost   || HOST_TEMPLATE.cloneNode(false);
  
    
// Remove placeholder before copying (as the placeholder has an affect on the computed style)
    
if (hasPlaceholder) {
      
textareaElement.removeAttribute("placeholder");
    }
  
    if (
textareaElement === originalActiveElement) {
      
textareaElement.blur();
    }
  
    
// --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
    
dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
  
    
// --------- editor styles ---------
    
dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
  
    
// --------- apply standard rules ---------
    
dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
  
    
// --------- :focus styles ---------
    
focusWithoutScrolling(textareaElement);
    
dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
    
dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
  
    
// Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
    // this is needed for when the change_view event is fired where the iframe is hidden and then
    // the blur event fires and re-displays it
    
var boxFormattingStyles wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
  
    
// --------- restore focus ---------
    
if (originalActiveElement) {
      
originalActiveElement.focus();
    } else {
      
textareaElement.blur();
    }
  
    
// --------- restore placeholder ---------
    
if (hasPlaceholder) {
      
textareaElement.setAttribute("placeholder"originalPlaceholder);
    }
  
    
// When copying styles, we only get the computed style which is never returned in percent unit
    // Therefore we've to recalculate style onresize
    
if (!wysihtml5.browser.hasCurrentStyleProperty()) {
      
dom.observe(win"resize", function() {
        var 
originalDisplayStyle dom.getStyle("display").from(textareaElement);
        
textareaElement.style.display "";
        
dom.copyStyles(RESIZE_STYLE)
          .
from(textareaElement)
          .
to(that.iframe)
          .
andTo(that.focusStylesHost)
          .
andTo(that.blurStylesHost);
        
textareaElement.style.display originalDisplayStyle;
      });
    }
  
    
// --------- Sync focus/blur styles ---------
    
this.parent.observe("focus:composer", function() {
      
dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
      
dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
    });

    
this.parent.observe("blur:composer", function() {
      
dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
      
dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
    });
  
    return 
this;
  };
})(
wysihtml5);/**
 * Taking care of events
 *  - Simulating 'change' event on contentEditable element
 *  - Handling drag & drop logic
 *  - Catch paste events
 *  - Dispatch proprietary newword:composer event
 *  - Keyboard shortcuts
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 */
(function(wysihtml5) {
  var 
dom       wysihtml5.dom,
      
browser   wysihtml5.browser,
      
/**
       * Map keyCodes to query commands
       */
      
shortcuts = {
        
"66""bold",     // B
        
"73""italic",   // I
        
"85""underline" // U
      
};
  
  
wysihtml5.views.Composer.prototype.observe = function() {
    var 
that                this,
        
state               this.getValue(),
        
iframe              this.sandbox.getIframe(),
        
element             this.element,
        
focusBlurElement    browser.supportsEventsInIframeCorrectly() ? element this.sandbox.getWindow(),
        
// Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same
        
pasteEvents         browser.supportsEvent("drop") ? ["drop""paste"] : ["dragdrop""paste"];

    
// --------- destroy:composer event ---------
    
dom.observe(iframe"DOMNodeRemoved", function() {
      
clearInterval(domNodeRemovedInterval);
      
that.parent.fire("destroy:composer");
    });

    
// DOMNodeRemoved event is not supported in IE 8
    
var domNodeRemovedInterval setInterval(function() {
      if (!
dom.contains(document.documentElementiframe)) {
        
clearInterval(domNodeRemovedInterval);
        
that.parent.fire("destroy:composer");
      }
    }, 
250);


    
// --------- Focus & blur logic ---------
    
dom.observe(focusBlurElement"focus", function() {
      
that.parent.fire("focus").fire("focus:composer");

      
// Delay storing of state until all focus handler are fired
      // especially the one which resets the placeholder
      
setTimeout(function() { state that.getValue(); }, 0);
    });

    
dom.observe(focusBlurElement"blur", function() {
      if (
state !== that.getValue()) {
        
that.parent.fire("change").fire("change:composer");
      }
      
that.parent.fire("blur").fire("blur:composer");
    });

    
// --------- Drag & Drop logic ---------
    
dom.observe(element"dragenter", function() {
      
that.parent.fire("unset_placeholder");
    });

    if (
browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
      
dom.observe(element, ["dragover""dragenter"], function(event) {
        
event.preventDefault();
      });
    }

    
dom.observe(elementpasteEvents, function(event) {
      var 
dataTransfer event.dataTransfer,
          
data;

      if (
dataTransfer && browser.supportsDataTransfer()) {
        
data dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");
      }
      if (
data) {
        
element.focus();
        
wysihtml5.commands.exec(element"insertHTML"data);
        
that.parent.fire("paste").fire("paste:composer");
        
event.stopPropagation();
        
event.preventDefault();
      } else {
        
setTimeout(function() {
          
that.parent.fire("paste").fire("paste:composer");
        }, 
0);
      }
    });

    
// --------- neword event ---------
    
dom.observe(element"keyup", function(event) {
      var 
keyCode event.keyCode;
      if (
keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
        
that.parent.fire("newword:composer");
      }
    });

    
this.parent.observe("paste:composer", function() {
      
setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
    });

    
// --------- Make sure that images are selected when clicking on them ---------
    
if (!browser.canSelectImagesInContentEditable()) {
      
dom.observe(element"mousedown", function(event) {
        var 
target event.target;
        if (
target.nodeName === "IMG") {
          
wysihtml5.selection.selectNode(target);
          
event.preventDefault();
        }
      });
    }

    
// --------- Shortcut logic ---------
    
dom.observe(element"keydown", function(event) {
      var 
keyCode  event.keyCode,
          
command  shortcuts[keyCode];
      if ((
event.ctrlKey || event.metaKey) && command) {
        
wysihtml5.commands.exec(elementcommand);
        
event.preventDefault();
      }
    });

    
// --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
    
dom.observe(element"keydown", function(event) {
      var 
target  wysihtml5.selection.getSelectedNode(element.ownerDocumenttrue),
          
keyCode event.keyCode,
          
parent;
      if (
target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
        
parent target.parentNode;
        
// delete the <img>
        
parent.removeChild(target);
        
// and it's parent <a> too if it hasn't got any other child nodes
        
if (parent.nodeName === "A" && !parent.firstChild) {
          
parent.parentNode.removeChild(parent);
        }

        
setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
        
event.preventDefault();
      }
    });

    
// --------- Show url in tooltip when hovering links or images ---------
    
var titlePrefixes = {
      
IMG"Image: ",
      
A:   "Link: "
    
};
    
    
dom.observe(element"mouseover", function(event) {
      var 
target   event.target,
          
nodeName target.nodeName,
          
title;
      if (
nodeName !== "A" && nodeName !== "IMG") {
        return;
      }

      
title titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
      
target.setAttribute("title"title);
    });
  };
})(
wysihtml5);/**
 * Class that takes care that the value of the composer and the textarea is always in sync
 */
(function(wysihtml5) {
  var 
INTERVAL 400;
  
  
wysihtml5.views.Synchronizer Base.extend(
    
/** @scope wysihtml5.views.Synchronizer.prototype */ {

    
constructor: function(editortextareacomposer) {
      
this.editor   editor;
      
this.textarea textarea;
      
this.composer composer;

      
this._observe();
    },

    
/**
     * Sync html from composer to textarea
     * Takes care of placeholders
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
     */
    
fromComposerToTextarea: function(shouldParseHtml) {
      
this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
    },

    
/**
     * Sync value of textarea to composer
     * Takes care of placeholders
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
     */
    
fromTextareaToComposer: function(shouldParseHtml) {
      var 
textareaValue this.textarea.getValue();
      if (
textareaValue) {
        
this.composer.setValue(textareaValueshouldParseHtml);
      } else {
        
this.composer.clear();
        
this.editor.fire("set_placeholder");
      }
    },

    
/**
     * Invoke syncing based on view state
     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
     */
    
sync: function(shouldParseHtml) {
      if (
this.editor.currentView.name === "textarea") {
        
this.fromTextareaToComposer(shouldParseHtml);
      } else {
        
this.fromComposerToTextarea(shouldParseHtml);
      }
    },

    
/**
     * Initializes interval-based syncing
     * also makes sure that on-submit the composer's content is synced with the textarea
     * immediately when the form gets submitted
     */
    
_observe: function() {
      var 
interval,
          
that          this,
          
form          this.textarea.element.form,
          
startInterval = function() {
            
interval setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
          },
          
stopInterval  = function() {
            
clearInterval(interval);
            
interval null;
          };

      
startInterval();

      if (
form) {
        
// If the textarea is in a form make sure that after onreset and onsubmit the composer
        // has the correct state
        
wysihtml5.dom.observe(form"submit", function() {
          
that.sync(true);
        });
        
wysihtml5.dom.observe(form"reset", function() {
          
setTimeout(function() { that.fromTextareaToComposer(); }, 0);
        });
      }

      
this.editor.observe("change_view", function(view) {
        if (
view === "composer" && !interval) {
          
that.fromTextareaToComposer(true);
          
startInterval();
        } else if (
view === "textarea") {
          
that.fromComposerToTextarea(true);
          
stopInterval();
        }
      });

      
this.editor.observe("destroy:composer"stopInterval);
    }
  });
})(
wysihtml5);
wysihtml5.views.Textarea wysihtml5.views.View.extend(
  
/** @scope wysihtml5.views.Textarea.prototype */ {
  
name"textarea",
  
  
constructor: function(parenttextareaElementconfig) {
    
this.base(parenttextareaElementconfig);
    
    
this._observe();
  },
  
  
clear: function() {
    
this.element.value "";
  },
  
  
getValue: function(parse) {
    var 
value this.isEmpty() ? "" this.element.value;
    if (
parse) {
      
value this.parent.parse(value);
    }
    return 
value;
  },
  
  
setValue: function(htmlparse) {
    if (
parse) {
      
html this.parent.parse(html);
    }
    
this.element.value html;
  },
  
  
hasPlaceholderSet: function() {
    var 
supportsPlaceholder wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
        
placeholderText     this.element.getAttribute("placeholder") || null,
        
value               this.element.value,
        
isEmpty             = !value;
    return (
supportsPlaceholder && isEmpty) || (value === placeholderText);
  },
  
  
isEmpty: function() {
    return !
wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
  },
  
  
_observe: function() {
    var 
element this.element,
        
parent  this.parent,
        
eventMapping = {
          
focusin:  "focus",
          
focusout"blur"
        
},
        
/**
         * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
         * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
         */
        
events wysihtml5.browser.supportsEvent("focusin") ? ["focusin""focusout""change"] : ["focus""blur""change"];
    
    
parent.observe("beforeload", function() {
      
wysihtml5.dom.observe(elementevents, function(event) {
        var 
eventName eventMapping[event.type] || event.type;
        
parent.fire(eventName).fire(eventName ":textarea");
      });
      
      
wysihtml5.dom.observe(element, ["paste""drop"], function() {
        
setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
      });
    });
  }
});
/**
 * Toolbar Dialog
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} link The toolbar link which causes the dialog to show up
 * @param {Element} container The dialog container
 *
 * @example
 *    <!-- Toolbar link -->
 *    <a data-wysihtml5-command="insertImage">insert an image</a>
 *
 *    <!-- Dialog -->
 *    <div data-wysihtml5-dialog="insertImage" style="display: none;">
 *      <label>
 *        URL: <input data-wysihtml5-dialog-field="src" value="http://">
 *      </label>
 *      <label>
 *        Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
 *      </label>
 *    </div>
 *
 *    <script>
 *      var dialog = new wysihtml5.toolbar.Dialog(
 *        document.querySelector("[data-wysihtml5-command='insertImage']"),
 *        document.querySelector("[data-wysihtml5-dialog='insertImage']")
 *      );
 *      dialog.observe("save", function(attributes) {
 *        // do something
 *      });
 *    </script>
 */
(function(wysihtml5) {
  var 
dom                     wysihtml5.dom,
      
CLASS_NAME_OPENED       "wysihtml5-command-dialog-opened",
      
SELECTOR_FORM_ELEMENTS  "input, select, textarea",
      
SELECTOR_FIELDS         "[data-wysihtml5-dialog-field]",
      
ATTRIBUTE_FIELDS        "data-wysihtml5-dialog-field";
      
  
  
wysihtml5.toolbar.Dialog wysihtml5.lang.Dispatcher.extend(
    
/** @scope wysihtml5.toolbar.Dialog.prototype */ {
    
constructor: function(linkcontainer) {
      
this.link       link;
      
this.container  container;
    },

    
_observe: function() {
      if (
this._observed) {
        return;
      }
      
      var 
that this,
          
callbackWrapper = function(event) {
            var 
attributes that._serialize();
            if (
attributes == that.elementToChange) {
              
that.fire("edit"attributes);
            } else {
              
that.fire("save"attributes);
            }
            
that.hide();
            
event.preventDefault();
            
event.stopPropagation();
          };

      
dom.observe(that.link"click", function(event) {
        if (
dom.hasClass(that.linkCLASS_NAME_OPENED)) {
          
setTimeout(function() { that.hide(); }, 0);
        }
      });

      
dom.observe(this.container"keydown", function(event) {
        var 
keyCode event.keyCode;
        if (
keyCode === wysihtml5.ENTER_KEY) {
          
callbackWrapper(event);
        }
        if (
keyCode === wysihtml5.ESCAPE_KEY) {
          
that.hide();
        }
      });

      
dom.delegate(this.container"[data-wysihtml5-dialog-action=save]""click"callbackWrapper);

      
dom.delegate(this.container"[data-wysihtml5-dialog-action=cancel]""click", function(event) {
        
that.fire("cancel");
        
that.hide();
        
event.preventDefault();
        
event.stopPropagation();
      });

      var 
formElements  this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
          
i             0,
          
length        formElements.length,
          
_clearInterval = function() { clearInterval(that.interval); };
      for (; 
i<lengthi++) {
        
dom.observe(formElements[i], "change"_clearInterval);
      }

      
this._observed true;
    },

    
/**
     * Grabs all fields in the dialog and puts them in key=>value style in an object which
     * then gets returned
     */
    
_serialize: function() {
      var 
data    this.elementToChange || {},
          
fields  this.container.querySelectorAll(SELECTOR_FIELDS),
          
length  fields.length,
          
i       0;
      for (; 
i<lengthi++) {
        
data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
      }
      return 
data;
    },

    
/**
     * Takes the attributes of the "elementToChange"
     * and inserts them in their corresponding dialog input fields
     * 
     * Assume the "elementToChange" looks like this:
     *    <a href="http://www.google.com" target="_blank">foo</a>
     *
     * and we have the following dialog:
     *    <input type="text" data-wysihtml5-dialog-field="href" value="">
     *    <input type="text" data-wysihtml5-dialog-field="target" value="">
     * 
     * after calling _interpolate() the dialog will look like this
     *    <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
     *    <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
     *
     * Basically it adopted the attribute values into the corresponding input fields
     *
     */
    
_interpolate: function() {
      var 
field,
          
fieldName,
          
newValue,
          
focusedElement document.querySelector(":focus"),
          
fields         this.container.querySelectorAll(SELECTOR_FIELDS),
          
length         fields.length,
          
i              0;
      for (; 
i<lengthi++) {
        
field fields[i];
        
// Never change elements where the user is currently typing in
        
if (field === focusedElement) {
          continue;
        }
        
fieldName field.getAttribute(ATTRIBUTE_FIELDS);
        
newValue  this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;
        
field.value newValue;
      }
    },

    
/**
     * Show the dialog element
     */
    
show: function(elementToChange) {
      var 
that        this,
          
firstField  this.container.querySelector(SELECTOR_FORM_ELEMENTS);
      
this.elementToChange elementToChange;
      
this._observe();
      
this._interpolate();
      if (
elementToChange) {
        
this.interval setInterval(function() { that._interpolate(); }, 500);
      }
      
dom.addClass(this.linkCLASS_NAME_OPENED);
      
this.container.style.display "";
      
this.fire("show");
      if (
firstField && !elementToChange) {
        try {
          
firstField.focus();
        } catch(
e) {}
      }
    },

    
/**
     * Hide the dialog element
     */
    
hide: function() {
      
clearInterval(this.interval);
      
this.elementToChange null;
      
dom.removeClass(this.linkCLASS_NAME_OPENED);
      
this.container.style.display "none";
      
this.fire("hide");
    }
  });
})(
wysihtml5);
/**
 * Converts speech-to-text and inserts this into the editor
 * As of now (2011/03/25) this only is supported in Chrome >= 11
 *
 * Note that it sends the recorded audio to the google speech recognition api:
 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
 *
 * Current HTML5 draft can be found here
 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
 * 
 * "Accessing Google Speech API Chrome 11"
 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 */
(function(wysihtml5) {
  var 
dom wysihtml5.dom;
  
  var 
linkStyles = {
    
position"relative"
  
};
  
  var 
wrapperStyles = {
    
left:     0,
    
margin:   0,
    
opacity:  0,
    
overflow"hidden",
    
padding:  0,
    
position"absolute",
    
top:      0,
    
zIndex:   1
  
};
  
  var 
inputStyles = {
    
cursor:     "inherit",
    
fontSize:   "50px",
    
height:     "50px",
    
marginTop:  "-25px",
    
outline:    0,
    
padding:    0,
    
position:   "absolute",
    
right:      "-4px",
    
top:        "50%"
  
};
  
  var 
inputAttributes = {
    
"x-webkit-speech""",
    
"speech":          ""
  
};
  
  
wysihtml5.toolbar.Speech = function(parentlink) {
    var 
input document.createElement("input");
    if (!
wysihtml5.browser.supportsSpeechApiOn(input)) {
      
link.style.display "none";
      return;
    }
    
    var 
wrapper document.createElement("div");
    
    
wysihtml5.lang.object(wrapperStyles).merge({
      
width:  link.offsetWidth  "px",
      
heightlink.offsetHeight "px"
    
});
    
    
dom.insert(input).into(wrapper);
    
dom.insert(wrapper).into(link);
    
    
dom.setStyles(inputStyles).on(input);
    
dom.setAttributes(inputAttributes).on(input)
    
    
dom.setStyles(wrapperStyles).on(wrapper);
    
dom.setStyles(linkStyles).on(link);
    
    var 
eventName "onwebkitspeechchange" in input "webkitspeechchange" "speechchange";
    
dom.observe(inputeventName, function() {
      
parent.execCommand("insertText"input.value);
      
input.value "";
    });
    
    
dom.observe(input"click", function(event) {
      if (
dom.hasClass(link"wysihtml5-command-disabled")) {
        
event.preventDefault();
      }
      
      
event.stopPropagation();
    });
  };
})(
wysihtml5);/**
 * Toolbar
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Object} parent Reference to instance of Editor instance
 * @param {Element} container Reference to the toolbar container element
 *
 * @example
 *    <div id="toolbar">
 *      <a data-wysihtml5-command="createLink">insert link</a>
 *      <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
 *    </div>
 *
 *    <script>
 *      var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
 *    </script>
 */
(function(wysihtml5) {
  var 
CLASS_NAME_COMMAND_DISABLED   "wysihtml5-command-disabled",
      
CLASS_NAME_COMMANDS_DISABLED  "wysihtml5-commands-disabled",
      
CLASS_NAME_COMMAND_ACTIVE     "wysihtml5-command-active",
      
dom                           wysihtml5.dom;
  
  
wysihtml5.toolbar.Toolbar Base.extend(
    
/** @scope wysihtml5.toolbar.Toolbar.prototype */ {
    
constructor: function(editorcontainer) {
      
this.editor     editor;
      
this.container  typeof(container) === "string" document.getElementById(container) : container;
      
this.composer   editor.composer;

      
this._getLinks("command");
      
this._getLinks("action");

      
this._observe();
      
this.show();
      
      var 
speechInputLinks  this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
          
length            speechInputLinks.length,
          
i                 0;
      for (; 
i<lengthi++) {
        new 
wysihtml5.toolbar.Speech(thisspeechInputLinks[i]);
      }
    },

    
_getLinks: function(type) {
      var 
links   this[type "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("a[data-wysihtml5-" type "]")).get(),
          
length  links.length,
          
i       0,
          
mapping this[type "Mapping"] = {},
          
link,
          
name,
          
value,
          
dialog;
      for (; 
i<lengthi++) {
        
link    links[i];
        
name    link.getAttribute("data-wysihtml5-" type);
        
value   link.getAttribute("data-wysihtml5-" type "-value");
        
dialog  this._getDialog(linkname);
        
        
mapping[name ":" value] = {
          
link:   link,
          
name:   name,
          
value:  value,
          
dialogdialog,
          
state:  false
        
};
      }
    },

    
_getDialog: function(linkcommand) {
      var 
that          this,
          
dialogElement this.container.querySelector("[data-wysihtml5-dialog='" command "']"),
          
sandboxDoc    this.composer.sandbox.getDocument(),
          
dialog,
          
caretBookmark;
      if (
dialogElement) {
        
dialog = new wysihtml5.toolbar.Dialog(linkdialogElement);

        
dialog.observe("show", function() {
          
caretBookmark wysihtml5.selection.getBookmark(sandboxDoc);

          
that.editor.fire("show:dialog", { commandcommanddialogContainerdialogElementcommandLinklink });
        });

        
dialog.observe("save", function(attributes) {
          
that.editor.focus(false);

          if (
caretBookmark) {
            
wysihtml5.selection.setBookmark(caretBookmark);
          }
          
that._execCommand(commandattributes);
          
          
that.editor.fire("save:dialog", { commandcommanddialogContainerdialogElementcommandLinklink });
        });

        
dialog.observe("cancel", function() {
          
that.editor.focus(false);
          
that.editor.fire("cancel:dialog", { commandcommanddialogContainerdialogElementcommandLinklink });
        });
      }
      return 
dialog;
    },

    
/**
     * @example
     *    var toolbar = new wysihtml5.Toolbar();
     *    // Insert a <blockquote> element or wrap current selection in <blockquote>
     *    toolbar.execCommand("formatBlock", "blockquote");
     */
    
execCommand: function(commandcommandValue) {
      if (
this.commandsDisabled) {
        return;
      }

      var 
commandObj this.commandMapping[command ":" commandValue];

      
// Show dialog when available
      
if (commandObj && commandObj.dialog && !commandObj.state) {
        
commandObj.dialog.show();
      } else {
        
this._execCommand(commandcommandValue);
      }
    },

    
_execCommand: function(commandcommandValue) {
      
// Make sure that composer is focussed (false => don't move caret to the end)
      
this.editor.focus(false);

      
wysihtml5.commands.exec(this.composer.elementcommandcommandValue);
      
this._updateLinkStates();
    },

    
execAction: function(action) {
      var 
editor this.editor;
      switch(
action) {
        case 
"change_view":
          if (
editor.currentView === editor.textarea) {
            
editor.fire("change_view""composer");
          } else {
            
editor.fire("change_view""textarea");
          }
          break;
      }
    },

    
_observe: function() {
      var 
that      this,
          
editor    this.editor,
          
container this.container,
          
links     this.commandLinks.concat(this.actionLinks),
          
length    links.length,
          
i         0;
      
      for (; 
i<lengthi++) {
        
// 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
        // (you know, a:link { ... } doesn't match anchors with missing href attribute)
        
dom.setAttributes({
          
href:         "javascript:;",
          
unselectable"on"
        
}).on(links[i]);
      }

      
// Needed for opera
      
dom.delegate(container"[data-wysihtml5-command]""mousedown", function(event) { event.preventDefault(); });
      
      
dom.delegate(container"[data-wysihtml5-command]""click", function(event) {
        var 
link          event.target,
            
command       link.getAttribute("data-wysihtml5-command"),
            
commandValue  link.getAttribute("data-wysihtml5-command-value");
        
that.execCommand(commandcommandValue);
        
event.preventDefault();
      });

      
dom.delegate(container"[data-wysihtml5-action]""click", function(event) {
        var 
action event.target.getAttribute("data-wysihtml5-action");
        
that.execAction(action);
        
event.preventDefault();
      });

      
editor.observe("focus:composer", function() {
        
that.bookmark null;
        
clearInterval(that.interval);
        
that.interval setInterval(function() { that._updateLinkStates(); }, 500);
      });

      
editor.observe("blur:composer", function() {
        
clearInterval(that.interval);
      });

      
editor.observe("destroy:composer", function() {
        
clearInterval(that.interval);
      });

      
editor.observe("change_view", function(currentView) {
        
// Set timeout needed in order to let the blur event fire first
        
setTimeout(function() {
          
that.commandsDisabled = (currentView !== "composer");
          
that._updateLinkStates();
          if (
that.commandsDisabled) {
            
dom.addClass(containerCLASS_NAME_COMMANDS_DISABLED);
          } else {
            
dom.removeClass(containerCLASS_NAME_COMMANDS_DISABLED);
          }
        }, 
0);
      });
    },

    
_updateLinkStates: function() {
      var 
element           this.composer.element,
          
commandMapping    this.commandMapping,
          
i,
          
state,
          
command;
      
// every millisecond counts... this is executed quite often
      
for (i in commandMapping) {
        
command commandMapping[i];
        if (
this.commandsDisabled) {
          
state false;
          
dom.removeClass(command.linkCLASS_NAME_COMMAND_ACTIVE);
          if (
command.dialog) {
            
command.dialog.hide();
          }
        } else {
          
state wysihtml5.commands.state(elementcommand.namecommand.value);
          if (
wysihtml5.lang.object(state).isArray()) {
            
// Grab first and only object/element in state array, otherwise convert state into boolean
            // to avoid showing a dialog for multiple selected elements which may have different attributes
            // eg. when two links with different href are selected, the state will be an array consisting of both link elements
            // but the dialog interface can only update one
            
state state.length === state[0] : true;
          }
          
dom.removeClass(command.linkCLASS_NAME_COMMAND_DISABLED);
        }

        if (
command.state === state) {
          continue;
        }

        
command.state state;
        if (
state) {
          
dom.addClass(command.linkCLASS_NAME_COMMAND_ACTIVE);
          if (
command.dialog) {
            if (
typeof(state) === "object") {
              
command.dialog.show(state);
            } else {
              
command.dialog.hide();
            }
          }
        } else {
          
dom.removeClass(command.linkCLASS_NAME_COMMAND_ACTIVE);
          if (
command.dialog) {
            
command.dialog.hide();
          }
        }
      }
    },

    
show: function() {
      
this.container.style.display "";
    },

    
hide: function() {
      
this.container.style.display "none";
    }
  });
  
})(
wysihtml5);
/**
 * Rich Text Query/Formatting Commands
 * 
 * @author Christopher Blum <christopher.blum@xing.com>
 */
wysihtml5.commands = {
  
/**
   * Check whether the browser supports the given command
   *
   * @param {Object} element The element which has contentEditable=true
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
   * @example
   *    wysihtml5.commands.supports(element, "createLink");
   */
  
support: function(elementcommand) {
    return 
wysihtml5.browser.supportsCommand(element.ownerDocumentcommand);
  },
  
  
/**
   * Check whether the browser supports the given command
   *
   * @param {Object} element The element which has contentEditable=true
   * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
   * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
   * @example
   *    wysihtml5.commands.exec(element, "insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
   */
  
exec: function(elementcommandvalue) {
    var 
obj     this[command],
        
method  obj && obj.exec;
    if (
method) {
      return 
method.call(objelementcommandvalue);
    } else {
      try {
        
// try/catch for buggy firefox
        
return element.ownerDocument.execCommand(commandfalsevalue);
      } catch(
e) {}
    }
  },
  
  
/**
   * Check whether the current command is active
   * If the caret is within a bold text, then calling this with command "bold" should return true
   *
   * @param {Object} element The element which has contentEditable=true
   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
   * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
   * @return {Boolean} Whether the command is active
   * @example
   *    var isCurrentSelectionBold = wysihtml5.commands.state(element, "bold");
   */
  
state: function(elementcommandcommandValue) {
    var 
obj     this[command],
        
method  obj && obj.state;
    if (
method) {
      return 
method.call(objelementcommandcommandValue);
    } else {
      try {
        
// try/catch for buggy firefox
        
return element.ownerDocument.queryCommandState(command);
      } catch(
e) {
        return 
false;
      }
    }
  },
  
  
/**
   * Get the current command's value
   *
   * @param {Object} element The element which has contentEditable=true
   * @param {String} command The command string which to check (eg. "formatBlock")
   * @return {String} The command value
   * @example
   *    var currentBlockElement = wysihtml5.commands.value(element, "formatBlock");
   */
  
value: function(elementcommand) {
    var 
obj     this[command],
        
method  obj && obj.value;
    if (
method) {
      
method(elementcommand);
    } else {
      try {
        
// try/catch for buggy firefox
        
return element.ownerDocument.queryCommandValue(command);
      } catch(
e) {
        return 
null;
      }
    }
  }
};(function(
wysihtml5) {
  var 
undef;
  
  
wysihtml5.commands.bold = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatInline.exec(elementcommand"b");
    },

    
state: function(elementcommandcolor) {
      
// element.ownerDocument.queryCommandState("bold") results:
      // firefox: only <b>
      // chrome:  <b>, <strong>, <h1>, <h2>, ...
      // ie:      <b>, <strong>
      // opera:   <b>, <strong>
      
return wysihtml5.commands.formatInline.state(elementcommand"b");
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);

(function(
wysihtml5) {
  var 
undef,
      
NODE_NAME "A",
      
dom       wysihtml5.dom;
  
  function 
_removeFormat(elementanchors) {
    var 
length anchors.length,
        
i      0,
        
anchor,
        
codeElement,
        
textContent;
    for (; 
i<lengthi++) {
      
anchor      anchors[i];
      
codeElement dom.getParentElement(anchor, { nodeName"code" });
      
textContent dom.getTextContent(anchor);

      
// if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
      // else replace <a> with its childNodes
      
if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
        
// <code> element is used to prevent later auto-linking of the content
        
codeElement dom.renameElement(anchor"code");
      } else {
        
dom.replaceWithChildNodes(anchor);
      }
    }
  }

  function 
_format(elementattributes) {
    var 
doc             element.ownerDocument,
        
tempClass       "_wysihtml5-temp-" + (+new Date()),
        
tempClassRegExp = /non-matching-class/g,
        
i               0,
        
length,
        
anchors,
        
anchor,
        
hasElementChild,
        
isEmpty,
        
elementToSetCaretAfter,
        
textContent,
        
whiteSpace,
        
j;
    
wysihtml5.commands.formatInline.exec(elementundefNODE_NAMEtempClasstempClassRegExp);
    
anchors doc.querySelectorAll(NODE_NAME "." tempClass);
    
length  anchors.length;
    for (; 
i<lengthi++) {
      
anchor anchors[i];
      
anchor.removeAttribute("class");
      for (
j in attributes) {
        
anchor.setAttribute(jattributes[j]);
      }
    }

    
elementToSetCaretAfter anchor;
    if (
length === 1) {
      
textContent dom.getTextContent(anchor);
      
hasElementChild = !!anchor.querySelector("*");
      
isEmpty textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
      if (!
hasElementChild && isEmpty) {
        
dom.setTextContent(anchoranchor.href);
        
whiteSpace doc.createTextNode(" ");
        
wysihtml5.selection.setAfter(anchor);
        
wysihtml5.selection.insertNode(whiteSpace);
        
elementToSetCaretAfter whiteSpace;
      }
    }
    
wysihtml5.selection.setAfter(elementToSetCaretAfter);
  }
  
  
wysihtml5.commands.createLink = {
    
/**
     * TODO: Use HTMLApplier or formatInline here
     *
     * Turns selection into a link
     * If selection is already a link, it removes the link and wraps it with a <code> element
     * The <code> element is needed to avoid auto linking
     * 
     * @example
     *    // either ...
     *    wysihtml5.commands.createLink.exec(element, "createLink", "http://www.google.de");
     *    // ... or ...
     *    wysihtml5.commands.createLink.exec(element, "createLink", { href: "http://www.google.de", target: "_blank" });
     */
    
exec: function(elementcommandvalue) {
      var 
doc           element.ownerDocument,
          
anchors       this.state(elementcommand);

      if (
anchors) {
        
// Selection contains links
        
wysihtml5.selection.executeAndRestore(doc, function() {
          
_removeFormat(elementanchors);
        });
      } else {
        
// Create links
        
value typeof(value) === "object" value : { hrefvalue };
        
_format(elementvalue);
      }
    },

    
state: function(elementcommand) {
      return 
wysihtml5.commands.formatInline.state(elementcommand"A");
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);/**
 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
 * which we don't want
 * Instead we set a css class
 */
(function(wysihtml5) {
  var 
undef,
      
REG_EXP = /wysiwyg-font-size-[a-z]+/g;
  
  
wysihtml5.commands.fontSize = {
    
exec: function(elementcommandsize) {
      return 
wysihtml5.commands.formatInline.exec(elementcommand"span""wysiwyg-font-size-" sizeREG_EXP);
    },

    
state: function(elementcommandsize) {
      return 
wysihtml5.commands.formatInline.state(elementcommand"span""wysiwyg-font-size-" sizeREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);
/**
 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
 * which we don't want
 * Instead we set a css class
 */
(function(wysihtml5) {
  var 
undef,
      
REG_EXP = /wysiwyg-color-[a-z]+/g;
  
  
wysihtml5.commands.foreColor = {
    
exec: function(elementcommandcolor) {
      return 
wysihtml5.commands.formatInline.exec(elementcommand"span""wysiwyg-color-" colorREG_EXP);
    },

    
state: function(elementcommandcolor) {
      return 
wysihtml5.commands.formatInline.state(elementcommand"span""wysiwyg-color-" colorREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
dom                     wysihtml5.dom,
      
selection               wysihtml5.selection,
      
DEFAULT_NODE_NAME       "DIV",
      
// Following elements are grouped
      // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
      // instead of creating a H4 within a H1 which would result in semantically invalid html
      
BLOCK_ELEMENTS_GROUP    = ["H1""H2""H3""H4""H5""H6""P""BLOCKQUOTE"DEFAULT_NODE_NAME];
  
  
/**
   * Remove similiar classes (based on classRegExp)
   * and add the desired class name
   */
  
function _addClass(elementclassNameclassRegExp) {
    if (
element.className) {
      
_removeClass(elementclassRegExp);
      
element.className += " " className;
    } else {
      
element.className className;
    }
  }

  function 
_removeClass(elementclassRegExp) {
    
element.className element.className.replace(classRegExp"");
  }

  
/**
   * Check whether given node is a text node and whether it's empty
   */
  
function _isBlankTextNode(node) {
    return 
node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
  }

  
/**
   * Returns previous sibling node that is not a blank text node
   */
  
function _getPreviousSiblingThatIsNotBlank(node) {
    var 
previousSibling node.previousSibling;
    while (
previousSibling && _isBlankTextNode(previousSibling)) {
      
previousSibling previousSibling.previousSibling;
    }
    return 
previousSibling;
  }

  
/**
   * Returns next sibling node that is not a blank text node
   */
  
function _getNextSiblingThatIsNotBlank(node) {
    var 
nextSibling node.nextSibling;
    while (
nextSibling && _isBlankTextNode(nextSibling)) {
      
nextSibling nextSibling.nextSibling;
    }
    return 
nextSibling;
  }

  
/**
   * Adds line breaks before and after the given node if the previous and next siblings
   * aren't already causing a visual line break (block element or <br>)
   */
  
function _addLineBreakBeforeAndAfter(node) {
    var 
doc             node.ownerDocument,
        
nextSibling     _getNextSiblingThatIsNotBlank(node),
        
previousSibling _getPreviousSiblingThatIsNotBlank(node);

    if (
nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
      
node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
    }
    if (
previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
      
node.parentNode.insertBefore(doc.createElement("br"), node);
    }
  }

  
/**
   * Removes line breaks before and after the given node
   */
  
function _removeLineBreakBeforeAndAfter(node) {
    var 
nextSibling     _getNextSiblingThatIsNotBlank(node),
        
previousSibling _getPreviousSiblingThatIsNotBlank(node);

    if (
nextSibling && _isLineBreak(nextSibling)) {
      
nextSibling.parentNode.removeChild(nextSibling);
    }
    if (
previousSibling && _isLineBreak(previousSibling)) {
      
previousSibling.parentNode.removeChild(previousSibling);
    }
  }

  function 
_removeLastChildIfLineBreak(node) {
    var 
lastChild node.lastChild;
    if (
lastChild && _isLineBreak(lastChild)) {
      
lastChild.parentNode.removeChild(lastChild);
    }
  }

  function 
_isLineBreak(node) {
    return 
node.nodeName === "BR";
  }

  
/**
   * Checks whether the elment causes a visual line break
   * (<br> or block elements)
   */
  
function _isLineBreakOrBlockElement(element) {
    if (
_isLineBreak(element)) {
      return 
true;
    }

    if (
dom.getStyle("display").from(element) === "block") {
      return 
true;
    }

    return 
false;
  }

  
/**
   * Execute native query command
   * and if necessary modify the inserted node's className
   */
  
function _execCommand(doccommandnodeNameclassName) {
    if (
className) {
      var 
eventListener dom.observe(doc"DOMNodeInserted", function(event) {
        var 
target event.target,
            
displayStyle;
        if (
target.nodeType !== wysihtml5.ELEMENT_NODE) {
          return;
        }
        
displayStyle  dom.getStyle("display").from(target);
        if (
displayStyle.substr(06) !== "inline") {
          
// Make sure that only block elements receive the given class
          
target.className += " " className;
        }
      });
    }
    
doc.execCommand(commandfalsenodeName);
    if (
eventListener) {
      
eventListener.stop();
    }
  }

  function 
_selectLineAndWrap(element) {
    
selection.selectLine(element.ownerDocument);
    
selection.surround(element);
    
_removeLineBreakBeforeAndAfter(element);
    
_removeLastChildIfLineBreak(element);
    
selection.selectNode(element);
  }

  function 
_hasClasses(element) {
    return !!
wysihtml5.lang.string(element.className).trim();
  }
  
  
wysihtml5.commands.formatBlock = {
    
exec: function(elementcommandnodeNameclassNameclassRegExp) {
      var 
doc          element.ownerDocument,
          
blockElement this.state(elementcommandnodeNameclassNameclassRegExp),
          
selectedNode;

      
nodeName typeof(nodeName) === "string" nodeName.toUpperCase() : nodeName;

      if (
blockElement) {
        
selection.executeAndRestoreSimple(doc, function() {
          if (
classRegExp) {
            
_removeClass(blockElementclassRegExp);
          }
          var 
hasClasses _hasClasses(blockElement);
          if (!
hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
            
// Insert a line break afterwards and beforewards when there are siblings
            // that are not of type line break or block element
            
_addLineBreakBeforeAndAfter(blockElement);
            
dom.replaceWithChildNodes(blockElement);
          } else if (
hasClasses) {
            
// Make sure that styling is kept by renaming the element to <div> and copying over the class name
            
dom.renameElement(blockElementDEFAULT_NODE_NAME);
          }
        });
        return;
      }

      
// Find similiar block element and rename it (<h2 class="foo"></h2>  =>  <h1 class="foo"></h1>)
      
if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
        
selectedNode selection.getSelectedNode(doc);
        
blockElement dom.getParentElement(selectedNode, {
          
nodeName:     BLOCK_ELEMENTS_GROUP
        
});

        if (
blockElement) {
          
selection.executeAndRestoreSimple(doc, function() {
            
// Rename current block element to new block element and add class
            
if (nodeName) {
              
blockElement dom.renameElement(blockElementnodeName);
            }
            if (
className) {
              
_addClass(blockElementclassNameclassRegExp);
            }
          });
          return;
        }
      }

      if (
wysihtml5.commands.support(elementcommand)) {
        
_execCommand(doccommandnodeName || DEFAULT_NODE_NAMEclassName);
        return;
      }

      
blockElement doc.createElement(nodeName || DEFAULT_NODE_NAME);
      if (
className) {
        
blockElement.className className;
      }
      
_selectLineAndWrap(blockElement);
    },

    
state: function(elementcommandnodeNameclassNameclassRegExp) {
      
nodeName typeof(nodeName) === "string" nodeName.toUpperCase() : nodeName;
      var 
selectedNode selection.getSelectedNode(element.ownerDocument);
      return 
dom.getParentElement(selectedNode, {
        
nodeName:     nodeName,
        
className:    className,
        
classRegExp:  classRegExp
      
});
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);/**
 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
 *
 *   #1 caret in unformatted text:
 *      abcdefg|
 *   output:
 *      abcdefg<b>|</b>
 *   
 *   #2 unformatted text selected:
 *      abc|deg|h
 *   output:
 *      abc<b>|deg|</b>h
 *   
 *   #3 unformatted text selected across boundaries:
 *      ab|c <span>defg|h</span>
 *   output:
 *      ab<b>|c </b><span><b>defg</b>|h</span>
 *
 *   #4 formatted text entirely selected
 *      <b>|abc|</b>
 *   output:
 *      |abc|
 *
 *   #5 formatted text partially selected
 *      <b>ab|c|</b>
 *   output:
 *      <b>ab</b>|c|
 *
 *   #6 formatted text selected across boundaries
 *      <span>ab|c</span> <b>de|fgh</b>
 *   output:
 *      <span>ab|c</span> de|<b>fgh</b>
 */
(function(wysihtml5) {
  var 
undef,
      
// Treat <b> as <strong> and vice versa
      
ALIAS_MAPPING = {
        
"strong""b",
        
"em":     "i",
        
"b":      "strong",
        
"i":      "em"
      
},
      
htmlApplier = {};
  
  function 
_getTagNames(tagName) {
    var 
alias ALIAS_MAPPING[tagName];
    return 
alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
  }
  
  function 
_getApplier(tagNameclassNameclassRegExp) {
    var 
identifier tagName ":" className;
    if (!
htmlApplier[identifier]) {
      
htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), classNameclassRegExptrue);
    }
    return 
htmlApplier[identifier];
  }
  
  
wysihtml5.commands.formatInline = {
    
exec: function(elementcommandtagNameclassNameclassRegExp) {
      var 
range wysihtml5.selection.getRange(element.ownerDocument);
      if (!
range) {
        return 
false;
      }
      
_getApplier(tagNameclassNameclassRegExp).toggleRange(range);
      
wysihtml5.selection.setSelection(range);
    },

    
state: function(elementcommandtagNameclassNameclassRegExp) {
      var 
doc           element.ownerDocument,
          
aliasTagName  ALIAS_MAPPING[tagName] || tagName,
          
range;

      
// Check whether the document contains a node with the desired tagName
      
if (!wysihtml5.dom.hasElementWithTagName(doctagName) &&
          !
wysihtml5.dom.hasElementWithTagName(docaliasTagName)) {
        return 
false;
      }

       
// Check whether the document contains a node with the desired className
      
if (className && !wysihtml5.dom.hasElementWithClassName(docclassName)) {
         return 
false;
      }

      
range wysihtml5.selection.getRange(doc);
      if (!
range) {
        return 
false;
      }

      return 
_getApplier(tagNameclassNameclassRegExp).isAppliedToRange(range);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef;
  
  
wysihtml5.commands.insertHTML = {
    
exec: function(elementcommandhtml) {
      if (
wysihtml5.commands.support(elementcommand)) {
        
element.ownerDocument.execCommand(commandfalsehtml);
      } else {
        
wysihtml5.selection.insertHTML(element.ownerDocumenthtml);
      }
    },

    
state: function() {
      return 
false;
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
NODE_NAME "IMG";
  
  
wysihtml5.commands.insertImage = {
    
/**
     * Inserts an <img>
     * If selection is already an image link, it removes it
     * 
     * @example
     *    // either ...
     *    wysihtml5.commands.insertImage.exec(element, "insertImage", "http://www.google.de/logo.jpg");
     *    // ... or ...
     *    wysihtml5.commands.insertImage.exec(element, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
     */
    
exec: function(elementcommandvalue) {
      
value typeof(value) === "object" value : { srcvalue };

      var 
doc   element.ownerDocument,
          
image this.state(element),
          
i,
          
parent;

      if (
image) {
        
// Image already selected, set the caret before it and delete it
        
wysihtml5.selection.setBefore(image);
        
parent image.parentNode;
        
parent.removeChild(image);

        
// and it's parent <a> too if it hasn't got any other relevant child nodes
        
wysihtml5.dom.removeEmptyTextNodes(parent);
        if (
parent.nodeName === "A" && !parent.firstChild) {
          
wysihtml5.selection.setAfter(parent);
          
parent.parentNode.removeChild(parent);
        }

        
// firefox and ie sometimes don't remove the image handles, even though the image got removed
        
wysihtml5.quirks.redraw(element);
        return;
      }

      
image doc.createElement(NODE_NAME);

      for (
i in value) {
        
image[i] = value[i];
      }

      
wysihtml5.selection.insertNode(image);
      
wysihtml5.selection.setAfter(image);
    },

    
state: function(element) {
      var 
doc element.ownerDocument,
          
selectedNode,
          
text,
          
imagesInSelection;

      if (!
wysihtml5.dom.hasElementWithTagName(docNODE_NAME)) {
        return 
false;
      }

      
selectedNode wysihtml5.selection.getSelectedNode(doc);
      if (!
selectedNode) {
        return 
false;
      }

      if (
selectedNode.nodeName === NODE_NAME) {
        
// This works perfectly in IE
        
return selectedNode;
      }

      if (
selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
        return 
false;
      }

      
text wysihtml5.selection.getText(doc);
      
text wysihtml5.lang.string(text).trim();
      if (
text) {
        return 
false;
      }

      
imagesInSelection wysihtml5.selection.getNodes(docwysihtml5.ELEMENT_NODE, function(node) {
        return 
node.nodeName === "IMG";
      });

      if (
imagesInSelection.length !== 1) {
        return 
false;
      }

      return 
imagesInSelection[0];
    },

    
value: function(element) {
      var 
image this.state(element);
      return 
image && image.src;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
LINE_BREAK "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " "");
  
  
wysihtml5.commands.insertLineBreak = {
    
exec: function(elementcommand) {
      if (
wysihtml5.commands.support(elementcommand)) {
        
element.ownerDocument.execCommand(commandfalsenull);
        if (!
wysihtml5.browser.autoScrollsToCaret()) {
          
wysihtml5.selection.scrollIntoView(element);
        }
      } else {
        
wysihtml5.commands.exec(element"insertHTML"LINE_BREAK);
      }
    },

    
state: function() {
      return 
false;
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef;
  
  
wysihtml5.commands.insertOrderedList = {
    
exec: function(elementcommand) {
      var 
doc element.ownerDocument,
          
selectedNode,
          
isEmpty,
          
tempElement,
          list;

      if (
wysihtml5.commands.support(elementcommand)) {
        
doc.execCommand(commandfalsenull);
      } else {
        
selectedNode wysihtml5.selection.getSelectedNode(doc);
        list = 
wysihtml5.dom.getParentElement(selectedNode, { nodeName: ["UL""OL"] }, 4);
        if (!list) {
          
tempElement doc.createElement("span");
          
wysihtml5.selection.surround(tempElement);
          
isEmpty tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
          
wysihtml5.selection.executeAndRestoreSimple(doc, function() {
            list = 
wysihtml5.dom.convertToList(tempElement"ol");
          });

          if (
isEmpty) {
            
wysihtml5.selection.selectNode(list.querySelector("li"));
          }
          return;
        }

        
wysihtml5.selection.executeAndRestoreSimple(doc, function() {
          if (list.
nodeName === "OL") {
            
// Unwrap list
            // <ol><li>foo</li><li>bar</li></ol>
            // becomes:
            // foo<br>bar<br>
            
wysihtml5.dom.resolveList(list);
          } else if (list.
nodeName === "UL" || list.nodeName === "MENU") {
            
// Turn an unordered list into an ordered list
            // <ul><li>foo</li><li>bar</li></ul>
            // becomes:
            // <ol><li>foo</li><li>bar</li></ol>
            
wysihtml5.dom.renameElement(list, "ol");
          }
        });
      }
    },

    
state: function(elementcommand) {
      try {
        return 
element.ownerDocument.queryCommandState(command);
      } catch(
e) {
        return 
false;
      }
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef;
  
  
wysihtml5.commands.insertUnorderedList = {
    
exec: function(elementcommand) {
      var 
doc element.ownerDocument,
          
selectedNode,
          
isEmpty,
          
tempElement,
          list;

      if (
wysihtml5.commands.support(elementcommand)) {
        
doc.execCommand(commandfalsenull);
      } else {
        
selectedNode wysihtml5.selection.getSelectedNode(doc);
        list = 
wysihtml5.dom.getParentElement(selectedNode, { nodeName: ["UL""OL"] });

        if (!list) {
          
tempElement doc.createElement("span");
          
wysihtml5.selection.surround(tempElement);
          
isEmpty tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
          
wysihtml5.selection.executeAndRestoreSimple(doc, function() {
            list = 
wysihtml5.dom.convertToList(tempElement"ul");
          });

          if (
isEmpty) {
            
wysihtml5.selection.selectNode(list.querySelector("li"));
          }
          return;
        }

        
wysihtml5.selection.executeAndRestoreSimple(doc, function() {
          if (list.
nodeName === "UL") {
            
// Unwrap list
            // <ul><li>foo</li><li>bar</li></ul>
            // becomes:
            // foo<br>bar<br>
            
wysihtml5.dom.resolveList(list);
          } else if (list.
nodeName === "OL" || list.nodeName === "MENU") {
            
// Turn an ordered list into an unordered list
            // <ol><li>foo</li><li>bar</li></ol>
            // becomes:
            // <ul><li>foo</li><li>bar</li></ul>
            
wysihtml5.dom.renameElement(list, "ul");
          }
        });
      }
    },

    
state: function(elementcommand) {
      try {
        return 
element.ownerDocument.queryCommandState(command);
      } catch(
e) {
        return 
false;
      }
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef;
  
  
wysihtml5.commands.italic = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatInline.exec(elementcommand"i");
    },

    
state: function(elementcommandcolor) {
      
// element.ownerDocument.queryCommandState("italic") results:
      // firefox: only <i>
      // chrome:  <i>, <em>, <blockquote>, ...
      // ie:      <i>, <em>
      // opera:   only <i>
      
return wysihtml5.commands.formatInline.state(elementcommand"i");
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
CLASS_NAME  "wysiwyg-text-align-center",
      
REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
  
  
wysihtml5.commands.justifyCenter = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.exec(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
state: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.state(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
CLASS_NAME  "wysiwyg-text-align-left",
      
REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
  
  
wysihtml5.commands.justifyLeft = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.exec(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
state: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.state(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
CLASS_NAME  "wysiwyg-text-align-right",
      
REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
  
  
wysihtml5.commands.justifyRight = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.exec(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
state: function(elementcommand) {
      return 
wysihtml5.commands.formatBlock.state(element"formatBlock"nullCLASS_NAMEREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
undef,
      
REG_EXP     = /wysiwyg-text-decoration-underline/g,
      
CLASS_NAME  "wysiwyg-text-decoration-underline";
  
  
wysihtml5.commands.underline = {
    
exec: function(elementcommand) {
      return 
wysihtml5.commands.formatInline.exec(elementcommand"span"CLASS_NAMEREG_EXP);
    },

    
state: function(elementcommand) {
      return 
wysihtml5.commands.formatInline.state(elementcommand"span"CLASS_NAMEREG_EXP);
    },

    
value: function() {
      return 
undef;
    }
  };
})(
wysihtml5);(function(wysihtml5) {
  var 
Z_KEY     90,
      
Y_KEY     89,
      
UNDO_HTML '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' wysihtml5.INVISIBLE_SPACE '</span>',
      
REDO_HTML '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' wysihtml5.INVISIBLE_SPACE '</span>',
      
dom       wysihtml5.dom;
  
  function 
cleanTempElements(doc) {
    var 
tempElement;
    while (
tempElement doc.querySelector("._wysihtml5-temp")) {
      
tempElement.parentNode.removeChild(tempElement);
    }
  }
  
  
wysihtml5.UndoManager wysihtml5.lang.Dispatcher.extend(
    
/** @scope wysihtml5.UndoManager.prototype */ {
    
constructor: function(editor) {
      
this.editor editor;
      
this.composerElement editor.composer.element;
      
this._observe();
    },
    
    
_observe: function() {
      var 
that this,
          
doc this.editor.composer.sandbox.getDocument();
          
      
// Catch CTRL+Z and CTRL+Y
      
dom.observe(this.composerElement"keydown", function(event) {
        if (!
event.ctrlKey && !event.metaKey) {
          return;
        }
        
        var 
keyCode event.keyCode,
            
isUndo keyCode === Z_KEY && !event.shiftKey,
            
isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
        
        if (
isUndo) {
          
that.undo();
          
event.preventDefault();
        } else if (
isRedo) {
          
that.redo();
          
event.preventDefault();
        }
      });
      
      var 
intervalobservedcleanUp = function() {
        
cleanTempElements(doc);
        
clearInterval(interval);
      };
      
      
      
// Now this is very hacky:
      // These days browsers don't offer a undo/redo event which we could hook into
      // to be notified when the user hits undo/redo in the contextmenu.
      // Therefore we simply insert two elements as soon as the contextmenu gets opened.
      // The last element being inserted will be immediately be removed again by a exexCommand("undo")
      // => Now when the second element appears in the dom tree then we know the user clicked "redo" in the context menu
      // => When the first element disappears in the dom tree then we know the user clicked "undo" in the context menu
      
dom.observe(this.composerElement"contextmenu", function() {
        
cleanUp();
        
wysihtml5.selection.executeAndRestoreSimple(doc, function() {
          if (
that.composerElement.lastChild) {
            
wysihtml5.selection.setAfter(that.composerElement.lastChild);
          }
          
doc.execCommand("insertHTML"falseUNDO_HTML);
          
doc.execCommand("insertHTML"falseREDO_HTML);
          
doc.execCommand("undo"falsenull);
        });
        
        
interval setInterval(function() {
          if (
doc.getElementById("_wysihtml5-redo")) {
            
cleanUp();
            
that.redo();
          } else if (!
doc.getElementById("_wysihtml5-undo")) {
            
cleanUp();
            
that.undo();
          }
        }, 
400);
        
        if (!
observed) {
          
observed true;
          
dom.observe(document"mousedown"cleanUp);
          
dom.observe(doc, ["mousedown""paste""cut""copy"], cleanUp);
        }
      });
      
    },
    
    
undo: function() {
      
this.editor.fire("undo:composer");
    },
    
    
redo: function() {
      
this.editor.fire("redo:composer");
    }
  });
})(
wysihtml5);
/**
 * WYSIHTML5 Editor
 *
 * @author Christopher Blum <christopher.blum@xing.com>
 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface
 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
 *
 * @events
 *    load
 *    beforeload (for internal use only)
 *    focus
 *    focus:composer
 *    focus:textarea
 *    blur
 *    blur:composer
 *    blur:textarea
 *    change
 *    change:composer
 *    change:textarea
 *    paste
 *    paste:composer
 *    paste:textarea
 *    newword:composer
 *    destroy:composer
 *    change_view
 */
(function(wysihtml5) {
  var 
undef;
  
  var 
defaultConfig = {
    
// Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 
    
name:                 undef,
    
// Whether the editor should look like the textarea (by adopting styles)
    
style:                true,
    
// Id of the toolbar element, pass falsey value if you don't want any toolbar logic
    
toolbar:              undef,
    
// Whether urls, entered by the user should automatically become clickable-links
    
autoLink:             true,
    
// Object which includes parser rules to apply when html gets inserted via copy & paste
    // See parser_rules/*.js for examples
    
parserRules:          { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
    
// Parser method to use when the user inserts content via copy & paste
    
parser:               wysihtml5.dom.parse,
    
// Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
    
composerClassName:    "wysihtml5-editor",
    
// Class name to add to the body when the wysihtml5 editor is supported
    
bodyClassName:        "wysihtml5-supported",
    
// Array (or single string) of stylesheet urls to be loaded in the editor's iframe
    
stylesheets:          [],
    
// Placeholder text to use, defaults to the placeholder attribute on the textarea element
    
placeholderText:      undef,
    
// Whether the composer should allow the user to manually resize images, tables etc.
    
allowObjectResizing:  true
  
};
  
  
wysihtml5.Editor wysihtml5.lang.Dispatcher.extend(
    
/** @scope wysihtml5.Editor.prototype */ {
    
constructor: function(textareaElementconfig) {
      
this.textareaElement  typeof(textareaElement) === "string" document.getElementById(textareaElement) : textareaElement;
      
this.config           wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
      
this.textarea         = new wysihtml5.views.Textarea(thisthis.textareaElementthis.config);
      
this.currentView      this.textarea;
      
this._isCompatible    wysihtml5.browser.supported();
      
      
// Sort out unsupported browsers here
      
if (!this._isCompatible) {
        var 
that this;
        
setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
        return;
      }
      
      
// Add class name to body, to indicate that the editor is supported
      
wysihtml5.dom.addClass(document.bodythis.config.bodyClassName);
      
      
this.composer = new wysihtml5.views.Composer(thisthis.textareaElementthis.config);
      
this.currentView this.composer;
      
      if (
typeof(this.config.parser) === "function") {
        
this._initParser();
      }
      
      
this.observe("beforeload", function() {
        
this.synchronizer = new wysihtml5.views.Synchronizer(thisthis.textareathis.composer);
        if (
this.config.toolbar) {
          
this.toolbar = new wysihtml5.toolbar.Toolbar(thisthis.config.toolbar);
        }
      });
      
      try {
        
console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
      } catch(
e) {}
    },
    
    
isCompatible: function() {
      return 
this._isCompatible;
    },

    
clear: function() {
      
this.currentView.clear();
      return 
this;
    },

    
getValue: function(parse) {
      return 
this.currentView.getValue(parse);
    },

    
setValue: function(htmlparse) {
      if (!
html) {
        return 
this.clear();
      }
      
this.currentView.setValue(htmlparse);
      return 
this;
    },

    
focus: function(setToEnd) {
      
this.currentView.focus(setToEnd);
      return 
this;
    },

    
/**
     * Deactivate editor (make it readonly)
     */
    
disable: function() {
      
this.currentView.disable();
      return 
this;
    },
    
    
/**
     * Activate editor
     */
    
enable: function() {
      
this.currentView.enable();
      return 
this;
    },
    
    
isEmpty: function() {
      return 
this.currentView.isEmpty();
    },
    
    
hasPlaceholderSet: function() {
      return 
this.currentView.hasPlaceholderSet();
    },
    
    
parse: function(htmlOrElement) {
      var 
returnValue this.config.parser(htmlOrElementthis.config.parserRulesthis.composer.sandbox.getDocument(), true);
      if (
typeof(htmlOrElement) === "object") {
        
wysihtml5.quirks.redraw(htmlOrElement);
      }
      return 
returnValue;
    },
    
    
/**
     * Prepare html parser logic
     *  - Observes for paste and drop
     */
    
_initParser: function() {
      
this.observe("paste:composer", function() {
        var 
keepScrollPosition  true,
            
that                this;
        
wysihtml5.selection.executeAndRestore(this.composer.sandbox.getDocument(), function() {
          
wysihtml5.quirks.cleanPastedHTML(that.composer.element);
          
that.parse(that.composer.element);
        }, 
keepScrollPosition);
      });
      
      
this.observe("paste:textarea", function() {
        var 
value   this.textarea.getValue(),
            
newValue;
        
newValue this.parse(value);
        
this.textarea.setValue(newValue);
      });
    }
  });
})(
wysihtml5);
?>
Онлайн: 0
Реклама