PageSourceSearch

https://list.vn/libs/bootstrap3-editable-1.5.1/inputs-ext/wysihtml…ootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js

js list.vn collected 2026-09-24 12:41:32 UTC 341,635 bytes, 9,521 lines download raw bytes

1/**
2 * @license wysihtml5 v0.3.0
3 * https://github.com/xing/wysihtml5
4 *
5 * Author: Christopher Blum (https://github.com/tiff)
6 *
7 * Copyright (C) 2012 XING AG
8 * Licensed under the MIT license (MIT)
9 *
10 */
11var wysihtml5 = {
12  version: "0.3.0",
13  
14  // namespaces
15  commands:   {},
16  dom:        {},
17  quirks:     {},
18  toolbar:    {},
19  lang:       {},
20  selection:  {},
21  views:      {},
22  
23  INVISIBLE_SPACE: "\uFEFF",
24  
25  EMPTY_FUNCTION: function() {},
26  
27  ELEMENT_NODE: 1,
28  TEXT_NODE:    3,
29  
30  BACKSPACE_KEY:  8,
31  ENTER_KEY:      13,
32  ESCAPE_KEY:     27,
33  SPACE_KEY:      32,
34  DELETE_KEY:     46
35};/**
36 * @license Rangy, a cross-browser JavaScript range and selection library
37 * http://code.google.com/p/rangy/
38 *
39 * Copyright 2011, Tim Down
40 * Licensed under the MIT license.
41 * Version: 1.2.2
42 * Build date: 13 November 2011
43 */
44window['rangy'] = (function() {
45
46
47    var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
48
49    var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
50        "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"];
51
52    var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
53        "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
54        "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
55
56    var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
57
58    // Subset of TextRange's full set of methods that we're interested in
59    var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark",
60        "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"];
61
62    /*----------------------------------------------------------------------------------------------------------------*/
63
64    // Trio of functions taken from Peter Michaux's article:
65    // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
66    function isHostMethod(o, p) {
67        var t = typeof o[p];
68        return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
69    }
70
71    function isHostObject(o, p) {
72        return !!(typeof o[p] == OBJECT && o[p]);
73    }
74
75    function isHostProperty(o, p) {
76        return typeof o[p] != UNDEFINED;
77    }
78
79    // Creates a convenience function to save verbose repeated calls to tests functions
80    function createMultiplePropertyTest(testFunc) {
81        return function(o, props) {
82            var i = props.length;
83            while (i--) {
84                if (!testFunc(o, props[i])) {
85                    return false;
86                }
87            }
88            return true;
89        };
90    }
91
92    // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
93    var areHostMethods = createMultiplePropertyTest(isHostMethod);
94    var areHostObjects = createMultiplePropertyTest(isHostObject);
95    var areHostProperties = createMultiplePropertyTest(isHostProperty);
96
97    function isTextRange(range) {
98        return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
99    }
100
101    var api = {
102        version: "1.2.2",
103        initialized: false,
104        supported: true,
105
106        util: {
107            isHostMethod: isHostMethod,
108            isHostObject: isHostObject,
109            isHostProperty: isHostProperty,
110            areHostMethods: areHostMethods,
111            areHostObjects: areHostObjects,
112            areHostProperties: areHostProperties,
113            isTextRange: isTextRange
114        },
115
116        features: {},
117
118        modules: {},
119        config: {
120            alertOnWarn: false,
121            preferTextRange: false
122        }
123    };
124
125    function fail(reason) {
126        window.alert("Rangy not supported in your browser. Reason: " + reason);
127        api.initialized = true;
128        api.supported = false;
129    }
130
131    api.fail = fail;
132
133    function warn(msg) {
134        var warningMessage = "Rangy warning: " + msg;
135        if (api.config.alertOnWarn) {
136            window.alert(warningMessage);
137        } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) {
138            window.console.log(warningMessage);
139        }
140    }
141
142    api.warn = warn;
143
144    if ({}.hasOwnProperty) {
145        api.util.extend = function(o, props) {
146            for (var i in props) {
147                if (props.hasOwnProperty(i)) {
148                    o[i] = props[i];
149                }
150            }
151        };
152    } else {
153        fail("hasOwnProperty not supported");
154    }
155
156    var initListeners = [];
157    var moduleInitializers = [];
158
159    // Initialization
160    function init() {
161        if (api.initialized) {
162            return;
163        }
164        var testRange;
165        var implementsDomRange = false, implementsTextRange = false;
166
167        // First, perform basic feature tests
168
169        if (isHostMethod(document, "createRange")) {
170            testRange = document.createRange();
171            if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
172                implementsDomRange = true;
173            }
174            testRange.detach();
175        }
176
177        var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
178
179        if (body && isHostMethod(body, "createTextRange")) {
180            testRange = body.createTextRange();
181            if (isTextRange(testRange)) {
182                implementsTextRange = true;
183            }
184        }
185
186        if (!implementsDomRange && !implementsTextRange) {
187            fail("Neither Range nor TextRange are implemented");
188        }
189
190        api.initialized = true;
191        api.features = {
192            implementsDomRange: implementsDomRange,
193            implementsTextRange: implementsTextRange
194        };
195
196        // Initialize modules and call init listeners
197        var allListeners = moduleInitializers.concat(initListeners);
198        for (var i = 0, len = allListeners.length; i < len; ++i) {
199            try {
200                allListeners[i](api);
201            } catch (ex) {
202                if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
203                    window.console.log("Init listener threw an exception. Continuing.", ex);
204                }
205
206            }
207        }
208    }
209
210    // Allow external scripts to initialize this library in case it's loaded after the document has loaded
211    api.init = init;
212
213    // Execute listener immediately if already initialized
214    api.addInitListener = function(listener) {
215        if (api.initialized) {
216            listener(api);
217        } else {
218            initListeners.push(listener);
219        }
220    };
221
222    var createMissingNativeApiListeners = [];
223
224    api.addCreateMissingNativeApiListener = function(listener) {
225        createMissingNativeApiListeners.push(listener);
226    };
227
228    function createMissingNativeApi(win) {
229        win = win || window;
230        init();
231
232        // Notify listeners
233        for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) {
234            createMissingNativeApiListeners[i](win);
235        }
236    }
237
238    api.createMissingNativeApi = createMissingNativeApi;
239
240    /**
241     * @constructor
242     */
243    function Module(name) {
244        this.name = name;
245        this.initialized = false;
246        this.supported = false;
247    }
248
249    Module.prototype.fail = function(reason) {
250        this.initialized = true;
251        this.supported = false;
252
253        throw new Error("Module '" + this.name + "' failed to load: " + reason);
254    };
255
256    Module.prototype.warn = function(msg) {
257        api.warn("Module " + this.name + ": " + msg);
258    };
259
260    Module.prototype.createError = function(msg) {
261        return new Error("Error in Rangy " + this.name + " module: " + msg);
262    };
263
264    api.createModule = function(name, initFunc) {
265        var module = new Module(name);
266        api.modules[name] = module;
267
268        moduleInitializers.push(function(api) {
269            initFunc(api, module);
270            module.initialized = true;
271            module.supported = true;
272        });
273    };
274
275    api.requireModules = function(modules) {
276        for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) {
277            moduleName = modules[i];
278            module = api.modules[moduleName];
279            if (!module || !(module instanceof Module)) {
280                throw new Error("Module '" + moduleName + "' not found");
281            }
282            if (!module.supported) {
283                throw new Error("Module '" + moduleName + "' not supported");
284            }
285        }
286    };
287
288    /*----------------------------------------------------------------------------------------------------------------*/
289
290    // Wait for document to load before running tests
291
292    var docReady = false;
293
294    var loadHandler = function(e) {
295
296        if (!docReady) {
297            docReady = true;
298            if (!api.initialized) {
299                init();
300            }
301        }
302    };
303
304    // Test whether we have window and document objects that we will need
305    if (typeof window == UNDEFINED) {
306        fail("No window found");
307        return;
308    }
309    if (typeof document == UNDEFINED) {
310        fail("No document found");
311        return;
312    }
313
314    if (isHostMethod(document, "addEventListener")) {
315        document.addEventListener("DOMContentLoaded", loadHandler, false);
316    }
317
318    // Add a fallback in case the DOMContentLoaded event isn't supported
319    if (isHostMethod(window, "addEventListener")) {
320        window.addEventListener("load", loadHandler, false);
321    } else if (isHostMethod(window, "attachEvent")) {
322        window.attachEvent("onload", loadHandler);
323    } else {
324        fail("Window does not have required addEventListener or attachEvent method");
325    }
326
327    return api;
328})();
329rangy.createModule("DomUtil", function(api, module) {
330
331    var UNDEF = "undefined";
332    var util = api.util;
333
334    // Perform feature tests
335    if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
336        module.fail("document missing a Node creation method");
337    }
338
339    if (!util.isHostMethod(document, "getElementsByTagName")) {
340        module.fail("document missing getElementsByTagName method");
341    }
342
343    var el = document.createElement("div");
344    if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
345            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
346        module.fail("Incomplete Element implementation");
347    }
348
349    // innerHTML is required for Range's createContextualFragment method
350    if (!util.isHostProperty(el, "innerHTML")) {
351        module.fail("Element is missing innerHTML property");
352    }
353
354    var textNode = document.createTextNode("test");
355    if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
356            !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
357            !util.areHostProperties(textNode, ["data"]))) {
358        module.fail("Incomplete Text Node implementation");
359    }
360
361    /*----------------------------------------------------------------------------------------------------------------*/
362
363    // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
364    // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
365    // contains just the document as a single element and the value searched for is the document.
366    var arrayContains = /*Array.prototype.indexOf ?
367        function(arr, val) {
368            return arr.indexOf(val) > -1;
369        }:*/
370
371        function(arr, val) {
372            var i = arr.length;
373            while (i--) {
374                if (arr[i] === val) {
375                    return true;
376                }
377            }
378            return false;
379        };
380
381    // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
382    function isHtmlNamespace(node) {
383        var ns;
384        return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
385    }
386
387    function parentElement(node) {
388        var parent = node.parentNode;
389        return (parent.nodeType == 1) ? parent : null;
390    }
391
392    function getNodeIndex(node) {
393        var i = 0;
394        while( (node = node.previousSibling) ) {
395            i++;
396        }
397        return i;
398    }
399
400    function getNodeLength(node) {
401        var childNodes;
402        return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0);
403    }
404
405    function getCommonAncestor(node1, node2) {
406        var ancestors = [], n;
407        for (n = node1; n; n = n.parentNode) {
408            ancestors.push(n);
409        }
410
411        for (n = node2; n; n = n.parentNode) {
412            if (arrayContains(ancestors, n)) {
413                return n;
414            }
415        }
416
417        return null;
418    }
419
420    function isAncestorOf(ancestor, descendant, selfIsAncestor) {
421        var n = selfIsAncestor ? descendant : descendant.parentNode;
422        while (n) {
423            if (n === ancestor) {
424                return true;
425            } else {
426                n = n.parentNode;
427            }
428        }
429        return false;
430    }
431
432    function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
433        var p, n = selfIsAncestor ? node : node.parentNode;
434        while (n) {
435            p = n.parentNode;
436            if (p === ancestor) {
437                return n;
438            }
439            n = p;
440        }
441        return null;
442    }
443
444    function isCharacterDataNode(node) {
445        var t = node.nodeType;
446        return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
447    }
448
449    function insertAfter(node, precedingNode) {
450        var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
451        if (nextNode) {
452            parent.insertBefore(node, nextNode);
453        } else {
454            parent.appendChild(node);
455        }
456        return node;
457    }
458
459    // Note that we cannot use splitText() because it is bugridden in IE 9.
460    function splitDataNode(node, index) {
461        var newNode = node.cloneNode(false);
462        newNode.deleteData(0, index);
463        node.deleteData(index, node.length - index);
464        insertAfter(newNode, node);
465        return newNode;
466    }
467
468    function getDocument(node) {
469        if (node.nodeType == 9) {
470            return node;
471        } else if (typeof node.ownerDocument != UNDEF) {
472            return node.ownerDocument;
473        } else if (typeof node.document != UNDEF) {
474            return node.document;
475        } else if (node.parentNode) {
476            return getDocument(node.parentNode);
477        } else {
478            throw new Error("getDocument: no document found for node");
479        }
480    }
481
482    function getWindow(node) {
483        var doc = getDocument(node);
484        if (typeof doc.defaultView != UNDEF) {
485            return doc.defaultView;
486        } else if (typeof doc.parentWindow != UNDEF) {
487            return doc.parentWindow;
488        } else {
489            throw new Error("Cannot get a window object for node");
490        }
491    }
492
493    function getIframeDocument(iframeEl) {
494        if (typeof iframeEl.contentDocument != UNDEF) {
495            return iframeEl.contentDocument;
496        } else if (typeof iframeEl.contentWindow != UNDEF) {
497            return iframeEl.contentWindow.document;
498        } else {
499            throw new Error("getIframeWindow: No Document object found for iframe element");
500        }
501    }
502
503    function getIframeWindow(iframeEl) {
504        if (typeof iframeEl.contentWindow != UNDEF) {
505            return iframeEl.contentWindow;
506        } else if (typeof iframeEl.contentDocument != UNDEF) {
507            return iframeEl.contentDocument.defaultView;
508        } else {
509            throw new Error("getIframeWindow: No Window object found for iframe element");
510        }
511    }
512
513    function getBody(doc) {
514        return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
515    }
516
517    function getRootContainer(node) {
518        var parent;
519        while ( (parent = node.parentNode) ) {
520            node = parent;
521        }
522        return node;
523    }
524
525    function comparePoints(nodeA, offsetA, nodeB, offsetB) {
526        // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
527        var nodeC, root, childA, childB, n;
528        if (nodeA == nodeB) {
529
530            // Case 1: nodes are the same
531            return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
532        } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
533
534            // Case 2: node C (container B or an ancestor) is a child node of A
535            return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
536        } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
537
538            // Case 3: node C (container A or an ancestor) is a child node of B
539            return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
540        } else {
541
542            // Case 4: containers are siblings or descendants of siblings
543            root = getCommonAncestor(nodeA, nodeB);
544            childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
545            childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
546
547            if (childA === childB) {
548                // This shouldn't be possible
549
550                throw new Error("comparePoints got to case 4 and childA and childB are the same!");
551            } else {
552                n = root.firstChild;
553                while (n) {
554                    if (n === childA) {
555                        return -1;
556                    } else if (n === childB) {
557                        return 1;
558                    }
559                    n = n.nextSibling;
560                }
561                throw new Error("Should not be here!");
562            }
563        }
564    }
565
566    function fragmentFromNodeChildren(node) {
567        var fragment = getDocument(node).createDocumentFragment(), child;
568        while ( (child = node.firstChild) ) {
569            fragment.appendChild(child);
570        }
571        return fragment;
572    }
573
574    function inspectNode(node) {
575        if (!node) {
576            return "[No node]";
577        }
578        if (isCharacterDataNode(node)) {
579            return '"' + node.data + '"';
580        } else if (node.nodeType == 1) {
581            var idAttr = node.id ? ' id="' + node.id + '"' : "";
582            return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]";
583        } else {
584            return node.nodeName;
585        }
586    }
587
588    /**
589     * @constructor
590     */
591    function NodeIterator(root) {
592        this.root = root;
593        this._next = root;
594    }
595
596    NodeIterator.prototype = {
597        _current: null,
598
599        hasNext: function() {
600            return !!this._next;
601        },
602
603        next: function() {
604            var n = this._current = this._next;
605            var child, next;
606            if (this._current) {
607                child = n.firstChild;
608                if (child) {
609                    this._next = child;
610                } else {
611                    next = null;
612                    while ((n !== this.root) && !(next = n.nextSibling)) {
613                        n = n.parentNode;
614                    }
615                    this._next = next;
616                }
617            }
618            return this._current;
619        },
620
621        detach: function() {
622            this._current = this._next = this.root = null;
623        }
624    };
625
626    function createIterator(root) {
627        return new NodeIterator(root);
628    }
629
630    /**
631     * @constructor
632     */
633    function DomPosition(node, offset) {
634        this.node = node;
635        this.offset = offset;
636    }
637
638    DomPosition.prototype = {
639        equals: function(pos) {
640            return this.node === pos.node & this.offset == pos.offset;
641        },
642
643        inspect: function() {
644            return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
645        }
646    };
647
648    /**
649     * @constructor
650     */
651    function DOMException(codeName) {
652        this.code = this[codeName];
653        this.codeName = codeName;
654        this.message = "DOMException: " + this.codeName;
655    }
656
657    DOMException.prototype = {
658        INDEX_SIZE_ERR: 1,
659        HIERARCHY_REQUEST_ERR: 3,
660        WRONG_DOCUMENT_ERR: 4,
661        NO_MODIFICATION_ALLOWED_ERR: 7,
662        NOT_FOUND_ERR: 8,
663        NOT_SUPPORTED_ERR: 9,
664        INVALID_STATE_ERR: 11
665    };
666
667    DOMException.prototype.toString = function() {
668        return this.message;
669    };
670
671    api.dom = {
672        arrayContains: arrayContains,
673        isHtmlNamespace: isHtmlNamespace,
674        parentElement: parentElement,
675        getNodeIndex: getNodeIndex,
676        getNodeLength: getNodeLength,
677        getCommonAncestor: getCommonAncestor,
678        isAncestorOf: isAncestorOf,
679        getClosestAncestorIn: getClosestAncestorIn,
680        isCharacterDataNode: isCharacterDataNode,
681        insertAfter: insertAfter,
682        splitDataNode: splitDataNode,
683        getDocument: getDocument,
684        getWindow: getWindow,
685        getIframeWindow: getIframeWindow,
686        getIframeDocument: getIframeDocument,
687        getBody: getBody,
688        getRootContainer: getRootContainer,
689        comparePoints: comparePoints,
690        inspectNode: inspectNode,
691        fragmentFromNodeChildren: fragmentFromNodeChildren,
692        createIterator: createIterator,
693        DomPosition: DomPosition
694    };
695
696    api.DOMException = DOMException;
697});rangy.createModule("DomRange", function(api, module) {
698    api.requireModules( ["DomUtil"] );
699
700
701    var dom = api.dom;
702    var DomPosition = dom.DomPosition;
703    var DOMException = api.DOMException;
704    
705    /*----------------------------------------------------------------------------------------------------------------*/
706
707    // Utility functions
708
709    function isNonTextPartiallySelected(node, range) {
710        return (node.nodeType != 3) &&
711               (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true));
712    }
713
714    function getRangeDocument(range) {
715        return dom.getDocument(range.startContainer);
716    }
717
718    function dispatchEvent(range, type, args) {
719        var listeners = range._listeners[type];
720        if (listeners) {
721            for (var i = 0, len = listeners.length; i < len; ++i) {
722                listeners[i].call(range, {target: range, args: args});
723            }
724        }
725    }
726
727    function getBoundaryBeforeNode(node) {
728        return new DomPosition(node.parentNode, dom.getNodeIndex(node));
729    }
730
731    function getBoundaryAfterNode(node) {
732        return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1);
733    }
734
735    function insertNodeAtPosition(node, n, o) {
736        var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
737        if (dom.isCharacterDataNode(n)) {
738            if (o == n.length) {
739                dom.insertAfter(node, n);
740            } else {
741                n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o));
742            }
743        } else if (o >= n.childNodes.length) {
744            n.appendChild(node);
745        } else {
746            n.insertBefore(node, n.childNodes[o]);
747        }
748        return firstNodeInserted;
749    }
750
751    function cloneSubtree(iterator) {
752        var partiallySelected;
753        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
754            partiallySelected = iterator.isPartiallySelectedSubtree();
755
756            node = node.cloneNode(!partiallySelected);
757            if (partiallySelected) {
758                subIterator = iterator.getSubtreeIterator();
759                node.appendChild(cloneSubtree(subIterator));
760                subIterator.detach(true);
761            }
762
763            if (node.nodeType == 10) { // DocumentType
764                throw new DOMException("HIERARCHY_REQUEST_ERR");
765            }
766            frag.appendChild(node);
767        }
768        return frag;
769    }
770
771    function iterateSubtree(rangeIterator, func, iteratorState) {
772        var it, n;
773        iteratorState = iteratorState || { stop: false };
774        for (var node, subRangeIterator; node = rangeIterator.next(); ) {
775            //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node));
776            if (rangeIterator.isPartiallySelectedSubtree()) {
777                // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the
778                // node selected by the Range.
779                if (func(node) === false) {
780                    iteratorState.stop = true;
781                    return;
782                } else {
783                    subRangeIterator = rangeIterator.getSubtreeIterator();
784                    iterateSubtree(subRangeIterator, func, iteratorState);
785                    subRangeIterator.detach(true);
786                    if (iteratorState.stop) {
787                        return;
788                    }
789                }
790            } else {
791                // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
792                // descendant
793                it = dom.createIterator(node);
794                while ( (n = it.next()) ) {
795                    if (func(n) === false) {
796                        iteratorState.stop = true;
797                        return;
798                    }
799                }
800            }
801        }
802    }
803
804    function deleteSubtree(iterator) {
805        var subIterator;
806        while (iterator.next()) {
807            if (iterator.isPartiallySelectedSubtree()) {
808                subIterator = iterator.getSubtreeIterator();
809                deleteSubtree(subIterator);
810                subIterator.detach(true);
811            } else {
812                iterator.remove();
813            }
814        }
815    }
816
817    function extractSubtree(iterator) {
818
819        for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
820
821
822            if (iterator.isPartiallySelectedSubtree()) {
823                node = node.cloneNode(false);
824                subIterator = iterator.getSubtreeIterator();
825                node.appendChild(extractSubtree(subIterator));
826                subIterator.detach(true);
827            } else {
828                iterator.remove();
829            }
830            if (node.nodeType == 10) { // DocumentType
831                throw new DOMException("HIERARCHY_REQUEST_ERR");
832            }
833            frag.appendChild(node);
834        }
835        return frag;
836    }
837
838    function getNodesInRange(range, nodeTypes, filter) {
839        //log.info("getNodesInRange, " + nodeTypes.join(","));
840        var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
841        var filterExists = !!filter;
842        if (filterNodeTypes) {
843            regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
844        }
845
846        var nodes = [];
847        iterateSubtree(new RangeIterator(range, false), function(node) {
848            if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) {
849                nodes.push(node);
850            }
851        });
852        return nodes;
853    }
854
855    function inspect(range) {
856        var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
857        return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
858                dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
859    }
860
861    /*----------------------------------------------------------------------------------------------------------------*/
862
863    // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
864
865    /**
866     * @constructor
867     */
868    function RangeIterator(range, clonePartiallySelectedTextNodes) {
869        this.range = range;
870        this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
871
872
873
874        if (!range.collapsed) {
875            this.sc = range.startContainer;
876            this.so = range.startOffset;
877            this.ec = range.endContainer;
878            this.eo = range.endOffset;
879            var root = range.commonAncestorContainer;
880
881            if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) {
882                this.isSingleCharacterDataNode = true;
883                this._first = this._last = this._next = this.sc;
884            } else {
885                this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ?
886                    this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true);
887                this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ?
888                    this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true);
889            }
890
891        }
892    }
893
894    RangeIterator.prototype = {
895        _current: null,
896        _next: null,
897        _first: null,
898        _last: null,
899        isSingleCharacterDataNode: false,
900
901        reset: function() {
902            this._current = null;
903            this._next = this._first;
904        },
905
906        hasNext: function() {
907            return !!this._next;
908        },
909
910        next: function() {
911            // Move to next node
912            var current = this._current = this._next;
913            if (current) {
914                this._next = (current !== this._last) ? current.nextSibling : null;
915
916                // Check for partially selected text nodes
917                if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
918                    if (current === this.ec) {
919
920                        (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
921                    }
922                    if (this._current === this.sc) {
923
924                        (current = current.cloneNode(true)).deleteData(0, this.so);
925                    }
926                }
927            }
928
929            return current;
930        },
931
932        remove: function() {
933            var current = this._current, start, end;
934
935            if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
936                start = (current === this.sc) ? this.so : 0;
937                end = (current === this.ec) ? this.eo : current.length;
938                if (start != end) {
939                    current.deleteData(start, end - start);
940                }
941            } else {
942                if (current.parentNode) {
943                    current.parentNode.removeChild(current);
944                } else {
945
946                }
947            }
948        },
949
950        // Checks if the current node is partially selected
951        isPartiallySelectedSubtree: function() {
952            var current = this._current;
953            return isNonTextPartiallySelected(current, this.range);
954        },
955
956        getSubtreeIterator: function() {
957            var subRange;
958            if (this.isSingleCharacterDataNode) {
959                subRange = this.range.cloneRange();
960                subRange.collapse();
961            } else {
962                subRange = new Range(getRangeDocument(this.range));
963                var current = this._current;
964                var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current);
965
966                if (dom.isAncestorOf(current, this.sc, true)) {
967                    startContainer = this.sc;
968                    startOffset = this.so;
969                }
970                if (dom.isAncestorOf(current, this.ec, true)) {
971                    endContainer = this.ec;
972                    endOffset = this.eo;
973                }
974
975                updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
976            }
977            return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
978        },
979
980        detach: function(detachRange) {
981            if (detachRange) {
982                this.range.detach();
983            }
984            this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
985        }
986    };
987
988    /*----------------------------------------------------------------------------------------------------------------*/
989
990    // Exceptions
991
992    /**
993     * @constructor
994     */
995    function RangeException(codeName) {
996        this.code = this[codeName];
997        this.codeName = codeName;
998        this.message = "RangeException: " + this.codeName;
999    }
1000
1001    RangeException.prototype = {
1002        BAD_BOUNDARYPOINTS_ERR: 1,
1003        INVALID_NODE_TYPE_ERR: 2
1004    };
1005
1006    RangeException.prototype.toString = function() {
1007        return this.message;
1008    };
1009
1010    /*----------------------------------------------------------------------------------------------------------------*/
1011
1012    /**
1013     * Currently iterates through all nodes in the range on creation until I think of a decent way to do it
1014     * TODO: Look into making this a proper iterator, not requiring preloading everything first
1015     * @constructor
1016     */
1017    function RangeNodeIterator(range, nodeTypes, filter) {
1018        this.nodes = getNodesInRange(range, nodeTypes, filter);
1019        this._next = this.nodes[0];
1020        this._position = 0;
1021    }
1022
1023    RangeNodeIterator.prototype = {
1024        _current: null,
1025
1026        hasNext: function() {
1027            return !!this._next;
1028        },
1029
1030        next: function() {
1031            this._current = this._next;
1032            this._next = this.nodes[ ++this._position ];
1033            return this._current;
1034        },
1035
1036        detach: function() {
1037            this._current = this._next = this.nodes = null;
1038        }
1039    };
1040
1041    var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1042    var rootContainerNodeTypes = [2, 9, 11];
1043    var readonlyNodeTypes = [5, 6, 10, 12];
1044    var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1045    var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1046
1047    function createAncestorFinder(nodeTypes) {
1048        return function(node, selfIsAncestor) {
1049            var t, n = selfIsAncestor ? node : node.parentNode;
1050            while (n) {
1051                t = n.nodeType;
1052                if (dom.arrayContains(nodeTypes, t)) {
1053                    return n;
1054                }
1055                n = n.parentNode;
1056            }
1057            return null;
1058        };
1059    }
1060
1061    var getRootContainer = dom.getRootContainer;
1062    var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1063    var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1064    var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1065
1066    function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1067        if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1068            throw new RangeException("INVALID_NODE_TYPE_ERR");
1069        }
1070    }
1071
1072    function assertNotDetached(range) {
1073        if (!range.startContainer) {
1074            throw new DOMException("INVALID_STATE_ERR");
1075        }
1076    }
1077
1078    function assertValidNodeType(node, invalidTypes) {
1079        if (!dom.arrayContains(invalidTypes, node.nodeType)) {
1080            throw new RangeException("INVALID_NODE_TYPE_ERR");
1081        }
1082    }
1083
1084    function assertValidOffset(node, offset) {
1085        if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1086            throw new DOMException("INDEX_SIZE_ERR");
1087        }
1088    }
1089
1090    function assertSameDocumentOrFragment(node1, node2) {
1091        if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1092            throw new DOMException("WRONG_DOCUMENT_ERR");
1093        }
1094    }
1095
1096    function assertNodeNotReadOnly(node) {
1097        if (getReadonlyAncestor(node, true)) {
1098            throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1099        }
1100    }
1101
1102    function assertNode(node, codeName) {
1103        if (!node) {
1104            throw new DOMException(codeName);
1105        }
1106    }
1107
1108    function isOrphan(node) {
1109        return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1110    }
1111
1112    function isValidOffset(node, offset) {
1113        return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length);
1114    }
1115
1116    function assertRangeValid(range) {
1117        assertNotDetached(range);
1118        if (isOrphan(range.startContainer) || isOrphan(range.endContainer) ||
1119                !isValidOffset(range.startContainer, range.startOffset) ||
1120                !isValidOffset(range.endContainer, range.endOffset)) {
1121            throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1122        }
1123    }
1124
1125    /*----------------------------------------------------------------------------------------------------------------*/
1126
1127    // Test the browser's innerHTML support to decide how to implement createContextualFragment
1128    var styleEl = document.createElement("style");
1129    var htmlParsingConforms = false;
1130    try {
1131        styleEl.innerHTML = "<b>x</b>";
1132        htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1133    } catch (e) {
1134        // IE 6 and 7 throw
1135    }
1136
1137    api.features.htmlParsingConforms = htmlParsingConforms;
1138
1139    var createContextualFragment = htmlParsingConforms ?
1140
1141        // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1142        // discussion and base code for this implementation at issue 67.
1143        // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1144        // Thanks to Aleks Williams.
1145        function(fragmentStr) {
1146            // "Let node the context object's start's node."
1147            var node = this.startContainer;
1148            var doc = dom.getDocument(node);
1149
1150            // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1151            // exception and abort these steps."
1152            if (!node) {
1153                throw new DOMException("INVALID_STATE_ERR");
1154            }
1155
1156            // "Let element be as follows, depending on node's interface:"
1157            // Document, Document Fragment: null
1158            var el = null;
1159
1160            // "Element: node"
1161            if (node.nodeType == 1) {
1162                el = node;
1163
1164            // "Text, Comment: node's parentElement"
1165            } else if (dom.isCharacterDataNode(node)) {
1166                el = dom.parentElement(node);
1167            }
1168
1169            // "If either element is null or element's ownerDocument is an HTML document
1170            // and element's local name is "html" and element's namespace is the HTML
1171            // namespace"
1172            if (el === null || (
1173                el.nodeName == "HTML"
1174                && dom.isHtmlNamespace(dom.getDocument(el).documentElement)
1175                && dom.isHtmlNamespace(el)
1176            )) {
1177
1178            // "let element be a new Element with "body" as its local name and the HTML
1179            // namespace as its namespace.""
1180                el = doc.createElement("body");
1181            } else {
1182                el = el.cloneNode(false);
1183            }
1184
1185            // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1186            // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1187            // "In either case, the algorithm must be invoked with fragment as the input
1188            // and element as the context element."
1189            el.innerHTML = fragmentStr;
1190
1191            // "If this raises an exception, then abort these steps. Otherwise, let new
1192            // children be the nodes returned."
1193
1194            // "Let fragment be a new DocumentFragment."
1195            // "Append all new children to fragment."
1196            // "Return fragment."
1197            return dom.fragmentFromNodeChildren(el);
1198        } :
1199
1200        // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1201        // previous versions of Rangy used (with the exception of using a body element rather than a div)
1202        function(fragmentStr) {
1203            assertNotDetached(this);
1204            var doc = getRangeDocument(this);
1205            var el = doc.createElement("body");
1206            el.innerHTML = fragmentStr;
1207
1208            return dom.fragmentFromNodeChildren(el);
1209        };
1210
1211    /*----------------------------------------------------------------------------------------------------------------*/
1212
1213    var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1214        "commonAncestorContainer"];
1215
1216    var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1217    var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1218
1219    function RangePrototype() {}
1220
1221    RangePrototype.prototype = {
1222        attachListener: function(type, listener) {
1223            this._listeners[type].push(listener);
1224        },
1225
1226        compareBoundaryPoints: function(how, range) {
1227            assertRangeValid(this);
1228            assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1229
1230            var nodeA, offsetA, nodeB, offsetB;
1231            var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1232            var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1233            nodeA = this[prefixA + "Container"];
1234            offsetA = this[prefixA + "Offset"];
1235            nodeB = range[prefixB + "Container"];
1236            offsetB = range[prefixB + "Offset"];
1237            return dom.comparePoints(nodeA, offsetA, nodeB, offsetB);
1238        },
1239
1240        insertNode: function(node) {
1241            assertRangeValid(this);
1242            assertValidNodeType(node, insertableNodeTypes);
1243            assertNodeNotReadOnly(this.startContainer);
1244
1245            if (dom.isAncestorOf(node, this.startContainer, true)) {
1246                throw new DOMException("HIERARCHY_REQUEST_ERR");
1247            }
1248
1249            // No check for whether the container of the start of the Range is of a type that does not allow
1250            // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1251            // to add the node
1252
1253            var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1254            this.setStartBefore(firstNodeInserted);
1255        },
1256
1257        cloneContents: function() {
1258            assertRangeValid(this);
1259
1260            var clone, frag;
1261            if (this.collapsed) {
1262                return getRangeDocument(this).createDocumentFragment();
1263            } else {
1264                if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) {
1265                    clone = this.startContainer.cloneNode(true);
1266                    clone.data = clone.data.slice(this.startOffset, this.endOffset);
1267                    frag = getRangeDocument(this).createDocumentFragment();
1268                    frag.appendChild(clone);
1269                    return frag;
1270                } else {
1271                    var iterator = new RangeIterator(this, true);
1272                    clone = cloneSubtree(iterator);
1273                    iterator.detach();
1274                }
1275                return clone;
1276            }
1277        },
1278
1279        canSurroundContents: function() {
1280            assertRangeValid(this);
1281            assertNodeNotReadOnly(this.startContainer);
1282            assertNodeNotReadOnly(this.endContainer);
1283
1284            // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1285            // no non-text nodes.
1286            var iterator = new RangeIterator(this, true);
1287            var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1288                    (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1289            iterator.detach();
1290            return !boundariesInvalid;
1291        },
1292
1293        surroundContents: function(node) {
1294            assertValidNodeType(node, surroundNodeTypes);
1295
1296            if (!this.canSurroundContents()) {
1297                throw new RangeException("BAD_BOUNDARYPOINTS_ERR");
1298            }
1299
1300            // Extract the contents
1301            var content = this.extractContents();
1302
1303            // Clear the children of the node
1304            if (node.hasChildNodes()) {
1305                while (node.lastChild) {
1306                    node.removeChild(node.lastChild);
1307                }
1308            }
1309
1310            // Insert the new node and add the extracted contents
1311            insertNodeAtPosition(node, this.startContainer, this.startOffset);
1312            node.appendChild(content);
1313
1314            this.selectNode(node);
1315        },
1316
1317        cloneRange: function() {
1318            assertRangeValid(this);
1319            var range = new Range(getRangeDocument(this));
1320            var i = rangeProperties.length, prop;
1321            while (i--) {
1322                prop = rangeProperties[i];
1323                range[prop] = this[prop];
1324            }
1325            return range;
1326        },
1327
1328        toString: function() {
1329            assertRangeValid(this);
1330            var sc = this.startContainer;
1331            if (sc === this.endContainer && dom.isCharacterDataNode(sc)) {
1332                return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) 
1332: "";
1333            } else {
1334                var textBits = [], iterator = new RangeIterator(this, true);
1335
1336                iterateSubtree(iterator, function(node) {
1337                    // Accept only text or CDATA nodes, not comments
1338
1339                    if (node.nodeType == 3 || node.nodeType == 4) {
1340                        textBits.push(node.data);
1341                    }
1342                });
1343                iterator.detach();
1344                return textBits.join("");
1345            }
1346        },
1347
1348        // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1349        // been removed from Mozilla.
1350
1351        compareNode: function(node) {
1352            assertRangeValid(this);
1353
1354            var parent = node.parentNode;
1355            var nodeIndex = dom.getNodeIndex(node);
1356
1357            if (!parent) {
1358                throw new DOMException("NOT_FOUND_ERR");
1359            }
1360
1361            var startComparison = this.comparePoint(parent, nodeIndex),
1362                endComparison = this.comparePoint(parent, nodeIndex + 1);
1363
1364            if (startComparison < 0) { // Node starts before
1365                return (endComparison > 0) ? n_b_a : n_b;
1366            } else {
1367                return (endComparison > 0) ? n_a : n_i;
1368            }
1369        },
1370
1371        comparePoint: function(node, offset) {
1372            assertRangeValid(this);
1373            assertNode(node, "HIERARCHY_REQUEST_ERR");
1374            assertSameDocumentOrFragment(node, this.startContainer);
1375
1376            if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1377                return -1;
1378            } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1379                return 1;
1380            }
1381            return 0;
1382        },
1383
1384        createContextualFragment: createContextualFragment,
1385
1386        toHtml: function() {
1387            assertRangeValid(this);
1388            var container = getRangeDocument(this).createElement("div");
1389            container.appendChild(this.cloneContents());
1390            return container.innerHTML;
1391        },
1392
1393        // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1394        // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1395        intersectsNode: function(node, touchingIsIntersecting) {
1396            assertRangeValid(this);
1397            assertNode(node, "NOT_FOUND_ERR");
1398            if (dom.getDocument(node) !== getRangeDocument(this)) {
1399                return false;
1400            }
1401
1402            var parent = node.parentNode, offset = dom.getNodeIndex(node);
1403            assertNode(parent, "NOT_FOUND_ERR");
1404
1405            var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset),
1406                endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1407
1408            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1409        },
1410
1411
1412        isPointInRange: function(node, offset) {
1413            assertRangeValid(this);
1414            assertNode(node, "HIERARCHY_REQUEST_ERR");
1415            assertSameDocumentOrFragment(node, this.startContainer);
1416
1417            return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1418                   (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1419        },
1420
1421        // The methods below are non-standard and invented by me.
1422
1423        // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1424        intersectsRange: function(range, touchingIsIntersecting) {
1425            assertRangeValid(this);
1426
1427            if (getRangeDocument(range) != getRangeDocument(this)) {
1428                throw new DOMException("WRONG_DOCUMENT_ERR");
1429            }
1430
1431            var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset),
1432                endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset);
1433
1434            return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
vendor: 10,745 bytes, lines 1435-1701
1435        },
1436
1437        intersection: function(range) {
1438            if (this.intersectsRange(range)) {
1439                var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1440                    endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1441
1442                var intersectionRange = this.cloneRange();
1443
1444                if (startComparison == -1) {
1445                    intersectionRange.setStart(range.startContainer, range.startOffset);
1446                }
1447                if (endComparison == 1) {
1448                    intersectionRange.setEnd(range.endContainer, range.endOffset);
1449                }
1450                return intersectionRange;
1451            }
1452            return null;
1453        },
1454
1455        union: function(range) {
1456            if (this.intersectsRange(range, true)) {
1457                var unionRange = this.cloneRange();
1458                if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1459                    unionRange.setStart(range.startContainer, range.startOffset);
1460                }
1461                if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1462                    unionRange.setEnd(range.endContainer, range.endOffset);
1463                }
1464                return unionRange;
1465            } else {
1466                throw new RangeException("Ranges do not intersect");
1467            }
1468        },
1469
1470        containsNode: function(node, allowPartial) {
1471            if (allowPartial) {
1472                return this.intersectsNode(node, false);
1473            } else {
1474                return this.compareNode(node) == n_i;
1475            }
1476        },
1477
1478        containsNodeContents: function(node) {
1479            return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0;
1480        },
1481
1482        containsRange: function(range) {
1483            return this.intersection(range).equals(range);
1484        },
1485
1486        containsNodeText: function(node) {
1487            var nodeRange = this.cloneRange();
1488            nodeRange.selectNode(node);
1489            var textNodes = nodeRange.getNodes([3]);
1490            if (textNodes.length > 0) {
1491                nodeRange.setStart(textNodes[0], 0);
1492                var lastTextNode = textNodes.pop();
1493                nodeRange.setEnd(lastTextNode, lastTextNode.length);
1494                var contains = this.containsRange(nodeRange);
1495                nodeRange.detach();
1496                return contains;
1497            } else {
1498                return this.containsNodeContents(node);
1499            }
1500        },
1501
1502        createNodeIterator: function(nodeTypes, filter) {
1503            assertRangeValid(this);
1504            return new RangeNodeIterator(this, nodeTypes, filter);
1505        },
1506
1507        getNodes: function(nodeTypes, filter) {
1508            assertRangeValid(this);
1509            return getNodesInRange(this, nodeTypes, filter);
1510        },
1511
1512        getDocument: function() {
1513            return getRangeDocument(this);
1514        },
1515
1516        collapseBefore: function(node) {
1517            assertNotDetached(this);
1518
1519            this.setEndBefore(node);
1520            this.collapse(false);
1521        },
1522
1523        collapseAfter: function(node) {
1524            assertNotDetached(this);
1525
1526            this.setStartAfter(node);
1527            this.collapse(true);
1528        },
1529
1530        getName: function() {
1531            return "DomRange";
1532        },
1533
1534        equals: function(range) {
1535            return Range.rangesEqual(this, range);
1536        },
1537
1538        inspect: function() {
1539            return inspect(this);
1540        }
1541    };
1542
1543    function copyComparisonConstantsToObject(obj) {
1544        obj.START_TO_START = s2s;
1545        obj.START_TO_END = s2e;
1546        obj.END_TO_END = e2e;
1547        obj.END_TO_START = e2s;
1548
1549        obj.NODE_BEFORE = n_b;
1550        obj.NODE_AFTER = n_a;
1551        obj.NODE_BEFORE_AND_AFTER = n_b_a;
1552        obj.NODE_INSIDE = n_i;
1553    }
1554
1555    function copyComparisonConstants(constructor) {
1556        copyComparisonConstantsToObject(constructor);
1557        copyComparisonConstantsToObject(constructor.prototype);
1558    }
1559
1560    function createRangeContentRemover(remover, boundaryUpdater) {
1561        return function() {
1562            assertRangeValid(this);
1563
1564            var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1565
1566            var iterator = new RangeIterator(this, true);
1567
1568            // Work out where to position the range after content removal
1569            var node, boundary;
1570            if (sc !== root) {
1571                node = dom.getClosestAncestorIn(sc, root, true);
1572                boundary = getBoundaryAfterNode(node);
1573                sc = boundary.node;
1574                so = boundary.offset;
1575            }
1576
1577            // Check none of the range is read-only
1578            iterateSubtree(iterator, assertNodeNotReadOnly);
1579
1580            iterator.reset();
1581
1582            // Remove the content
1583            var returnValue = remover(iterator);
1584            iterator.detach();
1585
1586            // Move to the new position
1587            boundaryUpdater(this, sc, so, sc, so);
1588
1589            return returnValue;
1590        };
1591    }
1592
1593    function createPrototypeRange(constructor, boundaryUpdater, detacher) {
1594        function createBeforeAfterNodeSetter(isBefore, isStart) {
1595            return function(node) {
1596                assertNotDetached(this);
1597                assertValidNodeType(node, beforeAfterNodeTypes);
1598                assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1599
1600                var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1601                (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1602            };
1603        }
1604
1605        function setRangeStart(range, node, offset) {
1606            var ec = range.endContainer, eo = range.endOffset;
1607            if (node !== range.startContainer || offset !== range.startOffset) {
1608                // Check the root containers of the range and the new boundary, and also check whether the new boundary
1609                // is after the current end. In either case, collapse the range to the new position
1610                if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) {
1611                    ec = node;
1612                    eo = offset;
1613                }
1614                boundaryUpdater(range, node, offset, ec, eo);
1615            }
1616        }
1617
1618        function setRangeEnd(range, node, offset) {
1619            var sc = range.startContainer, so = range.startOffset;
1620            if (node !== range.endContainer || offset !== range.endOffset) {
1621                // Check the root containers of the range and the new boundary, and also check whether the new boundary
1622                // is after the current end. In either case, collapse the range to the new position
1623                if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) {
1624                    sc = node;
1625                    so = offset;
1626                }
1627                boundaryUpdater(range, sc, so, node, offset);
1628            }
1629        }
1630
1631        function setRangeStartAndEnd(range, node, offset) {
1632            if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) {
1633                boundaryUpdater(range, node, offset, node, offset);
1634            }
1635        }
1636
1637        constructor.prototype = new RangePrototype();
1638
1639        api.util.extend(constructor.prototype, {
1640            setStart: function(node, offset) {
1641                assertNotDetached(this);
1642                assertNoDocTypeNotationEntityAncestor(node, true);
1643                assertValidOffset(node, offset);
1644
1645                setRangeStart(this, node, offset);
1646            },
1647
1648            setEnd: function(node, offset) {
1649                assertNotDetached(this);
1650                assertNoDocTypeNotationEntityAncestor(node, true);
1651                assertValidOffset(node, offset);
1652
1653                setRangeEnd(this, node, offset);
1654            },
1655
1656            setStartBefore: createBeforeAfterNodeSetter(true, true),
1657            setStartAfter: createBeforeAfterNodeSetter(false, true),
1658            setEndBefore: createBeforeAfterNodeSetter(true, false),
1659            setEndAfter: createBeforeAfterNodeSetter(false, false),
1660
1661            collapse: function(isStart) {
1662                assertRangeValid(this);
1663                if (isStart) {
1664                    boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1665                } else {
1666                    boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
1667                }
1668            },
1669
1670            selectNodeContents: function(node) {
1671                // This doesn't seem well specified: the spec talks only about selecting the node's contents, which
1672                // could be taken to mean only its children. However, browsers implement this the same as selectNode for
1673                // text nodes, so I shall do likewise
1674                assertNotDetached(this);
1675                assertNoDocTypeNotationEntityAncestor(node, true);
1676
1677                boundaryUpdater(this, node, 0, node, dom.getNodeLength(node));
1678            },
1679
1680            selectNode: function(node) {
1681                assertNotDetached(this);
1682                assertNoDocTypeNotationEntityAncestor(node, false);
1683                assertValidNodeType(node, beforeAfterNodeTypes);
1684
1685                var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
1686                boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
1687            },
1688
1689            extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
1690
1691            deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
1692
1693            canSurroundContents: function() {
1694                assertRangeValid(this);
1695                assertNodeNotReadOnly(this.startContainer);
1696                assertNodeNotReadOnly(this.endContainer);
1697
1698                // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1699                // no non-text nodes.
1700                var iterator = new RangeIterator(this, true);
1701                var boundariesInvalid = (iterator._first && (isNonTextParti
1701allySelected(iterator._first, this)) ||
1702                        (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1703                iterator.detach();
1704                return !boundariesInvalid;
1705            },
1706
1707            detach: function() {
1708                detacher(this);
1709            },
1710
1711            splitBoundaries: function() {
1712                assertRangeValid(this);
1713
1714
1715                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1716                var startEndSame = (sc === ec);
1717
1718                if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1719                    dom.splitDataNode(ec, eo);
1720
1721                }
1722
1723                if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1724
1725                    sc = dom.splitDataNode(sc, so);
1726                    if (startEndSame) {
1727                        eo -= so;
1728                        ec = sc;
1729                    } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) {
1730                        eo++;
1731                    }
1732                    so = 0;
1733
1734                }
1735                boundaryUpdater(this, sc, so, ec, eo);
1736            },
1737
1738            normalizeBoundaries: function() {
1739                assertRangeValid(this);
1740
1741                var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
1742
1743                var mergeForward = function(node) {
1744                    var sibling = node.nextSibling;
1745                    if (sibling && sibling.nodeType == node.nodeType) {
1746                        ec = node;
1747                        eo = node.length;
1748                        node.appendData(sibling.data);
1749                        sibling.parentNode.removeChild(sibling);
1750                    }
1751                };
1752
1753                var mergeBackward = function(node) {
1754                    var sibling = node.previousSibling;
1755                    if (sibling && sibling.nodeType == node.nodeType) {
1756                        sc = node;
1757                        var nodeLength = node.length;
1758                        so = sibling.length;
1759                        node.insertData(0, sibling.data);
1760                        sibling.parentNode.removeChild(sibling);
1761                        if (sc == ec) {
1762                            eo += so;
1763                            ec = sc;
1764                        } else if (ec == node.parentNode) {
1765                            var nodeIndex = dom.getNodeIndex(node);
1766                            if (eo == nodeIndex) {
1767                                ec = node;
1768                                eo = nodeLength;
1769                            } else if (eo > nodeIndex) {
1770                                eo--;
1771                            }
1772                        }
1773                    }
1774                };
1775
1776                var normalizeStart = true;
1777
1778                if (dom.isCharacterDataNode(ec)) {
1779                    if (ec.length == eo) {
1780                        mergeForward(ec);
1781                    }
1782                } else {
1783                    if (eo > 0) {
1784                        var endNode = ec.childNodes[eo - 1];
1785                        if (endNode && dom.isCharacterDataNode(endNode)) {
1786                            mergeForward(endNode);
1787                        }
1788                    }
1789                    normalizeStart = !this.collapsed;
1790                }
1791
1792                if (normalizeStart) {
1793                    if (dom.isCharacterDataNode(sc)) {
1794                        if (so == 0) {
1795                            mergeBackward(sc);
1796                        }
1797                    } else {
1798                        if (so < sc.childNodes.length) {
1799                            var startNode = sc.childNodes[so];
1800                            if (startNode && dom.isCharacterDataNode(startNode)) {
1801                                mergeBackward(startNode);
1802                            }
1803                        }
1804                    }
1805                } else {
1806                    sc = ec;
1807                    so = eo;
1808                }
1809
1810                boundaryUpdater(this, sc, so, ec, eo);
1811            },
1812
1813            collapseToPoint: function(node, offset) {
1814                assertNotDetached(this);
1815
1816                assertNoDocTypeNotationEntityAncestor(node, true);
1817                assertValidOffset(node, offset);
1818
1819                setRangeStartAndEnd(this, node, offset);
1820            }
1821        });
1822
1823        copyComparisonConstants(constructor);
1824    }
1825
1826    /*----------------------------------------------------------------------------------------------------------------*/
1827
1828    // Updates commonAncestorContainer and collapsed after boundary change
1829    function updateCollapsedAndCommonAncestor(range) {
1830        range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
1831        range.commonAncestorContainer = range.collapsed ?
1832            range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
1833    }
1834
1835    function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
1836        var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset);
1837        var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset);
1838
1839        range.startContainer = startContainer;
1840        range.startOffset = startOffset;
1841        range.endContainer = endContainer;
1842        range.endOffset = endOffset;
1843
1844        updateCollapsedAndCommonAncestor(range);
1845        dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved});
1846    }
1847
1848    function detach(range) {
1849        assertNotDetached(range);
1850        range.startContainer = range.startOffset = range.endContainer = range.endOffset = null;
1851        range.collapsed = range.commonAncestorContainer = null;
1852        dispatchEvent(range, "detach", null);
1853        range._listeners = null;
1854    }
1855
1856    /**
1857     * @constructor
1858     */
1859    function Range(doc) {
1860        this.startContainer = doc;
1861        this.startOffset = 0;
1862        this.endContainer = doc;
1863        this.endOffset = 0;
1864        this._listeners = {
1865            boundarychange: [],
1866            detach: []
1867        };
1868        updateCollapsedAndCommonAncestor(this);
1869    }
1870
1871    createPrototypeRange(Range, updateBoundaries, detach);
1872
1873    api.rangePrototype = RangePrototype.prototype;
1874
1875    Range.rangeProperties = rangeProperties;
1876    Range.RangeIterator = RangeIterator;
1877    Range.copyComparisonConstants = copyComparisonConstants;
1878    Range.createPrototypeRange = createPrototypeRange;
1879    Range.inspect = inspect;
1880    Range.getRangeDocument = getRangeDocument;
1881    Range.rangesEqual = function(r1, r2) {
1882        return r1.startContainer === r2.startContainer &&
1883               r1.startOffset === r2.startOffset &&
1884               r1.endContainer === r2.endContainer &&
1885               r1.endOffset === r2.endOffset;
1886    };
1887
1888    api.DomRange = Range;
1889    api.RangeException = RangeException;
1890});rangy.createModule("WrappedRange", function(api, module) {
1891    api.requireModules( ["DomUtil", "DomRange"] );
1892
1893    /**
1894     * @constructor
1895     */
1896    var WrappedRange;
1897    var dom = api.dom;
1898    var DomPosition = dom.DomPosition;
1899    var DomRange = api.DomRange;
1900
1901
1902
1903    /*----------------------------------------------------------------------------------------------------------------*/
1904
1905    /*
1906    This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
1907    method. For example, in the following (where pipes denote the selection boundaries):
1908
1909    <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
1910
1911    var range = document.selection.createRange();
1912    alert(range.parentElement().id); // Should alert "ul" but alerts "b"
1913
1914    This method returns the common ancestor node of the following:
1915    - the parentElement() of the textRange
1916    - the parentElement() of the textRange after calling collapse(true)
1917    - the parentElement() of the textRange after calling collapse(false)
1918     */
1919    function getTextRangeContainerElement(textRange) {
1920        var parentEl = textRange.parentElement();
1921
1922        var range = textRange.duplicate();
1923        range.collapse(true);
1924        var startEl = range.parentElement();
1925        range = textRange.duplicate();
1926        range.collapse(false);
1927        var endEl = range.parentElement();
1928        var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
1929
1930        return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
1931    }
1932
1933    function textRangeIsCollapsed(textRange) {
1934        return textRange.compareEndPoints("StartToEnd", textRange) == 0;
1935    }
1936
1937    // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as
1938    // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has
1939    // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling
1940    // for inputs and images, plus optimizations.
1941    function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) {
1942        var workingRange = textRange.duplicate();
1943
1944        workingRange.collapse(isStart);
1945        var containerElement = workingRange.parentElement();
1946
1947        // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
1948        // check for that
1949        // TODO: Find out when. Workaround for wholeRangeContainerElement may break this
1950        if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) {
1951            containerElement = wholeRangeContainerElement;
1952
1953        }
1954
1955
1956
1957        // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
1958        // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
1959        if (!containerElement.canHaveHTML) {
1960            return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
1961        }
1962
1963        var workingNode = dom.getDocument(containerElement).createElement("span");
1964        var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
1965        var previousNode, nextNode, boundaryPosition, boundaryNode;
1966
1967        // Move the working range through the container's children, starting at the end and working backwards, until the
1968        // working range reaches or goes past the boundary we're interested in
1969        do {
1970            containerElement.insertBefore(workingNode, workingNode.previousSibling);
1971            workingRange.moveToElementText(workingNode);
1972        } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 &&
1973                workingNode.previousSibling);
1974
1975        // We've now reached or gone past the boundary of the text range we're interested in
1976        // so have identified the node we want
1977        boundaryNode = workingNode.nextSibling;
1978
1979        if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) {
1980            // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the
1981            // node containing the text range's boundary, so we move the end of the working range to the boundary point
1982            // and measure the length of its text to get the boundary's offset within the node.
1983            workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
1984
1985
1986            var offset;
1987
1988            if (/[\r\n]/.test(boundaryNode.data)) {
1989                /*
1990                For the particular case of a boundary within a text node containing line breaks (within a <pre> element,
1991                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
1992
1993                - Each line break is represented as \r in the text node's data/nodeValue properties
1994                - Each line break is represented as \r\n in the TextRange's 'text' property
1995                - The 'text' property of the TextRange does not contain trailing line breaks
1996
1997                To get round the problem presented by the final fact above, we can use the fact that TextRange's
1998                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
1999                the same as the number of characters it was instructed to move. The simplest approach is to use this to
2000                store the characters moved when moving both the start and end of the range to the start of the document
2001                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
2002                However, this is extremely slow when the document is large and the range is near the end of it. Clearly
2003                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
2004                problem.
2005
2006                Another approach that works is to use moveStart() to move the start boundary of the range up to the end
2007                boundary one character at a time and incrementing a counter with the value returned by the moveStart()
2008                call. However, the check for whether the start boundary has reached the end boundary is expensive, so
2009                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
2010                the range within the document).
2011
2012                The method below is a hybrid of the two methods above. It uses the fact that a string containing the
2013                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
2014                text of the TextRange, so the start of the range is moved that length initially and then a character at
2015                a time to make up for any trailing line breaks not contained in the 'text' property. This has good
2016                performance in most situations compared to the previous two methods.
2017                */
2018                var tempRange = workingRange.duplicate();
2019                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2020
2021                offset = tempRange.moveStart("character", rangeLength);
2022                while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2023                    offset++;
2024                    tempRange.moveStart("character", 1);
2025                }
2026            } else {
2027                offset = workingRange.text.length;
2028            }
2029            boundaryPosition = new DomPosition(boundaryNode, offset);
2030        } else {
2031
2032
2033            // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2034            // a position within that, and likewise for a start boundary preceding a character data node
2035            previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2036            nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2037
2038
2039
2040            if (nextNode && dom.isCharacterDataNode(nextNode)) {
2041                boundaryPosition = new DomPosition(nextNode, 0);
2042            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
2043                boundaryPosition = new DomPosition(previousNode, previousNode.length);
2044            } else {
2045                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2046            }
2047        }
2048
2049        // Clean up
2050        workingNode.parentNode.removeChild(workingNode);
2051
2052        return boundaryPosition;
2053    }
2054
2055    // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
2056    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2057    // (http://code.google.com/p/ierange/)
2058    function createBoundaryTextRange(boundaryPosition, isStart) {
2059        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2060        var doc = dom.getDocument(boundaryPosition.node);
2061        var workingNode, childNodes, workingRange = doc.body.createTextRange();
2062        var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
2063
2064        if (nodeIsDataNode) {
2065            boundaryNode = boundaryPosition.node;
2066            boundaryParent = boundaryNode.parentNode;
2067        } else {
2068            childNodes = boundaryPosition.node.childNodes;
2069            boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2070            boundaryParent = boundaryPosition.node;
2071        }
2072
2073        // Position the range immediately before the node containing the boundary
2074        workingNode = doc.createElement("span");
2075
2076        // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
2077        // element rather than immediately before or after it, which is what we want
2078        workingNode.innerHTML = "&#feff;";
2079
2080        // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2081        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2082        if (boundaryNode) {
2083            boundaryParent.insertBefore(workingNode, boundaryNode);
2084        } else {
2085            boundaryParent.appendChild(workingNode);
2086        }
2087
2088        workingRange.moveToElementText(workingNode);
2089        workingRange.collapse(!isStart);
2090
2091        // Clean up
2092        boundaryParent.removeChild(workingNode);
2093
2094        // Move the working range to the text offset, if required
2095        if (nodeIsDataNode) {
2096            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2097        }
2098
2099        return workingRange;
2100    }
2101
2102    /*----------------------------------------------------------------------------------------------------------------*/
2103
2104    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
2105        // This is a wrapper around the browser's native DOM Range. It has two aims:
2106        // - Provide workarounds for specific browser bugs
2107        // - provide convenient extensions, which are inherited from Rangy's DomRange
2108
2109        (function() {
2110            var rangeProto;
2111            var rangeProperties = DomRange.rangeProperties;
2112            var canSetRangeStartAfterEnd;
2113
2114            function updateRangeProperties(range) {
2115                var i = rangeProperties.length, prop;
2116                while (i--) {
2117                    prop = rangeProperties[i];
2118                    range[prop] = range.nativeRange[prop];
2119                }
2120            }
2121
2122            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
2123                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2124                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2125
2126                // Always set both boundaries for the benefit of IE9 (see issue 35)
2127                if (startMoved || endMoved) {
2128                    range.setEnd(endContainer, endOffset);
2129                    range.setStart(startContainer, startOffset);
2130                }
2131            }
2132
2133            function detach(range) {
2134                range.nativeRange.detach();
2135                range.detached = true;
2136                var i = rangeProperties.length, prop;
2137                while (i--) {
2138                    prop = rangeProperties[i];
2139                    range[prop] = null;
2140                }
2141            }
2142
2143            var createBeforeAfterNodeSetter;
2144
2145            WrappedRange = function(range) {
2146                if (!range) {
2147                    throw new Error("Range must be specified");
2148                }
2149                this.nativeRange = range;
2150                updateRangeProperties(this);
2151            };
2152
2153            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
2154
2155            rangeProto = WrappedRange.prototype;
2156
2157            rangeProto.selectNode = function(node) {
2158                this.nativeRange.selectNode(node);
2159                updateRangeProperties(this);
2160            };
2161
2162            rangeProto.deleteContents = function() {
2163                this.nativeRange.deleteContents();
2164                updateRangeProperties(this);
2165            };
2166
2167            rangeProto.extractContents = function() {
2168                var frag = this.nativeRange.extractContents();
2169                updateRangeProperties(this);
2170                return frag;
2171            };
2172
2173            rangeProto.cloneContents = function() {
2174                return this.nativeRange.cloneContents();
2175            };
2176
2177            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
2178            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
2179            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
2180            // insertNode, which works but is almost certainly slower than the native implementation.
2181/*
2182            rangeProto.insertNode = function(node) {
2183                this.nativeRange.insertNode(node);
2184                updateRangeProperties(this);
2185            };
2186*/
2187
2188            rangeProto.surroundContents = function(node) {
2189                this.nativeRange.surroundContents(node);
2190                updateRangeProperties(this);
2191            };
2192
2193            rangeProto.collapse = function(isStart) {
2194                this.nativeRange.collapse(isStart);
2195                updateRangeProperties(this);
2196            };
2197
2198            rangeProto.cloneRange = function() {
2199                return new WrappedRange(this.nativeRange.cloneRange());
2200            };
2201
2202            rangeProto.refresh = function() {
2203                updateRangeProperties(this);
2204            };
2205
2206            rangeProto.toString = function() {
2207                return this.nativeRange.toString();
2208            };
2209
2210            // Create test range and node for feature detection
2211
2212            var testTextNode = document.createTextNode("test");
2213            dom.getBody(document).appendChild(testTextNode);
2214            var range = document.createRange();
2215
2216            /*--------------------------------------------------------------------------------------------------------*/
2217
2218            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2219            // correct for it
2220
2221            range.setStart(testTextNode, 0);
2222            range.setEnd(testTextNode, 0);
2223
2224            try {
2225                range.setStart(testTextNode, 1);
2226                canSetRangeStartAfterEnd = true;
2227
2228                rangeProto.setStart = function(node, offset) {
2229                    this.nativeRange.setStart(node, offset);
2230                    updateRangeProperties(this);
2231                };
2232
2233                rangeProto.setEnd = function(node, offset) {
2234                    this.nativeRange.setEnd(node, offset);
2235                    updateRangeProperties(this);
2236                };
2237
2238                createBeforeAfterNodeSetter = function(name) {
2239                    return function(node) {
2240                        this.nativeRange[name](node);
2241                        updateRangeProperties(this);
2242                    };
2243                };
2244
2245            } catch(ex) {
2246
2247
2248                canSetRangeStartAfterEnd = false;
2249
2250                rangeProto.setStart = function(node, offset) {
2251                    try {
2252                        this.nativeRange.setStart(node, offset);
2253                    } catch (ex) {
2254                        this.nativeRange.setEnd(node, offset);
2255                        this.nativeRange.setStart(node, offset);
2256                    }
2257                    updateRangeProperties(this);
2258                };
2259
2260                rangeProto.setEnd = function(node, offset) {
2261                    try {
2262                        this.nativeRange.setEnd(node, offset);
2263                    } catch (ex) {
2264                        this.nativeRange.setStart(node, offset);
2265                        this.nativeRange.setEnd(node, offset);
2266                    }
2267                    updateRangeProperties(this);
2268                };
2269
2270                createBeforeAfterNodeSetter = function(name, oppositeName) {
2271                    return function(node) {
2272                        try {
2273                            this.nativeRange[name](node);
2274                        } catch (ex) {
2275                            this.nativeRange[oppositeName](node);
2276                            this.nativeRange[name](node);
2277                        }
2278                        updateRangeProperties(this);
2279                    };
2280                };
2281            }
2282
2283            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2284            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2285            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2286            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2287
2288            /*--------------------------------------------------------------------------------------------------------*/
2289
2290            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
2291            // the 0th character of the text node
2292            range.selectNodeContents(testTextNode);
2293            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
2294                    range.startOffset == 0 && range.endOffset == testTextNode.length) {
2295                rangeProto.selectNodeContents = function(node) {
2296                    this.nativeRange.selectNodeContents(node);
2297                    updateRangeProperties(this);
2298                };
2299            } else {
2300                rangeProto.selectNodeContents = function(node) {
2301                    this.setStart(node, 0);
2302                    this.setEnd(node, DomRange.getEndOffset(node));
2303                };
2304            }
2305
2306            /*--------------------------------------------------------------------------------------------------------*/
2307
2308            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
2309            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2310
2311            range.selectNodeContents(testTextNode);
2312            range.setEnd(testTextNode, 3);
2313
2314            var range2 = document.createRange();
2315            range2.selectNodeContents(testTextNode);
2316            range2.setEnd(testTextNode, 4);
2317            range2.setStart(testTextNode, 2);
2318
2319            if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
2320                    range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2321                // This is the wrong way round, so correct for it
2322
2323
2324                rangeProto.compareBoundaryPoints = function(type, range) {
2325                    range = range.nativeRange || range;
2326                    if (type == range.START_TO_END) {
2327                        type = range.END_TO_START;
2328                    } else if (type == range.END_TO_START) {
2329                        type = range.START_TO_END;
2330                    }
2331                    return this.nativeRange.compareBoundaryPoints(type, range);
2332                };
2333            } else {
2334                rangeProto.compareBoundaryPoints = function(type, range) {
2335                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2336                };
2337            }
2338
2339            /*--------------------------------------------------------------------------------------------------------*/
2340
2341            // Test for existence of createContextualFragment and delegate to it if it exists
2342            if (api.util.isHostMethod(range, "createContextualFragment")) {
2343                rangeProto.createContextualFragment = function(fragmentStr) {
2344                    return this.nativeRange.createContextualFragment(fragmentStr);
2345                };
2346            }
2347
2348            /*--------------------------------------------------------------------------------------------------------*/
2349
2350            // Clean up
2351            dom.getBody(document).removeChild(testTextNode);
2352            range.detach();
2353            range2.detach();
2354        })();
2355
2356        api.createNativeRange = function(doc) {
2357            doc = doc || document;
2358            return doc.createRange();
2359        };
2360    } else if (api.features.implementsTextRange) {
2361        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2362        // prototype
2363
2364        WrappedRange = function(textRange) {
2365            this.textRange = textRange;
2366            this.refresh();
2367        };
2368
2369        WrappedRange.prototype = new DomRange(document);
2370
2371        WrappedRange.prototype.refresh = function() {
2372            var start, end;
2373
2374            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2375            var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2376
2377            if (textRangeIsCollapsed(this.textRange)) {
2378                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
2379            } else {
2380
2381                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2382                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
2383            }
2384
2385            this.setStart(start.node, start.offset);
2386            this.setEnd(end.node, end.offset);
2387        };
2388
2389        DomRange.copyComparisonConstants(WrappedRange);
2390
2391        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2392        var globalObj = (function() { return this; })();
2393        if (typeof globalObj.Range == "undefined") {
2394            globalObj.Range = WrappedRange;
2395        }
2396
2397        api.createNativeRange = function(doc) {
2398            doc = doc || document;
2399            return doc.body.createTextRange();
2400        };
2401    }
2402
2403    if (api.features.implementsTextRange) {
2404        WrappedRange.rangeToTextRange = function(range) {
2405            if (range.collapsed) {
2406                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2407
2408
2409
2410                return tr;
2411
2412                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2413            } else {
2414                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2415                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2416                var textRange = dom.getDocument(range.startContainer).body.createTextRange();
2417                textRange.setEndPoint("StartToStart", startRange);
2418                textRange.setEndPoint("EndToEnd", endRange);
2419                return textRange;
2420            }
2421        };
2422    }
2423
2424    WrappedRange.prototype.getName = function() {
2425        return "WrappedRange";
2426    };
2427
2428    api.WrappedRange = WrappedRange;
2429
2430    api.createRange = function(doc) {
2431        doc = doc || document;
2432        return new WrappedRange(api.createNativeRange(doc));
2433    };
2434
2435    api.createRangyRange = function(doc) {
2436        doc = doc || document;
2437        return new DomRange(doc);
2438    };
2439
2440    api.createIframeRange = function(iframeEl) {
2441        return api.createRange(dom.getIframeDocument(iframeEl));
2442    };
2443
2444    api.createIframeRangyRange = function(iframeEl) {
2445        return api.createRangyRange(dom.getIframeDocument(iframeEl));
2446    };
2447
2448    api.addCreateMissingNativeApiListener(function(win) {
2449        var doc = win.document;
2450        if (typeof doc.createRange == "undefined") {
2451            doc.createRange = function() {
2452                return api.createRange(this);
2453            };
2454        }
2455        doc = win = null;
2456    });
2457});rangy.createModule("WrappedSelection", function(api, module) {
2458    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
2459    // spec (http://html5.org/specs/dom-range.html)
2460
2461    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
2462
2463    api.config.checkSelectionRanges = true;
2464
2465    var BOOLEAN = "boolean",
2466        windowPropertyName = "_rangySelection",
2467        dom = api.dom,
2468        util = api.util,
2469        DomRange = api.DomRange,
2470        WrappedRange = api.WrappedRange,
2471        DOMException = api.DOMException,
2472        DomPosition = dom.DomPosition,
2473        getSelection,
2474        selectionIsCollapsed,
2475        CONTROL = "Control";
2476
2477
2478
2479    function getWinSelection(winParam) {
2480        return (winParam || window).getSelection();
2481    }
2482
2483    function getDocSelection(winParam) {
2484        return (winParam || window).document.selection;
2485    }
2486
2487    // Test for the Range/TextRange and Selection features required
2488    // Test for ability to retrieve selection
2489    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
2490        implementsDocSelection = api.util.isHostObject(document, "selection");
2491
2492    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2493
2494    if (useDocumentSelection) {
2495        getSelection = getDocSelection;
2496        api.isSelectionValid = function(winParam) {
2497            var doc = (winParam || window).document, nativeSel = doc.selection;
2498
2499            // Check whether the selection TextRange is actually contained within the correct document
2500            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
2501        };
2502    } else if (implementsWinGetSelection) {
2503        getSelection = getWinSelection;
2504        api.isSelectionValid = function() {
2505            return true;
2506        };
2507    } else {
2508        module.fail("Neither document.selection or window.getSelection() detected.");
2509    }
2510
2511    api.getNativeSelection = getSelection;
2512
2513    var testSelection = getSelection();
2514    var testRange = api.createNativeRange(document);
2515    var body = dom.getBody(document);
2516
2517    // Obtaining a range from a selection
2518    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focu
2518sNode"] &&
2519                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
2520    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2521
2522    // Test for existence of native selection extend() method
2523    var selectionHasExtend = util.isHostMethod(testSelection, "extend");
2524    api.features.selectionHasExtend = selectionHasExtend;
2525
2526    // Test if rangeCount exists
2527    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
2528    api.features.selectionHasRangeCount = selectionHasRangeCount;
2529
2530    var selectionSupportsMultipleRanges = false;
2531    var collapsedNonEditableSelectionsSupported = true;
2532
2533    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2534            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
2535
2536        (function() {
2537            var iframe = document.createElement("iframe");
2538            body.appendChild(iframe);
2539
2540            var iframeDoc = dom.getIframeDocument(iframe);
2541            iframeDoc.open();
2542            iframeDoc.write("<html><head></head><body>12</body></html>");
2543            iframeDoc.close();
2544
2545            var sel = dom.getIframeWindow(iframe).getSelection();
2546            var docEl = iframeDoc.documentElement;
2547            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
2548
2549            // Test whether the native selection will allow a collapsed selection within a non-editable element
2550            var r1 = iframeDoc.createRange();
2551            r1.setStart(textNode, 1);
2552            r1.collapse(true);
2553            sel.addRange(r1);
2554            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2555            sel.removeAllRanges();
2556
2557            // Test whether the native selection is capable of supporting multiple ranges
2558            var r2 = r1.cloneRange();
2559            r1.setStart(textNode, 0);
2560            r2.setEnd(textNode, 2);
2561            sel.addRange(r1);
2562            sel.addRange(r2);
2563
2564            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2565
2566            // Clean up
2567            r1.detach();
2568            r2.detach();
2569
2570            body.removeChild(iframe);
2571        })();
2572    }
2573
2574    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
vendor: 7,635 bytes, lines 2575-2763
2575    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2576
2577    // ControlRanges
2578    var implementsControlRange = false, testControlRange;
2579
2580    if (body && util.isHostMethod(body, "createControlRange")) {
2581        testControlRange = body.createControlRange();
2582        if (util.areHostProperties(testControlRange, ["item", "add"])) {
2583            implementsControlRange = true;
2584        }
2585    }
2586    api.features.implementsControlRange = implementsControlRange;
2587
2588    // Selection collapsedness
2589    if (selectionHasAnchorAndFocus) {
2590        selectionIsCollapsed = function(sel) {
2591            return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
2592        };
2593    } else {
2594        selectionIsCollapsed = function(sel) {
2595            return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
2596        };
2597    }
2598
2599    function updateAnchorAndFocusFromRange(sel, range, backwards) {
2600        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
2601        sel.anchorNode = range[anchorPrefix + "Container"];
2602        sel.anchorOffset = range[anchorPrefix + "Offset"];
2603        sel.focusNode = range[focusPrefix + "Container"];
2604        sel.focusOffset = range[focusPrefix + "Offset"];
2605    }
2606
2607    function updateAnchorAndFocusFromNativeSelection(sel) {
2608        var nativeSel = sel.nativeSelection;
2609        sel.anchorNode = nativeSel.anchorNode;
2610        sel.anchorOffset = nativeSel.anchorOffset;
2611        sel.focusNode = nativeSel.focusNode;
2612        sel.focusOffset = nativeSel.focusOffset;
2613    }
2614
2615    function updateEmptySelection(sel) {
2616        sel.anchorNode = sel.focusNode = null;
2617        sel.anchorOffset = sel.focusOffset = 0;
2618        sel.rangeCount = 0;
2619        sel.isCollapsed = true;
2620        sel._ranges.length = 0;
2621    }
2622
2623    function getNativeRange(range) {
2624        var nativeRange;
2625        if (range instanceof DomRange) {
2626            nativeRange = range._selectionNativeRange;
2627            if (!nativeRange) {
2628                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
2629                nativeRange.setEnd(range.endContainer, range.endOffset);
2630                nativeRange.setStart(range.startContainer, range.startOffset);
2631                range._selectionNativeRange = nativeRange;
2632                range.attachListener("detach", function() {
2633
2634                    this._selectionNativeRange = null;
2635                });
2636            }
2637        } else if (range instanceof WrappedRange) {
2638            nativeRange = range.nativeRange;
2639        } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
2640            nativeRange = range;
2641        }
2642        return nativeRange;
2643    }
2644
2645    function rangeContainsSingleElement(rangeNodes) {
2646        if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
2647            return false;
2648        }
2649        for (var i = 1, len = rangeNodes.length; i < len; ++i) {
2650            if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
2651                return false;
2652            }
2653        }
2654        return true;
2655    }
2656
2657    function getSingleElementFromRange(range) {
2658        var nodes = range.getNodes();
2659        if (!rangeContainsSingleElement(nodes)) {
2660            throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
2661        }
2662        return nodes[0];
2663    }
2664
2665    function isTextRange(range) {
2666        return !!range && typeof range.text != "undefined";
2667    }
2668
2669    function updateFromTextRange(sel, range) {
2670        // Create a Range from the selected TextRange
2671        var wrappedRange = new WrappedRange(range);
2672        sel._ranges = [wrappedRange];
2673
2674        updateAnchorAndFocusFromRange(sel, wrappedRange, false);
2675        sel.rangeCount = 1;
2676        sel.isCollapsed = wrappedRange.collapsed;
2677    }
2678
2679    function updateControlSelection(sel) {
2680        // Update the wrapped selection based on what's now in the native selection
2681        sel._ranges.length = 0;
2682        if (sel.docSelection.type == "None") {
2683            updateEmptySelection(sel);
2684        } else {
2685            var controlRange = sel.docSelection.createRange();
2686            if (isTextRange(controlRange)) {
2687                // This case (where the selection type is "Control" and calling createRange() on the selection returns
2688                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
2689                // ControlRange have been removed from the ControlRange and removed from the document.
2690                updateFromTextRange(sel, controlRange);
2691            } else {
2692                sel.rangeCount = controlRange.length;
2693                var range, doc = dom.getDocument(controlRange.item(0));
2694                for (var i = 0; i < sel.rangeCount; ++i) {
2695                    range = api.createRange(doc);
2696                    range.selectNode(controlRange.item(i));
2697                    sel._ranges.push(range);
2698                }
2699                sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
2700                updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
2701            }
2702        }
2703    }
2704
2705    function addRangeToControlSelection(sel, range) {
2706        var controlRange = sel.docSelection.createRange();
2707        var rangeElement = getSingleElementFromRange(range);
2708
2709        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
2710        // contained by the supplied range
2711        var doc = dom.getDocument(controlRange.item(0));
2712        var newControlRange = dom.getBody(doc).createControlRange();
2713        for (var i = 0, len = controlRange.length; i < len; ++i) {
2714            newControlRange.add(controlRange.item(i));
2715        }
2716        try {
2717            newControlRange.add(rangeElement);
2718        } catch (ex) {
2719            throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
2720        }
2721        newControlRange.select();
2722
2723        // Update the wrapped selection based on what's now in the native selection
2724        updateControlSelection(sel);
2725    }
2726
2727    var getSelectionRangeAt;
2728
2729    if (util.isHostMethod(testSelection,  "getRangeAt")) {
2730        getSelectionRangeAt = function(sel, index) {
2731            try {
2732                return sel.getRangeAt(index);
2733            } catch(ex) {
2734                return null;
2735            }
2736        };
2737    } else if (selectionHasAnchorAndFocus) {
2738        getSelectionRangeAt = function(sel) {
2739            var doc = dom.getDocument(sel.anchorNode);
2740            var range = api.createRange(doc);
2741            range.setStart(sel.anchorNode, sel.anchorOffset);
2742            range.setEnd(sel.focusNode, sel.focusOffset);
2743
2744            // Handle the case when the selection was selected backwards (from the end to the start in the
2745            // document)
2746            if (range.collapsed !== this.isCollapsed) {
2747                range.setStart(sel.focusNode, sel.focusOffset);
2748                range.setEnd(sel.anchorNode, sel.anchorOffset);
2749            }
2750
2751            return range;
2752        };
2753    }
2754
2755    /**
2756     * @constructor
2757     */
2758    function WrappedSelection(selection, docSelection, win) {
2759        this.nativeSelection = selection;
2760        this.docSelection = docSelection;
2761        this._ranges = [];
2762        this.win = win;
2763        this.refresh();
2764    }
2765
2766    api.getSelection = function(win) {
2767        win = win || window;
2768        var sel = win[windowPropertyName];
2769        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
2770        if (sel) {
2771            sel.nativeSelection = nativeSel;
2772            sel.docSelection = docSel;
2773            sel.refresh(win);
2774        } else {
2775            sel = new WrappedSelection(nativeSel, docSel, win);
2776            win[windowPropertyName] = sel;
2777        }
2778        return sel;
2779    };
2780
2781    api.getIframeSelection = function(iframeEl) {
2782        return api.getSelection(dom.getIframeWindow(iframeEl));
2783    };
2784
2785    var selProto = WrappedSelection.prototype;
2786
2787    function createControlSelection(sel, ranges) {
2788        // Ensure that the selection becomes of type "Control"
2789        var doc = dom.getDocument(ranges[0].startContainer);
2790        var controlRange = dom.getBody(doc).createControlRange();
2791        for (var i = 0, el; i < rangeCount; ++i) {
2792            el = getSingleElementFromRange(ranges[i]);
2793            try {
2794                controlRange.add(el);
2795            } catch (ex) {
2796                throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
2797            }
2798        }
2799        controlRange.select();
2800
2801        // Update the wrapped selection based on what's now in the native selection
2802        updateControlSelection(sel);
2803    }
2804
2805    // Selecting a range
2806    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
2807        selProto.removeAllRanges = function() {
2808            this.nativeSelection.removeAllRanges();
2809            updateEmptySelection(this);
2810        };
2811
2812        var addRangeBackwards = function(sel, range) {
2813            var doc = DomRange.getRangeDocument(range);
2814            var endRange = api.createRange(doc);
2815            endRange.collapseToPoint(range.endContainer, range.endOffset);
2816            sel.nativeSelection.addRange(getNativeRange(endRange));
2817            sel.nativeSelection.extend(range.startContainer, range.startOffset);
2818            sel.refresh();
2819        };
2820
2821        if (selectionHasRangeCount) {
2822            selProto.addRange = function(range, backwards) {
2823                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
2824                    addRangeToControlSelection(this, range);
2825                } else {
2826                    if (backwards && selectionHasExtend) {
2827                        addRangeBackwards(this, range);
2828                    } else {
2829                        var previousRangeCount;
2830                        if (selectionSupportsMultipleRanges) {
2831                            previousRangeCount = this.rangeCount;
2832                        } else {
2833                            this.removeAllRanges();
2834                            previousRangeCount = 0;
2835                        }
2836                        this.nativeSelection.addRange(getNativeRange(range));
2837
2838                        // Check whether adding the range was successful
2839                        this.rangeCount = this.nativeSelection.rangeCount;
2840
2841                        if (this.rangeCount == previousRangeCount + 1) {
2842                            // The range was added successfully
2843
2844                            // Check whether the range that we added to the selection is reflected in the last range extracted from
2845                            // the selection
2846                            if (api.config.checkSelectionRanges) {
2847                                var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
2848                                if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
2849                                    // Happens in WebKit with, for example, a selection placed at the start of a text node
2850                                    range = new WrappedRange(nativeRange);
2851                                }
2852                            }
2853                            this._ranges[this.rangeCount - 1] = range;
2854                            updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
2855                            this.isCollapsed = selectionIsCollapsed(this);
2856                        } else {
2857                            // The range was not added successfully. The simplest thing is to refresh
2858                            this.refresh();
vendor: 10,254 bytes, lines 2859-3114
2859                        }
2860                    }
2861                }
2862            };
2863        } else {
2864            selProto.addRange = function(range, backwards) {
2865                if (backwards && selectionHasExtend) {
2866                    addRangeBackwards(this, range);
2867                } else {
2868                    this.nativeSelection.addRange(getNativeRange(range));
2869                    this.refresh();
2870                }
2871            };
2872        }
2873
2874        selProto.setRanges = function(ranges) {
2875            if (implementsControlRange && ranges.length > 1) {
2876                createControlSelection(this, ranges);
2877            } else {
2878                this.removeAllRanges();
2879                for (var i = 0, len = ranges.length; i < len; ++i) {
2880                    this.addRange(ranges[i]);
2881                }
2882            }
2883        };
2884    } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
2885               implementsControlRange && useDocumentSelection) {
2886
2887        selProto.removeAllRanges = function() {
2888            // Added try/catch as fix for issue #21
2889            try {
2890                this.docSelection.empty();
2891
2892                // Check for empty() not working (issue #24)
2893                if (this.docSelection.type != "None") {
2894                    // Work around failure to empty a control selection by instead selecting a TextRange and then
2895                    // calling empty()
2896                    var doc;
2897                    if (this.anchorNode) {
2898                        doc = dom.getDocument(this.anchorNode);
2899                    } else if (this.docSelection.type == CONTROL) {
2900                        var controlRange = this.docSelection.createRange();
2901                        if (controlRange.length) {
2902                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
2903                        }
2904                    }
2905                    if (doc) {
2906                        var textRange = doc.body.createTextRange();
2907                        textRange.select();
2908                        this.docSelection.empty();
2909                    }
2910                }
2911            } catch(ex) {}
2912            updateEmptySelection(this);
2913        };
2914
2915        selProto.addRange = function(range) {
2916            if (this.docSelection.type == CONTROL) {
2917                addRangeToControlSelection(this, range);
2918            } else {
2919                WrappedRange.rangeToTextRange(range).select();
2920                this._ranges[0] = range;
2921                this.rangeCount = 1;
2922                this.isCollapsed = this._ranges[0].collapsed;
2923                updateAnchorAndFocusFromRange(this, range, false);
2924            }
2925        };
2926
2927        selProto.setRanges = function(ranges) {
2928            this.removeAllRanges();
2929            var rangeCount = ranges.length;
2930            if (rangeCount > 1) {
2931                createControlSelection(this, ranges);
2932            } else if (rangeCount) {
2933                this.addRange(ranges[0]);
2934            }
2935        };
2936    } else {
2937        module.fail("No means of selecting a Range or TextRange was found");
2938        return false;
2939    }
2940
2941    selProto.getRangeAt = function(index) {
2942        if (index < 0 || index >= this.rangeCount) {
2943            throw new DOMException("INDEX_SIZE_ERR");
2944        } else {
2945            return this._ranges[index];
2946        }
2947    };
2948
2949    var refreshSelection;
2950
2951    if (useDocumentSelection) {
2952        refreshSelection = function(sel) {
2953            var range;
2954            if (api.isSelectionValid(sel.win)) {
2955                range = sel.docSelection.createRange();
2956            } else {
2957                range = dom.getBody(sel.win.document).createTextRange();
2958                range.collapse(true);
2959            }
2960
2961
2962            if (sel.docSelection.type == CONTROL) {
2963                updateControlSelection(sel);
2964            } else if (isTextRange(range)) {
2965                updateFromTextRange(sel, range);
2966            } else {
2967                updateEmptySelection(sel);
2968            }
2969        };
2970    } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
2971        refreshSelection = function(sel) {
2972            if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
2973                updateControlSelection(sel);
2974            } else {
2975                sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
2976                if (sel.rangeCount) {
2977                    for (var i = 0, len = sel.rangeCount; i < len; ++i) {
2978                        sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
2979                    }
2980                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
2981                    sel.isCollapsed = selectionIsCollapsed(sel);
2982                } else {
2983                    updateEmptySelection(sel);
2984                }
2985            }
2986        };
2987    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
2988        refreshSelection = function(sel) {
2989            var range, nativeSel = sel.nativeSelection;
2990            if (nativeSel.anchorNode) {
2991                range = getSelectionRangeAt(nativeSel, 0);
2992                sel._ranges = [range];
2993                sel.rangeCount = 1;
2994                updateAnchorAndFocusFromNativeSelection(sel);
2995                sel.isCollapsed = selectionIsCollapsed(sel);
2996            } else {
2997                updateEmptySelection(sel);
2998            }
2999        };
3000    } else {
3001        module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3002        return false;
3003    }
3004
3005    selProto.refresh = function(checkForChanges) {
3006        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3007        refreshSelection(this);
3008        if (checkForChanges) {
3009            var i = oldRanges.length;
3010            if (i != this._ranges.length) {
3011                return false;
3012            }
3013            while (i--) {
3014                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
3015                    return false;
3016                }
3017            }
3018            return true;
3019        }
3020    };
3021
3022    // Removal of a single range
3023    var removeRangeManually = function(sel, range) {
3024        var ranges = sel.getAllRanges(), removed = false;
3025        sel.removeAllRanges();
3026        for (var i = 0, len = ranges.length; i < len; ++i) {
3027            if (removed || range !== ranges[i]) {
3028                sel.addRange(ranges[i]);
3029            } else {
3030                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
3031                // times. removeRange should only remove the first instance, so the following ensures only the first
3032                // instance is removed
3033                removed = true;
3034            }
3035        }
3036        if (!sel.rangeCount) {
3037            updateEmptySelection(sel);
3038        }
3039    };
3040
3041    if (implementsControlRange) {
3042        selProto.removeRange = function(range) {
3043            if (this.docSelection.type == CONTROL) {
3044                var controlRange = this.docSelection.createRange();
3045                var rangeElement = getSingleElementFromRange(range);
3046
3047                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3048                // element contained by the supplied range
3049                var doc = dom.getDocument(controlRange.item(0));
3050                var newControlRange = dom.getBody(doc).createControlRange();
3051                var el, removed = false;
3052                for (var i = 0, len = controlRange.length; i < len; ++i) {
3053                    el = controlRange.item(i);
3054                    if (el !== rangeElement || removed) {
3055                        newControlRange.add(controlRange.item(i));
3056                    } else {
3057                        removed = true;
3058                    }
3059                }
3060                newControlRange.select();
3061
3062                // Update the wrapped selection based on what's now in the native selection
3063                updateControlSelection(this);
3064            } else {
3065                removeRangeManually(this, range);
3066            }
3067        };
3068    } else {
3069        selProto.removeRange = function(range) {
3070            removeRangeManually(this, range);
3071        };
3072    }
3073
3074    // Detecting if a selection is backwards
3075    var selectionIsBackwards;
3076    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
3077        selectionIsBackwards = function(sel) {
3078            var backwards = false;
3079            if (sel.anchorNode) {
3080                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
3081            }
3082            return backwards;
3083        };
3084
3085        selProto.isBackwards = function() {
3086            return selectionIsBackwards(this);
3087        };
3088    } else {
3089        selectionIsBackwards = selProto.isBackwards = function() {
3090            return false;
3091        };
3092    }
3093
3094    // Selection text
3095    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
3096    selProto.toString = function() {
3097
3098        var rangeTexts = [];
3099        for (var i = 0, len = this.rangeCount; i < len; ++i) {
3100            rangeTexts[i] = "" + this._ranges[i];
3101        }
3102        return rangeTexts.join("");
3103    };
3104
3105    function assertNodeInSameDocument(sel, node) {
3106        if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
3107            throw new DOMException("WRONG_DOCUMENT_ERR");
3108        }
3109    }
3110
3111    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
3112    selProto.collapse = function(node, offset) {
3113        assertNodeInSameDocument(this, node);
3114        var range = api.createRange(dom.getDocument(node));
3115        range.collapseToPoint(node, offset);
3116        this.removeAllRanges();
3117        this.addRange(range);
3118        this.isCollapsed = true;
3119    };
3120
3121    selProto.collapseToStart = function() {
3122        if (this.rangeCount) {
3123            var range = this._ranges[0];
3124            this.collapse(range.startContainer, range.startOffset);
3125        } else {
3126            throw new DOMException("INVALID_STATE_ERR");
3127        }
3128    };
3129
3130    selProto.collapseToEnd = function() {
3131        if (this.rangeCount) {
3132            var range = this._ranges[this.rangeCount - 1];
3133            this.collapse(range.endContainer, range.endOffset);
3134        } else {
3135            throw new DOMException("INVALID_STATE_ERR");
3136        }
3137    };
3138
3139    // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
3140    // never used by Rangy.
3141    selProto.selectAllChildren = function(node) {
3142        assertNodeInSameDocument(this, node);
3143        var range = api.createRange(dom.getDocument(node));
3144        range.selectNodeContents(node);
3145        this.removeAllRanges();
3146        this.addRange(range);
3147    };
3148
3149    selProto.deleteFromDocument = function() {
3150        // Sepcial behaviour required for Control selections
3151        if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3152            var controlRange = this.docSelection.createRange();
3153            var element;
3154            while (controlRange.length) {
3155                element = controlRange.item(0);
3156                controlRange.remove(element);
3157                element.parentNode.removeChild(element);
3158            }
3159            this.refresh();
3160        } else if (this.rangeCount) {
3161            var ranges = this.getAllRanges();
3162            this.removeAllRanges();
3163            for (var i = 0, len = ranges.length; i < len; ++i) {
3164                ranges[i].deleteContents();
3165            }
3166            // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
3167            // range. Firefox moves the selection to where the final selected range was, so we emulate that
3168            this.addRange(ranges[len - 1]);
3169        }
3170    };
3171
3172    // The following are non-standard extensions
3173    selProto.getAllRanges = function() {
3174        return this._ranges.slice(0);
3175    };
3176
3177    selProto.setSingleRange = function(range) {
3178        this.setRanges( [range] );
3179    };
3180
3181    selProto.containsNode = function(node, allowPartial) {
3182        for (var i = 0, len = this._ranges.length; i < len; ++i) {
3183            if (this._ranges[i].containsNode(node, allowPartial)) {
3184                return true;
3185            }
3186        }
3187        return false;
3188    };
3189
3190    selProto.toHtml = function() {
3191        var html = "";
3192        if (this.rangeCount) {
3193            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
3194            for (var i = 0, len = this._ranges.length; i < len; ++i) {
3195                container.appendChild(this._ranges[i].cloneContents());
3196            }
3197            html = container.innerHTML;
3198        }
3199        return html;
3200    };
3201
3202    function inspect(sel) {
3203        var rangeInspects = [];
3204        var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3205        var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3206        var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3207
3208        if (typeof sel.rangeCount != "undefined") {
3209            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3210                rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3211            }
3212        }
3213        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3214                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3215
3216    }
3217
3218    selProto.getName = function() {
3219        return "WrappedSelection";
3220    };
3221
3222    selProto.inspect = function() {
3223        return inspect(this);
3224    };
3225
3226    selProto.detach = function() {
3227        this.win[windowPropertyName] = null;
3228        this.win = this.anchorNode = this.focusNode = null;
3229    };
3230
3231    WrappedSelection.inspect = inspect;
3232
3233    api.Selection = WrappedSelection;
3234
3235    api.selectionPrototype = selProto;
3236
3237    api.addCreateMissingNativeApiListener(function(win) {
3238        if (typeof win.getSelection == "undefined") {
3239            win.getSelection = function() {
3240                return api.getSelection(this);
3241            };
3242        }
3243        win = null;
3244    });
3245});
3246/*
3247	Base.js, version 1.1a
3248	Copyright 2006-2010, Dean Edwards
3249	License: http://www.opensource.org/licenses/mit-license.php
3250*/
3251
3252var Base = function() {
3253	// dummy
3254};
3255
3256Base.extend = function(_instance, _static) { // subclass
3257	var extend = Base.prototype.extend;
3258	
3259	// build the prototype
3260	Base._prototyping = true;
3261	var proto = new this;
3262	extend.call(proto, _instance);
3263  proto.base = function() {
3264    // call this method from any other method to invoke that method's ancestor
3265  };
3266	delete Base._prototyping;
3267	
3268	// create the wrapper for the constructor function
3269	//var constructor = proto.constructor.valueOf(); //-dean
3270	var constructor = proto.constructor;
3271	var klass = proto.constructor = function() {
3272		if (!Base._prototyping) {
3273			if (this._constructing || this.constructor == klass) { // instantiation
3274				this._constructing = true;
3275				constructor.apply(this, arguments);
3276				delete this._constructing;
3277			} else if (arguments[0] != null) { // casting
3278				return (arguments[0].extend || extend).call(arguments[0], proto);
3279			}
3280		}
3281	};
3282	
3283	// build the class interface
3284	klass.ancestor = this;
3285	klass.extend = this.extend;
3286	klass.forEach = this.forEach;
3287	klass.implement = this.implement;
3288	klass.prototype = proto;
3289	klass.toString = this.toString;
3290	klass.valueOf = function(type) {
3291		//return (type == "object") ? klass : constructor; //-dean
3292		return (type == "object") ? klass : constructor.valueOf();
3293	};
3294	extend.call(klass, _static);
3295	// class initialisation
3296	if (typeof klass.init == "function") klass.init();
3297	return klass;
3298};
3299
3300Base.prototype = {	
3301	extend: function(source, value) {
3302		if (arguments.length > 1) { // extending with a name/value pair
3303			var ancestor = this[source];
3304			if (ancestor && (typeof value == "function") && // overriding a method?
3305				// the valueOf() comparison is to avoid circular references
3306				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
3307				/\bbase\b/.test(value)) {
3308				// get the underlying method
3309				var method = value.valueOf();
3310				// override
3311				value = function() {
3312					var previous = this.base || Base.prototype.base;
3313					this.base = ancestor;
3314					var returnValue = method.apply(this, arguments);
3315					this.base = previous;
3316					return returnValue;
3317				};
3318				// point to the underlying method
3319				value.valueOf = function(type) {
3320					return (type == "object") ? value : method;
3321				};
3322				value.toString = Base.toString;
3323			}
3324			this[source] = value;
3325		} else if (source) { // extending with an object literal
3326			var extend = Base.prototype.extend;
3327			// if this object has a customised extend method then use it
3328			if (!Base._prototyping && typeof this != "function") {
3329				extend = this.extend || extend;
3330			}
3331			var proto = {toSource: null};
3332			// do the "toString" and other methods manually
3333			var hidden = ["constructor", "toString", "valueOf"];
3334			// if we are prototyping then include the constructor
3335			var i = Base._prototyping ? 0 : 1;
3336			while (key = hidden[i++]) {
3337				if (source[key] != proto[key]) {
3338					extend.call(this, key, source[key]);
3339
3340				}
3341			}
3342			// copy each of the source object's properties to this object
3343			for (var key in source) {
3344				if (!proto[key]) extend.call(this, key, source[key]);
3345			}
3346		}
3347		return this;
3348	}
3349};
3350
3351// initialise
3352Base = Base.extend({
3353	constructor: function() {
3354		this.extend(arguments[0]);
3355	}
3356}, {
3357	ancestor: Object,
3358	version: "1.1",
3359	
3360	forEach: function(object, block, context) {
3361		for (var key in object) {
3362			if (this.prototype[key] === undefined) {
3363				block.call(context, object[key], key, object);
3364			}
3365		}
3366	},
3367		
3368	implement: function() {
3369		for (var i = 0; i < arguments.length; i++) {
3370			if (typeof arguments[i] == "function") {
3371				// if it's a function, call it
3372				arguments[i](this.prototype);
3373			} else {
3374				// add the interface using the extend method
3375				this.prototype.extend(arguments[i]);
3376			}
3377		}
3378		return this;
3379	},
3380	
3381	toString: function() {
3382		return String(this.valueOf());
3383	}
3384});/**
3385 * Detect browser support for specific features
3386 */
3387wysihtml5.browser = (function() {
3388  var userAgent   = navigator.userAgent,
3389      testElement = document.createElement("div"),
3390      // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
3391      isIE        = userAgent.indexOf("MSIE")         !== -1 && userAgent.indexOf("Opera") === -1,
3392      isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1,
3393      isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1,
3394      isChrome    = userAgent.indexOf("Chrome/")      !== -1,
3395      isOpera     = userAgent.indexOf("Opera/")       !== -1;
3396  
3397  function iosVersion(userAgent) {
3398    return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1];
3399  }
3400  
3401  return {
3402    // Static variable needed, publicly accessible, to be able override it in unit tests
3403    USER_AGENT: userAgent,
3404    
3405    /**
3406     * Exclude browsers that are not capable of displaying and handling
3407     * contentEditable as desired:
3408     *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
3409     *    - IE < 8 create invalid markup and crash randomly from time to time
3410     *
3411     * @return {Boolean}
3412     */
3413    supported: function() {
3414      var userAgent                   = this.USER_AGENT.toLowerCase(),
3415          // Essential for making html elements editable
3416          hasContentEditableSupport   = "contentEditable" in testElement,
3417          // Following methods are needed in order to interact with the contentEditable area
3418          hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
3419          // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
3420          hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
3421          // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
3422          isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
3423      
3424      return hasContentEditableSupport
3425        && hasEditingApiSupport
3426        && hasQuerySelectorSupport
3427        && !isIncompatibleMobileBrowser;
3428    },
3429    
3430    isTouchDevice: function() {
3431      return this.supportsEvent("touchmove");
3432    },
3433    
3434    isIos: function() {
3435      var userAgent = this.USER_AGENT.toLowerCase();
3436      return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1;
3437    },
3438    
3439    /**
3440     * Whether the browser supports sandboxed iframes
3441     * Currently only IE 6+ offers such feature <iframe security="restricted">
3442     *
3443     * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
3444     * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
3445     *
3446     * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
3447     */
3448    supportsSandboxedIframes: function() {
3449      return isIE;
3450    },
3451
3452    /**
3453     * IE6+7 throw a mixed content warning when the src of an iframe
3454     * is empty/unset or about:blank
3455     * window.querySelector is implemented as of IE8
3456     */
3457    throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
3458      return !("querySelector" in document);
3459    },
3460
3461    /**
3462     * Whether the caret is correctly displayed in contentEditable elements
3463     * Firefox sometimes shows a huge caret in the beginning after focusing
3464     */
3465    displaysCaretInEmptyContentEditableCorrectly: function() {
3466      return !isGecko;
3467    },
3468
3469    /**
3470     * Opera and IE are the only browsers who offer the css value
3471     * in the original unit, thx to the currentStyle object
3472     * All other browsers provide the computed style in px via window.getComputedStyle
3473     */
3474    hasCurrentStyleProperty: function() {
3475      return "currentStyle" in testElement;
3476    },
3477
3478    /**
3479     * Whether the browser inserts a <br> when pressing enter in a contentEditable element
3480     */
3481    insertsLineBreaksOnReturn: function() {
3482      return isGecko;
3483    },
3484
3485    supportsPlaceholderAttributeOn: function(element) {
3486      return "placeholder" in element;
3487    },
3488
3489    supportsEvent: function(eventName) {
3490      return "on" + eventName in testElement || (function() {
3491        testElement.setAttribute("on" + eventName, "return;");
3492        return typeof(testElement["on" + eventName]) === "function";
3493      })();
3494    },
3495
3496    /**
3497     * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
3498     */
3499    supportsEventsInIframeCorrectly: function() {
3500      return !isOpera;
3501    },
3502
3503    /**
3504     * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled
3505     * with event.preventDefault
3506     * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs
3507     * to be cancelled
3508     */
3509    firesOnDropOnlyWhenOnDragOverIsCancelled: function() {
3510      return isWebKit || isGecko;
3511    },
3512    
3513    /**
3514     * Whether the browser supports the event.dataTransfer property in a proper way
3515     */
3516    supportsDataTransfer: function() {
3517      try {
3518        // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does)
3519        return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData;
3520      } catch(e) {
3521        return false;
3522      }
3523    },
3524
3525    /**
3526     * Everything below IE9 doesn't know how to treat HTML5 tags
3527     *
3528     * @param {Object} context The document object on which to check HTML5 support
3529     *
3530     * @example
3531     *    wysihtml5.browser.supportsHTML5Tags(document);
3532     */
3533    supportsHTML5Tags: function(context) {
3534      var element = context.createElement("div"),
3535          html5   = "<article>foo</article>";
3536      element.innerHTML = html5;
3537      return element.innerHTML.toLowerCase() === html5;
3538    },
3539
3540    /**
3541     * Checks whether a document supports a certain queryCommand
3542     * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
3543     * in oder to report correct results
3544     *
3545     * @param {Object} doc Document object on which to check for a query command
3546     * @param {String} command The query command to check for
3547     * @return {Boolean}
3548     *
3549     * @example
3550     *    wysihtml5.browser.supportsCommand(document, "bold");
3551     */
3552    supportsCommand: (function() {
3553      // Following commands are supported but contain bugs in some browsers
3554      var buggyCommands = {
3555        // formatBlock fails with some tags (eg. <blockquote>)
3556        "formatBlock":          isIE,
3557         // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
3558         // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
3559         // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
3560        "insertUnorderedList":  isIE || isOpera || isWebKit,
3561        "insertOrderedList":    isIE || isOpera || isWebKit
3562      };
3563      
3564      // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
3565      var supported = {
3566        "insertHTML": isGecko
3567      };
3568
3569      return function(doc, command) {
3570        var isBuggy = buggyCommands[command];
3571        if (!isBuggy) {
3572          // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
3573          try {
3574            return doc.queryCommandSupported(command);
3575          } catch(e1) {}
3576
3577          try {
3578            return doc.queryCommandEnabled(command);
3579          } catch(e2) {
3580            return !!supported[command];
3581          }
3582        }
3583        return false;
3584      };
3585    })(),
3586
3587    /**
3588     * IE: URLs starting with:
3589     *    www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
3590     *    nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
3591     * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
3592     * space bar when the caret is directly after such an url.
3593     * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
3594     * (related blog post on msdn
3595     * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
3596     */
3597    doesAutoLinkingInContentEditable: function() {
3598      return isIE;
3599    },
3600
3601    /**
3602     * As stated above, IE auto links urls typed into contentEditable elements
3603     * Since IE9 it's possible to prevent this behavior
3604     */
3605    canDisableAutoLinking: function() {
3606      return this.supportsCommand(document, "AutoUrlDetect");
3607    },
3608
3609    /**
3610     * IE leaves an empty paragraph in the contentEditable element after clearing it
3611     * Chrome/Safari sometimes an empty <div>
3612     */
3613    clearsContentEditableCorrectly: function() {
3614      return isGecko || isOpera || isWebKit;
3615    },
3616
3617    /**
3618     * IE gives wrong results for getAttribute
3619     */
3620    supportsGetAttributeCorrectly: function() {
3621      var td = document.createElement("td");
3622      return td.getAttribute("rowspan") != "1";
3623    },
3624
3625    /**
3626     * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
3627     * Chrome and Safari both don't support this
3628     */
3629    canSelectImagesInContentEditable: function() {
3630      return isGecko || isIE || isOpera;
3631    },
3632
3633    /**
3634     * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container
3635     * pressing backspace doesn't remove the entire list as done in other browsers
3636     */
3637    clearsListsInContentEditableCorrectly: function() {
3638      return isGecko || isIE || isWebKit;
3639    },
3640
3641    /**
3642     * All browsers except Safari and Chrome automatically scroll the range/caret position into view
3643     */
3644    autoScrollsToCaret: function() {
3645      return !isWebKit;
3646    },
3647
3648    /**
3649     * Check whether the browser automatically closes tags that don't need to be opened
3650     */
3651    autoClosesUnclosedTags: function() {
3652      var clonedTestElement = testElement.cloneNode(false),
3653          returnValue,
3654          innerHTML;
3655
3656      clonedTestElement.innerHTML = "<p><div></div>";
3657      innerHTML                   = clonedTestElement.innerHTML.toLowerCase();
3658      returnValue                 = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
3659
3660      // Cache result by overwriting current function
3661      this.autoClosesUnclosedTags = function() { return returnValue; };
3662
3663      return returnValue;
3664    },
3665
3666    /**
3667     * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
3668     */
3669    supportsNativeGetElementsByClassName: function() {
3670      return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
3671    },
3672
3673    /**
3674     * As of now (19.04.2011) only supported by Firefox 4 and Chrome
3675     * See https://developer.mozilla.org/en/DOM/Selection/modify
3676     */
3677    supportsSelectionModify: function() {
3678      return "getSelection" in window && "modify" in window.getSelection();
3679    },
3680    
3681    /**
3682     * Whether the browser supports the classList object for fast className manipulation
3683     * See https://developer.mozilla.org/en/DOM/element.classList
3684     */
3685    supportsClassList: function() {
3686      return "classList" in testElement;
3687    },
3688    
3689    /**
3690     * Opera needs a white space after a <br> in order to position the caret correctly
3691     */
3692    needsSpaceAfterLineBreak: function() {
3693      return isOpera;
3694    },
3695    
3696    /**
3697     * Whether the browser supports the speech api on the given element
3698     * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
3699     *
3700     * @example
3701     *    var input = document.createElement("input");
3702     *    if (wysihtml5.browser.supportsSpeechApiOn(input)) {
3703     *      // ...
3704     *    }
3705     */
3706    supportsSpeechApiOn: function(input) {
3707      var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0];
3708      return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
3709    },
3710    
3711    /**
3712     * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
3713     * See https://connect.microsoft.com/ie/feedback/details/650112
3714     * or try the POC http://tifftiff.de/ie9_crash/
3715     */
3716    crashesWhenDefineProperty: function(property) {
3717      return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest");
3718    },
3719    
3720    /**
3721     * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
3722     */
3723    doesAsyncFocus: function() {
3724      return isIE;
3725    },
3726    
3727    /**
3728     * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document
3729     */
3730    hasProblemsSettingCaretAfterImg: function() {
3731      return isIE;
3732    },
3733    
3734    hasUndoInContextMenu: function() {
3735      return isGecko || isChrome || isOpera;
3736    }
3737  };
3738})();wysihtml5.lang.array = function(arr) {
3739  return {
3740    /**
3741     * Check whether a given object exists in an array
3742     *
3743     * @example
3744     *    wysihtml5.lang.array([1, 2]).contains(1);
3745     *    // => true
3746     */
3747    contains: function(needle) {
3748      if (arr.indexOf) {
3749        return arr.indexOf(needle) !== -1;
3750      } else {
3751        for (var i=0, length=arr.length; i<length; i++) {
3752          if (arr[i] === needle) { return true; }
3753        }
3754        return false;
3755      }
3756    },
3757    
3758    /**
3759     * Substract one array from another
3760     *
3761     * @example
3762     *    wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
3763     *    // => [1, 2]
3764     */
3765    without: function(arrayToSubstract) {
3766      arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
3767      var newArr  = [],
3768          i       = 0,
3769          length  = arr.length;
3770      for (; i<length; i++) {
3771        if (!arrayToSubstract.contains(arr[i])) {
3772          newArr.push(arr[i]);
3773        }
3774      }
3775      return newArr;
3776    },
3777    
3778    /**
3779     * Return a clean native array
3780     * 
3781     * Following will convert a Live NodeList to a proper Array
3782     * @example
3783     *    var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
3784     */
3785    get: function() {
3786      var i        = 0,
3787          length   = arr.length,
3788          newArray = [];
3789      for (; i<length; i++) {
3790        newArray.push(arr[i]);
3791      }
3792      return newArray;
3793    }
3794  };
3795};wysihtml5.lang.Dispatcher = Base.extend(
3796  /** @scope wysihtml5.lang.Dialog.prototype */ {
3797  observe: function(eventName, handler) {
3798    this.events = this.events || {};
3799    this.events[eventName] = this.events[eventName] || [];
3800    this.events[eventName].push(handler);
3801    return this;
3802  },
3803
3804  on: function() {
3805    return this.observe.apply(this, wysihtml5.lang.array(arguments).get());
3806  },
3807
3808  fire: function(eventName, payload) {
3809    this.events = this.events || {};
3810    var handlers = this.events[eventName] || [],
3811        i        = 0;
3812    for (; i<handlers.length; i++) {
3813      handlers[i].call(this, payload);
3814    }
3815    return this;
3816  },
3817
3818  stopObserving: function(eventName, handler) {
3819    this.events = this.events || {};
3820    var i = 0,
3821        handlers,
3822        newHandlers;
3823    if (eventName) {
3824      handlers    = this.events[eventName] || [],
3825      newHandlers = [];
3826      for (; i<handlers.length; i++) {
3827        if (handlers[i] !== handler && handler) {
3828          newHandlers.push(handlers[i]);
3829        }
3830      }
3831      this.events[eventName] = newHandlers;
3832    } else {
3833      // Clean up all events
3834      this.events = {};
3835    }
3836    return this;
3837  }
3838});wysihtml5.lang.object = function(obj) {
3839  return {
3840    /**
3841     * @example
3842     *    wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
3843     *    // => { foo: 1, bar: 2, baz: 3 }
3844     */
3845    merge: function(otherObj) {
3846      for (var i in otherObj) {
3847        obj[i] = otherObj[i];
3848      }
3849      return this;
3850    },
3851    
3852    get: function() {
3853      return obj;
3854    },
3855    
3856    /**
3857     * @example
3858     *    wysihtml5.lang.object({ foo: 1 }).clone();
3859     *    // => { foo: 1 }
3860     */
3861    clone: function() {
3862      var newObj = {},
3863          i;
3864      for (i in obj) {
3865        newObj[i] = obj[i];
3866      }
3867      return newObj;
3868    },
3869    
3870    /**
3871     * @example
3872     *    wysihtml5.lang.object([]).isArray();
3873     *    // => true
3874     */
3875    isArray: function() {
3876      return Object.prototype.toString.call(obj) === "[object Array]";
3877    }
3878  };
3879};(function() {
3880  var WHITE_SPACE_START = /^\s+/,
3881      WHITE_SPACE_END   = /\s+$/;
3882  wysihtml5.lang.string = function(str) {
3883    str = String(str);
3884    return {
3885      /**
3886       * @example
3887       *    wysihtml5.lang.string("   foo   ").trim();
3888       *    // => "foo"
3889       */
3890      trim: function() {
3891        return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
3892      },
3893      
3894      /**
3895       * @example
3896       *    wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
3897       *    // => "Hello Christopher"
3898       */
3899      interpolate: function(vars) {
3900        for (var i in vars) {
3901          str = this.replace("#{" + i + "}").by(vars[i]);
3902        }
3903        return str;
3904      },
3905      
3906      /**
3907       * @example
3908       *    wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
3909       *    // => "Hello Hans"
3910       */
3911      replace: function(search) {
3912        return {
3913          by: function(replace) {
3914            return str.split(search).join(replace);
3915          }
3916        }
3917      }
3918    };
3919  };
3920})();/**
3921 * Find urls in descendant text nodes of an element and auto-links them
3922 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
3923 *
3924 * @param {Element} element Container element in which to search for urls
3925 *
3926 * @example
3927 *    <div id="text-container">Please click here: www.google.com</div>
3928 *    <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
3929 */
3930(function(wysihtml5) {
3931  var /**
3932       * Don't auto-link urls that are contained in the following elements:
3933       */
3934      IGNORE_URLS_IN        = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
3935      /**
3936       * revision 1:
3937       *    /(\S+\.{1}[^\s\,\.\!]+)/g
3938       *
3939       * revision 2:
3940       *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
3941       *
3942       * put this in the beginning if you don't wan't to match within a word
3943       *    (^|[\>\(\{\[\s\>])
3944       */
3945      URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
3946      TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
3947      MAX_DISPLAY_LENGTH    = 100,
3948      BRACKETS              = { ")": "(", "]": "[", "}": "{" };
3949  
3950  function autoLink(element) {
3951    if (_hasParentThatShouldBeIgnored(element)) {
3952      return element;
3953    }
3954
3955    if (element === element.ownerDocument.documentElement) {
3956      element = element.ownerDocument.body;
3957    }
3958
3959    return _parseNode(element);
3960  }
3961  
3962  /**
3963   * This is basically a rebuild of
3964   * the rails auto_link_urls text helper
3965   */
3966  function _convertUrlsToLinks(str) {
3967    return str.replace(URL_REG_EXP, function(match, url) {
3968      var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
3969          opening     = BRACKETS[punctuation];
3970      url = url.replace(TRAILING_CHAR_REG_EXP, "");
3971
3972      if (url.split(opening).length > url.split(punctuation).length) {
3973        url = url + punctuation;
3974        punctuation = "";
3975      }
3976      var realUrl    = url,
3977          displayUrl = url;
3978      if (url.length > MAX_DISPLAY_LENGTH) {
3979        displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
3980      }
3981      // Add http prefix if necessary
3982      if (realUrl.substr(0, 4) === "www.") {
3983        realUrl = "http://" + realUrl;
3984      }
3985      
3986      return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
3987    });
3988  }
3989  
3990  /**
3991   * Creates or (if already cached) returns a temp element
3992   * for the given document object
3993   */
3994  function _getTempElement(context) {
3995    var tempElement = context._wysihtml5_tempElement;
3996    if (!tempElement) {
3997      tempElement = context._wysihtml5_tempElement = context.createElement("div");
3998    }
3999    return tempElement;
4000  }
4001  
4002  /**
4003   * Replaces the original text nodes with the newly auto-linked dom tree
4004   */
4005  function _wrapMatchesInNode(textNode) {
4006    var parentNode  = textNode.parentNode,
4007        tempElement = _getTempElement(parentNode.ownerDocument);
4008    
4009    // We need to insert an empty/temporary <span /> to fix IE quirks
4010    // Elsewise IE would strip white space in the beginning
4011    tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data);
4012    tempElement.removeChild(tempElement.firstChild);
4013    
4014    while (tempElement.firstChild) {
4015      // inserts tempElement.firstChild before textNode
4016      parentNode.insertBefore(tempElement.firstChild, textNode);
4017    }
4018    parentNode.removeChild(textNode);
4019  }
4020  
4021  function _hasParentThatShouldBeIgnored(node) {
4022    var nodeName;
4023    while (node.parentNode) {
4024      node = node.parentNode;
4025      nodeName = node.nodeName;
4026      if (IGNORE_URLS_IN.contains(nodeName)) {
4027        return true;
4028      } else if (nodeName === "body") {
4029        return false;
4030      }
4031    }
4032    return false;
4033  }
4034  
4035  function _parseNode(element) {
4036    if (IGNORE_URLS_IN.contains(element.nodeName)) {
4037      return;
4038    }
4039    
4040    if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
4041      _wrapMatchesInNode(element);
4042      return;
4043    }
4044    
4045    var childNodes        = wysihtml5.lang.array(element.childNodes).get(),
4046        childNodesLength  = childNodes.length,
4047        i                 = 0;
4048    
4049    for (; i<childNodesLength; i++) {
4050      _parseNode(childNodes[i]);
4051    }
4052    
4053    return element;
4054  }
4055  
4056  wysihtml5.dom.autoLink = autoLink;
4057  
4058  // Reveal url reg exp to the outside
4059  wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
4060})(wysihtml5);(function(wysihtml5) {
4061  var supportsClassList = wysihtml5.browser.supportsClassList(),
4062      api               = wysihtml5.dom;
4063  
4064  api.addClass = function(element, className) {
4065    if (supportsClassList) {
4066      return element.classList.add(className);
4067    }
4068    if (api.hasClass(element, className)) {
4069      return;
4070    }
4071    element.className += " " + className;
4072  };
4073  
4074  api.removeClass = function(element, className) {
4075    if (supportsClassList) {
4076      return element.classList.remove(className);
4077    }
4078    
4079    element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
4080  };
4081  
4082  api.hasClass = function(element, className) {
4083    if (supportsClassList) {
4084      return element.classList.contains(className);
4085    }
4086    
4087    var elementClassName = element.className;
4088    return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
4089  };
4090})(wysihtml5);
4091wysihtml5.dom.contains = (function() {
4092  var documentElement = document.documentElement;
4093  if (documentElement.contains) {
4094    return function(container, element) {
4095      if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
4096        element = element.parentNode;
4097      }
4098      return container !== element && container.contains(element);
4099    };
4100  } else if (documentElement.compareDocumentPosition) {
4101    return function(container, element) {
4102      // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
4103      return !!(container.compareDocumentPosition(element) & 16);
4104    };
4105  }
4106})();/**
4107 * Converts an HTML fragment/element into a unordered/ordered list
4108 *
4109 * @param {Element} element The element which should be turned into a list
4110 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
4111 * @return {Element} The created list
4112 *
4113 * @example
4114 *    <!-- Assume the following dom: -->
4115 *    <span id="pseudo-list">
4116 *      eminem<br>
4117 *      dr. dre
4118 *      <div>50 Cent</div>
4119 *    </span>
4120 *
4121 *    <script>
4122 *      wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
4123 *    </script>
4124 *
4125 *    <!-- Will result in: -->
4126 *    <ul>
4127 *      <li>eminem</li>
4128 *      <li>dr. dre</li>
4129 *      <li>50 Cent</li>
4130 *    </ul>
4131 */
4132wysihtml5.dom.convertToList = (function() {
4133  function _createListItem(doc, list) {
4134    var listItem = doc.createElement("li");
4135    list.appendChild(listItem);
4136    return listItem;
4137  }
4138  
4139  function _createList(doc, type) {
4140    return doc.createElement(type);
4141  }
4142  
4143  function convertToList(element, listType) {
4144    if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
4145      // Already a list
4146      return element;
4147    }
4148    
4149    var doc               = element.ownerDocument,
4150        list              = _createList(doc, listType),
4151        lineBreaks        = element.querySelectorAll("br"),
4152        lineBreaksLength  = lineBreaks.length,
4153        childNodes,
4154        childNodesLength,
4155        childNode,
4156        lineBreak,
4157        parentNode,
4158        isBlockElement,
4159        isLineBreak,
4160        currentListItem,
4161        i;
4162    
4163    // First find <br> at the end of inline elements and move them behind them
4164    for (i=0; i<lineBreaksLength; i++) {
4165      lineBreak = lineBreaks[i];
4166      while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
4167        if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
4168          parentNode.removeChild(lineBreak);
4169          break;
4170        }
4171        wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
4172      }
4173    }
4174    
4175    childNodes        = wysihtml5.lang.array(element.childNodes).get();
4176    childNodesLength  = childNodes.length;
4177    
4178    for (i=0; i<childNodesLength; i++) {
4179      currentListItem   = currentListItem || _createListItem(doc, list);
4180      childNode         = childNodes[i];
4181      isBlockElement    = wysihtml5.dom.getStyle("display").from(childNode) === "block";
4182      isLineBreak       = childNode.nodeName === "BR";
4183      
4184      if (isBlockElement) {
4185        // Append blockElement to current <li> if empty, otherwise create a new one
4186        currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
4187        currentListItem.appendChild(childNode);
4188        currentListItem = null;
4189        continue;
4190      }
4191      
4192      if (isLineBreak) {
4193        // Only create a new list item in the next iteration when the current one has already content
4194        currentListItem = currentListItem.firstChild ? null : currentListItem;
4195        continue;
4196      }
4197      
4198      currentListItem.appendChild(childNode);
4199    }
4200    
4201    element.parentNode.replaceChild(list, element);
4202    return list;
4203  }
4204  
4205  return convertToList;
4206})();/**
4207 * Copy a set of attributes from one element to another
4208 *
4209 * @param {Array} attributesToCopy List of attributes which should be copied
4210 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4211 *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked 
4212 *    with the element where to copy the attributes to (see example)
4213 *
4214 * @example
4215 *    var textarea    = document.querySelector("textarea"),
4216 *        div         = document.querySelector("div[contenteditable=true]"),
4217 *        anotherDiv  = document.querySelector("div.preview");
4218 *    wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
4219 *
4220 */
4221wysihtml5.dom.copyAttributes = function(attributesToCopy) {
4222  return {
4223    from: function(elementToCopyFrom) {
4224      return {
4225        to: function(elementToCopyTo) {
4226          var attribute,
4227              i         = 0,
4228              length    = attributesToCopy.length;
4229          for (; i<length; i++) {
4230            attribute = attributesToCopy[i];
4231            if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
4232              elementToCopyTo[attribute] = elementToCopyFrom[attribute];
4233            }
4234          }
4235          return { andTo: arguments.callee };
4236        }
4237      };
4238    }
4239  };
4240};/**
4241 * Copy a set of styles from one element to another
4242 * Please note that this only works properly across browsers when the element from which to copy the styles
4243 * is in the dom
4244 *
4245 * Interesting article on how to copy styles
4246 *
4247 * @param {Array} stylesToCopy List of styles which should be copied
4248 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
4249 *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked 
4250 *    with the element where to copy the styles to (see example)
4251 *
4252 * @example
4253 *    var textarea    = document.querySelector("textarea"),
4254 *        div         = document.querySelector("div[contenteditable=true]"),
4255 *        anotherDiv  = document.querySelector("div.preview");
4256 *    wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
4257 *
4258 */
4259(function(dom) {
4260  
4261  /**
4262   * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
4263   * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then 
4264   * its computed css width will be 198px
4265   */
4266  var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
4267  
4268  var shouldIgnoreBoxSizingBorderBox = function(element) {
4269    if (hasBoxSizingBorderBox(element)) {
4270       return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
4271    }
4272    return false;
4273  };
4274  
4275  var hasBoxSizingBorderBox = function(element) {
4276    var i       = 0,
4277        length  = BOX_SIZING_PROPERTIES.length;
4278    for (; i<length; i++) {
4279      if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
4280        return BOX_SIZING_PROPERTIES[i];
4281      }
4282    }
4283  };
4284  
4285  dom.copyStyles = function(stylesToCopy) {
4286    return {
4287      from: function(element) {
4288        if (shouldIgnoreBoxSizingBorderBox(element)) {
4289          stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
4290        }
4291        
4292        var cssText = "",
4293            length  = stylesToCopy.length,
4294            i       = 0,
4295            property;
4296        for (; i<length; i++) {
4297          property = stylesToCopy[i];
4298          cssText += property + ":" + dom.getStyle(property).from(element) + ";";
4299        }
4300        
4301        return {
4302          to: function(element) {
4303            dom.setStyles(cssText).on(element);
4304            return { andTo: arguments.callee };
4305          }
4306        };
4307      }
4308    };
4309  };
4310})(wysihtml5.dom);/**
4311 * Event Delegation
4312 *
4313 * @example
4314 *    wysihtml5.dom.delegate(document.body, "a", "click", function() {
4315 *      // foo
4316 *    });
4317 */
4318(function(wysihtml5) {
4319  
4320  wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
4321    return wysihtml5.dom.observe(container, eventName, function(event) {
4322      var target    = event.target,
4323          match     = wysihtml5.lang.array(container.querySelectorAll(selector));
4324      
4325      while (target && target !== container) {
4326        if (match.contains(target)) {
4327          handler.call(target, event);
4328          break;
4329        }
4330        target = target.parentNode;
4331      }
4332    });
4333  };
4334  
4335})(wysihtml5);/**
4336 * Returns the given html wrapped in a div element
4337 *
4338 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
4339 * when inserted via innerHTML
4340 * 
4341 * @param {String} html The html which should be wrapped in a dom element
4342 * @param {Obejct} [context] Document object of the context the html belongs to
4343 *
4344 * @example
4345 *    wysihtml5.dom.getAsDom("<article>foo</article>");
4346 */
4347wysihtml5.dom.getAsDom = (function() {
4348  
4349  var _innerHTMLShiv = function(html, context) {
4350    var tempElement = context.createElement("div");
4351    tempElement.style.display = "none";
4352    context.body.appendChild(tempElement);
4353    // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
4354    try { tempElement.innerHTML = html; } catch(e) {}
4355    context.body.removeChild(tempElement);
4356    return tempElement;
4357  };
4358  
4359  /**
4360   * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
4361   */
4362  var _ensureHTML5Compatibility = function(context) {
4363    if (context._wysihtml5_supportsHTML5Tags) {
4364      return;
4365    }
4366    for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
4367      context.createElement(HTML5_ELEMENTS[i]);
4368    }
4369    context._wysihtml5_supportsHTML5Tags = true;
4370  };
4371  
4372  
4373  /**
4374   * List of html5 tags
4375   * taken from http://simon.html5.org/html5-elements
4376   */
4377  var HTML5_ELEMENTS = [
4378    "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
4379    "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
4380    "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
4381  ];
4382  
4383  return function(html, context) {
4384    context = context || document;
4385    var tempElement;
4386    if (typeof(html) === "object" && html.nodeType) {
4387      tempElement = context.createElement("div");
4388      tempElement.appendChild(html);
4389    } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
4390      tempElement = context.createElement("div");
4391      tempElement.innerHTML = html;
4392    } else {
4393      _ensureHTML5Compatibility(context);
4394      tempElement = _innerHTMLShiv(html, context);
4395    }
4396    return tempElement;
4397  };
4398})();/**
4399 * Walks the dom tree from the given node up until it finds a match
4400 * Designed for optimal performance.
4401 *
4402 * @param {Element} node The from which to check the parent nodes
4403 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
4404 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
4405 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
4406 * @example
4407 *    var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
4408 *    // ... or ...
4409 *    var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
4410 *    // ... or ...
4411 *    var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
4412 */
4413wysihtml5.dom.getParentElement = (function() {
4414  
4415  function _isSameNodeName(nodeName, desiredNodeNames) {
4416    if (!desiredNodeNames || !desiredNodeNames.length) {
4417      return true;
4418    }
4419    
4420    if (typeof(desiredNodeNames) === "string") {
4421      return nodeName === desiredNodeNames;
4422    } else {
4423      return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
4424    }
4425  }
4426  
4427  function _isElement(node) {
4428    return node.nodeType === wysihtml5.ELEMENT_NODE;
4429  }
4430  
4431  function _hasClassName(element, className, classRegExp) {
4432    var classNames = (element.className || "").match(classRegExp) || [];
4433    if (!className) {
4434      return !!classNames.length;
4435    }
4436    return classNames[classNames.length - 1] === className;
4437  }
4438  
4439  function _getParentElementWithNodeName(node, nodeName, levels) {
4440    while (levels-- && node && node.nodeName !== "BODY") {
4441      if (_isSameNodeName(node.nodeName, nodeName)) {
4442        return node;
4443      }
4444      node = node.parentNode;
4445    }
4446    return null;
4447  }
4448  
4449  function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) {
4450    while (levels-- && node && node.nodeName !== "BODY") {
4451      if (_isElement(node) &&
4452          _isSameNodeName(node.nodeName, nodeName) &&
4453          _hasClassName(node, className, classRegExp)) {
4454        return node;
4455      }
4456      node = node.parentNode;
4457    }
4458    return null;
4459  }
4460  
4461  return function(node, matchingSet, levels) {
4462    levels = levels || 50; // Go max 50 nodes upwards from current node
4463    if (matchingSet.className || matchingSet.classRegExp) {
4464      return _getParentElementWithNodeNameAndClassName(
4465        node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels
4466      );
4467    } else {
4468      return _getParentElementWithNodeName(
4469        node, matchingSet.nodeName, levels
4470      );
4471    }
4472  };
4473})();
4474/**
4475 * Get element's style for a specific css property
4476 *
4477 * @param {Element} element The element on which to retrieve the style
4478 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
4479 *
4480 * @example
4481 *    wysihtml5.dom.getStyle("display").from(document.body);
4482 *    // => "block"
4483 */
4484wysihtml5.dom.getStyle = (function() {
4485  var stylePropertyMapping = {
4486        "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
4487      },
4488      REG_EXP_CAMELIZE = /\-[a-z]/g;
4489  
4490  function camelize(str) {
4491    return str.replace(REG_EXP_CAMELIZE, function(match) {
4492      return match.charAt(1).toUpperCase();
4493    });
4494  }
4495  
4496  return function(property) {
4497    return {
4498      from: function(element) {
4499        if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
4500          return;
4501        }
4502        
4503        var doc               = element.ownerDocument,
4504            camelizedProperty = stylePropertyMapping[property] || camelize(property),
4505            style             = element.style,
4506            currentStyle      = element.currentStyle,
4507            styleValue        = style[camelizedProperty];
4508        if (styleValue) {
4509          return styleValue;
4510        }
4511        
4512        // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
4513        // window.getComputedStyle, since it returns css property values in their original unit:
4514        // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
4515        // gives you the original "50%".
4516        // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
4517        if (currentStyle) {
4518          try {
4519                return currentStyle[camelizedProperty];
4520          } catch(e) {
4521            //ie will occasionally fail for unknown reasons. swallowing exception
4522          }
4523        }
4524
4525        var win                 = doc.defaultView || doc.parentWindow,
4526            needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
4527            originalOverflow,
4528            returnValue;
4529
4530        if (win.getComputedStyle) {
4531          // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
4532          // therfore we remove and restore the scrollbar and calculate the value in between
4533          if (needsOverflowReset) {
4534            originalOverflow = style.overflow;
4535            style.overflow = "hidden";
4536          }
4537          returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
4538          if (needsOverflowReset) {
4539            style.overflow = originalOverflow || "";
4540          }
4541          return returnValue;
4542        }
4543      }
4544    };
4545  };
4546})();/**
4547 * High performant way to check whether an element with a specific tag name is in the given document
4548 * Optimized for being heavily executed
4549 * Unleashes the power of live node lists
4550 *
4551 * @param {Object} doc The document object of the context where to check
4552 * @param {String} tagName Upper cased tag name
4553 * @example
4554 *    wysihtml5.dom.hasElementWithTagName(document, "IMG");
4555 */
4556wysihtml5.dom.hasElementWithTagName = (function() {
4557  var LIVE_CACHE          = {},
4558      DOCUMENT_IDENTIFIER = 1;
4559  
4560  function _getDocumentIdentifier(doc) {
4561    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4562  }
4563  
4564  return function(doc, tagName) {
4565    var key         = _getDocumentIdentifier(doc) + ":" + tagName,
4566        cacheEntry  = LIVE_CACHE[key];
4567    if (!cacheEntry) {
4568      cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
4569    }
4570    
4571    return cacheEntry.length > 0;
4572  };
4573})();/**
4574 * High performant way to check whether an element with a specific class name is in the given document
4575 * Optimized for being heavily executed
4576 * Unleashes the power of live node lists
4577 *
4578 * @param {Object} doc The document object of the context where to check
4579 * @param {String} tagName Upper cased tag name
4580 * @example
4581 *    wysihtml5.dom.hasElementWithClassName(document, "foobar");
4582 */
4583(function(wysihtml5) {
4584  var LIVE_CACHE          = {},
4585      DOCUMENT_IDENTIFIER = 1;
4586
4587  function _getDocumentIdentifier(doc) {
4588    return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
4589  }
4590  
4591  wysihtml5.dom.hasElementWithClassName = function(doc, className) {
4592    // getElementsByClassName is not supported by IE<9
4593    // but is sometimes mocked via library code (which then doesn't return live node lists)
4594    if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
4595      return !!doc.querySelector("." + className);
4596    }
4597
4598    var key         = _getDocumentIdentifier(doc) + ":" + className,
4599        cacheEntry  = LIVE_CACHE[key];
4600    if (!cacheEntry) {
4601      cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
4602    }
4603
4604    return cacheEntry.length > 0;
4605  };
4606})(wysihtml5);
4607wysihtml5.dom.insert = function(elementToInsert) {
4608  return {
4609    after: function(element) {
4610      element.parentNode.insertBefore(elementToInsert, element.nextSibling);
4611    },
4612    
4613    before: function(element) {
4614      element.parentNode.insertBefore(elementToInsert, element);
4615    },
4616    
4617    into: function(element) {
4618      element.appendChild(elementToInsert);
4619    }
4620  };
4621};wysihtml5.dom.insertCSS = function(rules) {
4622  rules = rules.join("\n");
4623  
4624  return {
4625    into: function(doc) {
4626      var head         = doc.head || doc.getElementsByTagName("head")[0],
4627          styleElement = doc.createElement("style");
4628
4629      styleElement.type = "text/css";
4630
4631      if (styleElement.styleSheet) {
4632        styleElement.styleSheet.cssText = rules;
4633      } else {
4634        styleElement.appendChild(doc.createTextNode(rules));
4635      }
4636
4637      if (head) {
4638        head.appendChild(styleElement);
4639      }
4640    }
4641  };
4642};/**
4643 * Method to set dom events
4644 *
4645 * @example
4646 *    wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
4647 */
4648wysihtml5.dom.observe = function(element, eventNames, handler) {
4649  eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
4650  
4651  var handlerWrapper,
4652      eventName,
4653      i       = 0,
4654      length  = eventNames.length;
4655  
4656  for (; i<length; i++) {
4657    eventName = eventNames[i];
4658    if (element.addEventListener) {
4659      element.addEventListener(eventName, handler, false);
4660    } else {
4661      handlerWrapper = function(event) {
4662        if (!("target" in event)) {
4663          event.target = event.srcElement;
4664        }
4665        event.preventDefault = event.preventDefault || function() {
4666          this.returnValue = false;
4667        };
4668        event.stopPropagation = event.stopPropagation || function() {
4669          this.cancelBubble = true;
4670        };
4671        handler.call(element, event);
4672      };
4673      element.attachEvent("on" + eventName, handlerWrapper);
4674    }
4675  }
4676  
4677  return {
4678    stop: function() {
4679      var eventName,
4680          i       = 0,
4681          length  = eventNames.length;
4682      for (; i<length; i++) {
4683        eventName = eventNames[i];
4684        if (element.removeEventListener) {
4685          element.removeEventListener(eventName, handler, false);
4686        } else {
4687          element.detachEvent("on" + eventName, handlerWrapper);
4688        }
4689      }
4690    }
4691  };
4692};
4693/**
4694 * HTML Sanitizer
4695 * Rewrites the HTML based on given rules
4696 *
4697 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
4698 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
4699 *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
4700 *    desired substitution.
4701 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
4702 *
4703 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
4704 *
4705 * @example
4706 *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
4707 *    wysihtml5.dom.parse(userHTML, {
4708 *      tags {
4709 *        p:      "div",      // Rename p tags to div tags
4710 *        font:   "span"      // Rename font tags to span tags
4711 *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
4712 *        script: undefined   // Remove script elements
4713 *      }
4714 *    });
4715 *    // => <div><div><span>foo bar</span></div></div>
4716 *
4717 *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
4718 *    wysihtml5.dom.parse(userHTML);
4719 *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
4720 *
4721 *    var userHTML = '<div>foobar<br>foobar</div>';
4722 *    wysihtml5.dom.parse(userHTML, {
4723 *      tags: {
4724 *        div: undefined,
4725 *        br:  true
4726 *      }
4727 *    });
4728 *    // => ''
4729 *
4730 *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
4731 *    wysihtml5.dom.parse(userHTML, {
4732 *      classes: {
4733 *        red:    1,
4734 *        green:  1
4735 *      },
4736 *      tags: {
4737 *        div: {
4738 *          rename_tag:     "p"
4739 *        }
4740 *      }
4741 *    });
4742 *    // => '<p class="red">foo</p><p>bar</p>'
4743 */
4744wysihtml5.dom.parse = (function() {
4745  
4746  /**
4747   * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
4748   * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
4749   * node isn't closed
4750   *
4751   * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
4752   */
4753  var NODE_TYPE_MAPPING = {
4754        "1": _handleElement,
4755        "3": _handleText
4756      },
4757      // Rename unknown tags to this
4758      DEFAULT_NODE_NAME   = "span",
4759      WHITE_SPACE_REG_EXP = /\s+/,
4760      defaultRules        = { tags: {}, classes: {} },
4761      currentRules        = {};
4762  
4763  /**
4764   * Iterates over all childs of the element, recreates them, appends them into a document fragment
4765   * which later replaces the entire body content
4766   */
4767  function parse(elementOrHtml, rules, context, cleanUp) {
4768    wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get();
4769    
4770    context           = context || elementOrHtml.ownerDocument || document;
4771    var fragment      = context.createDocumentFragment(),
4772        isString      = typeof(elementOrHtml) === "string",
4773        element,
4774        newNode,
4775        firstChild;
4776    
4777    if (isString) {
4778      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
4779    } else {
4780      element = elementOrHtml;
4781    }
4782    
4783    while (element.firstChild) {
4784      firstChild  = element.firstChild;
4785      element.removeChild(firstChild);
4786      newNode = _convert(firstChild, cleanUp);
4787      if (newNode) {
4788        fragment.appendChild(newNode);
4789      }
4790    }
4791    
4792    // Clear element contents
4793    element.innerHTML = "";
4794    
4795    // Insert new DOM tree
4796    element.appendChild(fragment);
4797    
4798    return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
4799  }
4800  
4801  function _convert(oldNode, cleanUp) {
4802    var oldNodeType     = oldNode.nodeType,
4803        oldChilds       = oldNode.childNodes,
4804        oldChildsLength = oldChilds.length,
4805        newNode,
4806        method          = NODE_TYPE_MAPPING[oldNodeType],
4807        i               = 0;
4808    
4809    newNode = method && method(oldNode);
4810    
4811    if (!newNode) {
4812      return null;
4813    }
4814    
4815    for (i=0; i<oldChildsLength; i++) {
4816      newChild = _convert(oldChilds[i], cleanUp);
4817      if (newChild) {
4818        newNode.appendChild(newChild);
4819      }
4820    }
4821    
4822    // Cleanup senseless <span> elements
4823    if (cleanUp &&
4824        newNode.childNodes.length <= 1 &&
4825        newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
4826        !newNode.attributes.length) {
4827      return newNode.firstChild;
4828    }
4829    
4830    return newNode;
4831  }
4832  
4833  function _handleElement(oldNode) {
4834    var rule,
4835        newNode,
4836        endTag,
4837        tagRules    = currentRules.tags,
4838        nodeName    = oldNode.nodeName.toLowerCase(),
4839        scopeName   = oldNode.scopeName;
4840    
4841    /**
4842     * We already parsed that element
4843     * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
4844     */
4845    if (oldNode._wysihtml5) {
4846      return null;
4847    }
4848    oldNode._wysihtml5 = 1;
4849    
4850    if (oldNode.className === "wysihtml5-temp") {
4851      return null;
4852    }
4853    
4854    /**
4855     * IE is the only browser who doesn't include the namespace in the
4856     * nodeName, that's why we have to prepend it by ourselves
4857     * scopeName is a proprietary IE feature
4858     * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
4859     */
4860    if (scopeName && scopeName != "HTML") {
4861      nodeName = scopeName + ":" + nodeName;
4862    }
4863    
4864    /**
4865     * Repair node
4866     * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
4867     * 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
4868     */
4869    if ("outerHTML" in oldNode) {
4870      if (!wysihtml5.browser.autoClosesUnclosedTags() &&
4871          oldNode.nodeName === "P" &&
4872          oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
4873        nodeName = "div";
4874      }
4875    }
4876    
4877    if (nodeName in tagRules) {
4878      rule = tagRules[nodeName];
4879      if (!rule || rule.remove) {
4880        return null;
4881      }
4882      
4883      rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
4884    } else if (oldNode.firstChild) {
4885      rule = { rename_tag: DEFAULT_NODE_NAME };
4886    } else {
4887      // Remove empty unknown elements
4888      return null;
4889    }
4890    
4891    newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName);
4892    _handleAttributes(oldNode, newNode, rule);
4893    
4894    oldNode = null;
4895    return newNode;
4896  }
4897  
4898  function _handleAttributes(oldNode, newNode, rule) {
4899    var attributes          = {},                         // fresh new set of attributes to set on newNode
4900        setClass            = rule.set_class,             // classes to set
4901        addClass            = rule.add_class,             // add classes based on existing attributes
4902        setAttributes       = rule.set_attributes,        // attributes to set on the current node
4903        checkAttributes     = rule.check_attributes,      // check/convert values of attributes
4904        allowedClasses      = currentRules.classes,
4905        i                   = 0,
4906        classes             = [],
4907        newClasses          = [],
4908        newUniqueClasses    = [],
4909        oldClasses          = [],
4910        classesLength,
4911        newClassesLength,
4912        currentClass,
4913        newClass,
4914        attributeName,
4915        newAttributeValue,
4916        method;
4917    
4918    if (setAttributes) {
4919      attributes = wysihtml5.lang.object(setAttributes).clone();
4920    }
4921    
4922    if (checkAttributes) {
4923      for (attributeName in checkAttributes) {
4924        method = attributeCheckMethods[checkAttributes[attributeName]];
4925        if (!method) {
4926          continue;
4927        }
4928        newAttributeValue = method(_getAttribute(oldNode, attributeName));
4929        if (typeof(newAttributeValue) === "string") {
4930          attributes[attributeName] = newAttributeValue;
4931        }
4932      }
4933    }
4934    
4935    if (setClass) {
4936      classes.push(setClass);
4937    }
4938    
4939    if (addClass) {
4940      for (attributeName in addClass) {
4941        method = addClassMethods[addClass[attributeName]];
4942        if (!method) {
4943          continue;
4944        }
4945        newClass = method(_getAttribute(oldNode, attributeName));
4946        if (typeof(newClass) === "string") {
4947          classes.push(newClass);
4948        }
4949      }
4950    }
4951    
4952    // make sure that wysihtml5 temp class doesn't get stripped out
4953    allowedClasses["_wysihtml5-temp-placeholder"] = 1;
4954    
4955    // add old classes last
4956    oldClasses = oldNode.getAttribute("class");
4957    if (oldClasses) {
4958      classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
4959    }
4960    classesLength = classes.length;
4961    for (; i<classesLength; i++) {
4962      currentClass = classes[i];
4963      if (allowedClasses[currentClass]) {
4964        newClasses.push(currentClass);
4965      }
4966    }
4967    
4968    // remove duplicate entries and preserve class specificity
4969    newClassesLength = newClasses.length;
4970    while (newClassesLength--) {
4971      currentClass = newClasses[newClassesLength];
4972      if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) {
4973        newUniqueClasses.unshift(currentClass);
4974      }
4975    }
4976    
4977    if (newUniqueClasses.length) {
4978      attributes["class"] = newUniqueClasses.join(" ");
4979    }
4980    
4981    // set attributes on newNode
4982    for (attributeName in attributes) {
4983      // Setting attributes can cause a js error in IE under certain circumstances
4984      // eg. on a <img> under https when it's new attribute value is non-https
4985      // TODO: Investigate this further and check for smarter handling
4986      try {
4987        newNode.setAttribute(attributeName, attributes[attributeName]);
4988      } catch(e) {}
4989    }
4990    
4991    // IE8 sometimes loses the width/height attributes when those are set before the "src"
4992    // so we make sure to set them again
4993    if (attributes.src) {
4994      if (typeof(attributes.width) !== "undefined") {
4995        newNode.setAttribute("width", attributes.width);
4996      }
4997      if (typeof(attributes.height) !== "undefined") {
4998        newNode.setAttribute("height", attributes.height);
4999      }
5000    }
5001  }
5002  
5003  /**
5004   * IE gives wrong results for hasAttribute/getAttribute, for example:
5005   *    var td = document.createElement("td");
5006   *    td.getAttribute("rowspan"); // => "1" in IE
5007   *
5008   * Therefore we have to check the element's outerHTML for the attribute
5009   */
5010  var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
5011  function _getAttribute(node, attributeName) {
5012    attributeName = attributeName.toLowerCase();
5013    var nodeName = node.nodeName;
5014    if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) {
5015      // Get 'src' attribute value via object property since this will always contain the
5016      // full absolute url (http://...)
5017      // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
5018      // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
5019      return node.src;
5020    } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
5021      // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
5022      var outerHTML      = node.outerHTML.toLowerCase(),
5023          // TODO: This might not work for attributes without value: <input disabled>
5024          hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
5025      
5026      return hasAttribute ? node.getAttribute(attributeName) : null;
5027    } else{
5028      return node.getAttribute(attributeName);
5029    }
5030  }
5031  
5032  /**
5033   * Check whether the given node is a proper loaded image
5034   * FIXME: Returns undefined when unknown (Chrome, Safari)
5035   */
5036  function _isLoadedImage(node) {
5037    try {
5038      return node.complete && !node.mozMatchesSelector(":-moz-broken");
5039    } catch(e) {
5040      if (node.complete && node.readyState === "complete") {
5041        return true;
5042      }
5043    }
5044  }
5045  
5046  function _handleText(oldNode) {
5047    return oldNode.ownerDocument.createTextNode(oldNode.data);
5048  }
5049  
5050  
5051  // ------------ attribute checks ------------ \\
5052  var attributeCheckMethods = {
5053    url: (function() {
5054      var REG_EXP = /^https?:\/\//i;
5055      return function(attributeValue) {
5056        if (!attributeValue || !attributeValue.match(REG_EXP)) {
5057          return null;
5058        }
5059        return attributeValue.replace(REG_EXP, function(match) {
5060          return match.toLowerCase();
5061        });
5062      };
5063    })(),
5064    
5065    alt: (function() {
5066      var REG_EXP = /[^ a-z0-9_\-]/gi;
5067      return function(attributeValue) {
5068        if (!attributeValue) {
5069          return "";
5070        }
5071        return attributeValue.replace(REG_EXP, "");
5072      };
5073    })(),
5074    
5075    numbers: (function() {
5076      var REG_EXP = /\D/g;
5077      return function(attributeValue) {
5078        attributeValue = (attributeValue || "").replace(REG_EXP, "");
5079        return attributeValue || null;
5080      };
5081    })()
5082  };
5083  
5084  // ------------ class converter (converts an html attribute to a class name) ------------ \\
5085  var addClassMethods = {
5086    align_img: (function() {
5087      var mapping = {
5088        left:   "wysiwyg-float-left",
5089        right:  "wysiwyg-float-right"
5090      };
5091      return function(attributeValue) {
5092        return mapping[String(attributeValue).toLowerCase()];
5093      };
5094    })(),
5095    
5096    align_text: (function() {
5097      var mapping = {
5098        left:     "wysiwyg-text-align-left",
5099        right:    "wysiwyg-text-align-right",
5100        center:   "wysiwyg-text-align-center",
5101        justify:  "wysiwyg-text-align-justify"
5102      };
5103      return function(attributeValue) {
5104        return mapping[String(attributeValue).toLowerCase()];
5105      };
5106    })(),
5107    
5108    clear_br: (function() {
5109      var mapping = {
5110        left:   "wysiwyg-clear-left",
5111        right:  "wysiwyg-clear-right",
5112        both:   "wysiwyg-clear-both",
5113        all:    "wysiwyg-clear-both"
5114      };
5115      return function(attributeValue) {
5116        return mapping[String(attributeValue).toLowerCase()];
5117      };
5118    })(),
5119    
5120    size_font: (function() {
5121      var mapping = {
5122        "1": "wysiwyg-font-size-xx-small",
5123        "2": "wysiwyg-font-size-small",
5124        "3": "wysiwyg-font-size-medium",
5125        "4": "wysiwyg-font-size-large",
5126        "5": "wysiwyg-font-size-x-large",
5127        "6": "wysiwyg-font-size-xx-large",
5128        "7": "wysiwyg-font-size-xx-large",
5129        "-": "wysiwyg-font-size-smaller",
5130        "+": "wysiwyg-font-size-larger"
5131      };
5132      return function(attributeValue) {
5133        return mapping[String(attributeValue).charAt(0)];
5134      };
5135    })()
5136  };
5137  
5138  return parse;
5139})();/**
5140 * Checks for empty text node childs and removes them
5141 *
5142 * @param {Element} node The element in which to cleanup
5143 * @example
5144 *    wysihtml5.dom.removeEmptyTextNodes(element);
5145 */
5146wysihtml5.dom.removeEmptyTextNodes = function(node) {
5147  var childNode,
5148      childNodes        = wysihtml5.lang.array(node.childNodes).get(),
5149      childNodesLength  = childNodes.length,
5150      i                 = 0;
5151  for (; i<childNodesLength; i++) {
5152    childNode = childNodes[i];
5153    if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
5154      childNode.parentNode.removeChild(childNode);
5155    }
5156  }
5157};
5158/**
5159 * Renames an element (eg. a <div> to a <p>) and keeps its childs
5160 *
5161 * @param {Element} element The list element which should be renamed
5162 * @param {Element} newNodeName The desired tag name
5163 *
5164 * @example
5165 *    <!-- Assume the following dom: -->
5166 *    <ul id="list">
5167 *      <li>eminem</li>
5168 *      <li>dr. dre</li>
5169 *      <li>50 Cent</li>
5170 *    </ul>
5171 *
5172 *    <script>
5173 *      wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
5174 *    </script>
5175 *
5176 *    <!-- Will result in: -->
5177 *    <ol>
5178 *      <li>eminem</li>
5179 *      <li>dr. dre</li>
5180 *      <li>50 Cent</li>
5181 *    </ol>
5182 */
5183wysihtml5.dom.renameElement = function(element, newNodeName) {
5184  var newElement = element.ownerDocument.createElement(newNodeName),
5185      firstChild;
5186  while (firstChild = element.firstChild) {
5187    newElement.appendChild(firstChild);
5188  }
5189  wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
5190  element.parentNode.replaceChild(newElement, element);
5191  return newElement;
5192};/**
5193 * Takes an element, removes it and replaces it with it's childs
5194 * 
5195 * @param {Object} node The node which to replace with it's child nodes
5196 * @example
5197 *    <div id="foo">
5198 *      <span>hello</span>
5199 *    </div>
5200 *    <script>
5201 *      // Remove #foo and replace with it's children
5202 *      wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
5203 *    </script>
5204 */
5205wysihtml5.dom.replaceWithChildNodes = function(node) {
5206  if (!node.parentNode) {
5207    return;
5208  }
5209  
5210  if (!node.firstChild) {
5211    node.parentNode.removeChild(node);
5212    return;
5213  }
5214  
5215  var fragment = node.ownerDocument.createDocumentFragment();
5216  while (node.firstChild) {
5217    fragment.appendChild(node.firstChild);
5218  }
5219  node.parentNode.replaceChild(fragment, node);
5220  node = fragment = null;
5221};
5222/**
5223 * Unwraps an unordered/ordered list
5224 *
5225 * @param {Element} element The list element which should be unwrapped
5226 *
5227 * @example
5228 *    <!-- Assume the following dom: -->
5229 *    <ul id="list">
5230 *      <li>eminem</li>
5231 *      <li>dr. dre</li>
5232 *      <li>50 Cent</li>
5233 *    </ul>
5234 *
5235 *    <script>
5236 *      wysihtml5.dom.resolveList(document.getElementById("list"));
5237 *    </script>
5238 *
5239 *    <!-- Will result in: -->
5240 *    eminem<br>
5241 *    dr. dre<br>
5242 *    50 Cent<br>
5243 */
5244(function(dom) {
5245  function _isBlockElement(node) {
5246    return dom.getStyle("display").from(node) === "block";
5247  }
5248  
5249  function _isLineBreak(node) {
5250    return node.nodeName === "BR";
5251  }
5252  
5253  function _appendLineBreak(element) {
5254    var lineBreak = element.ownerDocument.createElement("br");
5255    element.appendChild(lineBreak);
5256  }
5257  
5258  function resolveList(list) {
5259    if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") {
5260      return;
5261    }
5262    
5263    var doc             = list.ownerDocument,
5264        fragment        = doc.createDocumentFragment(),
5265        previousSibling = list.previousElementSibling || list.previousSibling,
5266        firstChild,
5267        lastChild,
5268        isLastChild,
5269        shouldAppendLineBreak,
5270        listItem;
5271    
5272    if (previousSibling && !_isBlockElement(previousSibling)) {
5273      _appendLineBreak(fragment);
5274    }
5275    
5276    while (listItem = list.firstChild) {
5277      lastChild = listItem.lastChild;
5278      while (firstChild = listItem.firstChild) {
5279        isLastChild           = firstChild === lastChild;
5280        // This needs to be done before appending it to the fragment, as it otherwise will loose style information
5281        shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
5282        fragment.appendChild(firstChild);
5283        if (shouldAppendLineBreak) {
5284          _appendLineBreak(fragment);
5285        }
5286      }
5287      
5288      listItem.parentNode.removeChild(listItem);
5289    }
5290    list.parentNode.replaceChild(fragment, list);
5291  }
5292  
5293  dom.resolveList = resolveList;
5294})(wysihtml5.dom);/**
5295 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
5296 *
5297 * Browser Compatibility:
5298 *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
5299 *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
5300 *
5301 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
5302 *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
5303 *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
5304 *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
5305 *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
5306 *      can do anything as if the sandbox attribute wasn't set
5307 *
5308 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
5309 * @param {Object} [config] Optional parameters
5310 *
5311 * @example
5312 *    new wysihtml5.dom.Sandbox(function(sandbox) {
5313 *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
5314 *    });
5315 */
5316(function(wysihtml5) {
5317  var /**
5318       * Default configuration
5319       */
5320      doc                 = document,
5321      /**
5322       * Properties to unset/protect on the window object
5323       */
5324      windowProperties    = [
5325        "parent", "top", "opener", "frameElement", "frames",
5326        "localStorage", "globalStorage", "sessionStorage", "indexedDB"
5327      ],
5328      /**
5329       * Properties on the window object which are set to an empty function
5330       */
5331      windowProperties2   = [
5332        "open", "close", "openDialog", "showModalDialog",
5333        "alert", "confirm", "prompt",
5334        "openDatabase", "postMessage",
5335        "XMLHttpRequest", "XDomainRequest"
5336      ],
5337      /**
5338       * Properties to unset/protect on the document object
5339       */
5340      documentProperties  = [
5341        "referrer",
5342        "write", "open", "close"
5343      ];
5344  
5345  wysihtml5.dom.Sandbox = Base.extend(
5346    /** @scope wysihtml5.dom.Sandbox.prototype */ {
5347
5348    constructor: function(readyCallback, config) {
5349      this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
5350      this.config   = wysihtml5.lang.object({}).merge(config).get();
5351      this.iframe   = this._createIframe();
5352    },
5353    
5354    insertInto: function(element) {
5355      if (typeof(element) === "string") {
5356        element = doc.getElementById(element);
5357      }
5358      
5359      element.appendChild(this.iframe);
5360    },
5361
5362    getIframe: function() {
5363      return this.iframe;
5364    },
5365
5366    getWindow: function() {
5367      this._readyError();
5368    },
5369
5370    getDocument: function() {
5371      this._readyError();
5372    },
5373
5374    destroy: function() {
5375      var iframe = this.getIframe();
5376      iframe.parentNode.removeChild(iframe);
5377    },
5378
5379    _readyError: function() {
5380      throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
5381    },
5382
5383    /**
5384     * Creates the sandbox iframe
5385     *
5386     * Some important notes:
5387     *  - We can't use HTML5 sandbox for now:
5388     *    setting it causes that the iframe's dom can't be accessed from the outside
5389     *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
5390     *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
5391     *    In order to make this happen we need to set the "allow-scripts" flag.
5392     *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
5393     *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
5394     *  - IE needs to have the security="restricted" attribute set before the iframe is 
5395     *    inserted into the dom tree
5396     *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
5397     *    though it supports it
5398     *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
5399     *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
5400     *    on the onreadystatechange event
5401     */
5402    _createIframe: function() {
5403      var that   = this,
5404          iframe = doc.createElement("iframe");
5405      iframe.className = "wysihtml-sandbox";
5406      wysihtml5.dom.setAttributes({
5407        "security":           "restricted",
5408        "allowtransparency":  "true",
5409        "frameborder":        0,
5410        "width":              0,
5411        "height":             0,
5412        "marginwidth":        0,
5413        "marginheight":       0
5414      }).on(iframe);
5415
5416      // Setting the src like this prevents ssl warnings in IE6
5417      if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
5418        iframe.src = "javascript:'<html></html>'";
5419      }
5420
5421      iframe.onload = function() {
5422        iframe.onreadystatechange = iframe.onload = null;
5423        that._onLoadIframe(iframe);
5424      };
5425
5426      iframe.onreadystatechange = function() {
5427        if (/loaded|complete/.test(iframe.readyState)) {
5428          iframe.onreadystatechange = iframe.onload = null;
5429          that._onLoadIframe(iframe);
5430        }
5431      };
5432
5433      return iframe;
5434    },
5435
5436    /**
5437     * Callback for when the iframe has finished loading
5438     */
5439    _onLoadIframe: function(iframe) {
5440      // don't resume when the iframe got unloaded (eg. by removing it from the dom)
5441      if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
5442        return;
5443      }
5444
5445      var that           = this,
5446          iframeWindow   = iframe.contentWindow,
5447          iframeDocument = iframe.contentWindow.document,
5448          charset        = doc.characterSet || doc.charset || "utf-8",
5449          sandboxHtml    = this._getHtml({
5450            charset:      charset,
5451            stylesheets:  this.config.stylesheets
5452          });
5453
5454      // Create the basic dom tree including proper DOCTYPE and charset
5455      iframeDocument.open("text/html", "replace");
5456      iframeDocument.write(sandboxHtml);
5457      iframeDocument.close();
5458
5459      this.getWindow = function() { return iframe.contentWindow; };
5460      this.getDocument = function() { return iframe.contentWindow.document; };
5461
5462      // Catch js errors and pass them to the parent's onerror event
5463      // addEventListener("error") doesn't work properly in some browsers
5464      // TODO: apparently this doesn't work in IE9!
5465      iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
5466        throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
5467      };
5468
5469      if (!wysihtml5.browser.supportsSandboxedIframes()) {
5470        // Unset a bunch of sensitive variables
5471        // Please note: This isn't hack safe!  
5472        // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
5473        // IE is secure though, which is the most important thing, since IE is the only browser, who
5474        // takes over scripts & styles into contentEditable elements when copied from external websites
5475        // or applications (Microsoft Word, ...)
5476        var i, length;
5477        for (i=0, length=windowProperties.length; i<length; i++) {
5478          this._unset(iframeWindow, windowProperties[i]);
5479        }
5480        for (i=0, length=windowProperties2.length; i<length; i++) {
5481          this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
5482        }
5483        for (i=0, length=documentProperties.length; i<length; i++) {
5484          this._unset(iframeDocument, documentProperties[i]);
5485        }
5486        // This doesn't work in Safari 5 
5487        // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
5488        this._unset(iframeDocument, "cookie", "", true);
5489      }
5490
5491      this.loaded = true;
5492
5493      // Trigger the callback
5494      setTimeout(function() { that.callback(that); }, 0);
5495    },
5496
5497    _getHtml: function(templateVars) {
5498      var stylesheets = templateVars.stylesheets,
5499          html        = "",
5500          i           = 0,
5501          length;
5502      stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
5503      if (stylesheets) {
5504        length = stylesheets.length;
5505        for (; i<length; i++) {
5506          html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
5507        }
5508      }
5509      templateVars.stylesheets = html;
5510
5511      return wysihtml5.lang.string(
5512        '<!DOCTYPE html><html><head>'
5513        + '<meta charset="#{charset}">#{stylesheets}</head>'
5514        + '<body></body></html>'
5515      ).interpolate(templateVars);
5516    },
5517
5518    /**
5519     * Method to unset/override existing variables
5520     * @example
5521     *    // Make cookie unreadable and unwritable
5522     *    this._unset(document, "cookie", "", true);
5523     */
5524    _unset: function(object, property, value, setter) {
5525      try { object[property] = value; } catch(e) {}
5526
5527      try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
5528      if (setter) {
5529        try { object.__defineSetter__(property, function() {}); } catch(e) {}
5530      }
5531
5532      if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
5533        try {
5534          var config = {
5535            get: function() { return value; }
5536          };
5537          if (setter) {
5538            config.set = function() {};
5539          }
5540          Object.defineProperty(object, property, config);
5541        } catch(e) {}
5542      }
5543    }
5544  });
5545})(wysihtml5);
5546(function() {
5547  var mapping = {
5548    "className": "class"
5549  };
5550  wysihtml5.dom.setAttributes = function(attributes) {
5551    return {
5552      on: function(element) {
5553        for (var i in attributes) {
5554          element.setAttribute(mapping[i] || i, attributes[i]);
5555        }
5556      }
5557    }
5558  };
5559})();wysihtml5.dom.setStyles = function(styles) {
5560  return {
5561    on: function(element) {
5562      var style = element.style;
5563      if (typeof(styles) === "string") {
5564        style.cssText += ";" + styles;
5565        return;
5566      }
5567      for (var i in styles) {
5568        if (i === "float") {
5569          style.cssFloat = styles[i];
5570          style.styleFloat = styles[i];
5571        } else {
5572          style[i] = styles[i];
5573        }
5574      }
5575    }
5576  };
5577};/**
5578 * Simulate HTML5 placeholder attribute
5579 *
5580 * Needed since
5581 *    - div[contentEditable] elements don't support it
5582 *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
5583 *
5584 * @param {Object} parent Instance of main wysihtml5.Editor class
5585 * @param {Element} view Instance of wysihtml5.views.* class
5586 * @param {String} placeholderText
5587 *
5588 * @example
5589 *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
5590 */
5591(function(dom) {
5592  dom.simulatePlaceholder = function(editor, view, placeholderText) {
5593    var CLASS_NAME = "placeholder",
5594        unset = function() {
5595          if (view.hasPlaceholderSet()) {
5596            view.clear();
5597          }
5598          dom.removeClass(view.element, CLASS_NAME);
5599        },
5600        set = function() {
5601          if (view.isEmpty()) {
5602            view.setValue(placeholderText);
5603            dom.addClass(view.element, CLASS_NAME);
5604          }
5605        };
5606
5607    editor
5608      .observe("set_placeholder", set)
5609      .observe("unset_placeholder", unset)
5610      .observe("focus:composer", unset)
5611      .observe("paste:composer", unset)
5612      .observe("blur:composer", set);
5613
5614    set();
5615  };
5616})(wysihtml5.dom);
5617(function(dom) {
5618  var documentElement = document.documentElement;
5619  if ("textContent" in documentElement) {
5620    dom.setTextContent = function(element, text) {
5621      element.textContent = text;
5622    };
5623
5624    dom.getTextContent = function(element) {
5625      return element.textContent;
5626    };
5627  } else if ("innerText" in documentElement) {
5628    dom.setTextContent = function(element, text) {
5629      element.innerText = text;
5630    };
5631
5632    dom.getTextContent = function(element) {
5633      return element.innerText;
5634    };
5635  } else {
5636    dom.setTextContent = function(element, text) {
5637      element.nodeValue = text;
5638    };
5639
5640    dom.getTextContent = function(element) {
5641      return element.nodeValue;
5642    };
5643  }
5644})(wysihtml5.dom);
5645
5646/**
5647 * Fix most common html formatting misbehaviors of browsers implementation when inserting
5648 * content via copy & paste contentEditable
5649 *
5650 * @author Christopher Blum
5651 */
5652wysihtml5.quirks.cleanPastedHTML = (function() {
5653  // TODO: We probably need more rules here
5654  var defaultRules = {
5655    // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling
5656    "a u": wysihtml5.dom.replaceWithChildNodes
5657  };
5658  
5659  function cleanPastedHTML(elementOrHtml, rules, context) {
5660    rules   = rules || defaultRules;
5661    context = context || elementOrHtml.ownerDocument || document;
5662    
5663    var element,
5664        isString = typeof(elementOrHtml) === "string",
5665        method,
5666        matches,
5667        matchesLength,
5668        i,
5669        j = 0;
5670    if (isString) {
5671      element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5672    } else {
5673      element = elementOrHtml;
5674    }
5675    
5676    for (i in rules) {
5677      matches       = element.querySelectorAll(i);
5678      method        = rules[i];
5679      matchesLength = matches.length;
5680      for (; j<matchesLength; j++) {
5681        method(matches[j]);
5682      }
5683    }
5684    
5685    matches = elementOrHtml = rules = null;
5686    
5687    return isString ? element.innerHTML : element;
5688  }
5689  
5690  return cleanPastedHTML;
5691})();/**
5692 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
5693 *
5694 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5695 * @exaple
5696 *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5697 */
5698(function(wysihtml5) {
5699  var dom = wysihtml5.dom;
5700  
5701  wysihtml5.quirks.ensureProperClearing = (function() {
5702    var clearIfNecessary = function(event) {
5703      var element = this;
5704      setTimeout(function() {
5705        var innerHTML = element.innerHTML.toLowerCase();
5706        if (innerHTML == "<p>&nbsp;</p>" ||
5707            innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
5708          element.innerHTML = "";
5709        }
5710      }, 0);
5711    };
5712
5713    return function(composer) {
5714      dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
5715    };
5716  })();
5717
5718
5719
5720  /**
5721   * 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
5722   *
5723   * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
5724   * @exaple
5725   *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
5726   */
5727  wysihtml5.quirks.ensureProperClearingOfLists = (function() {
5728    var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"];
5729
5730    var clearIfNecessary = function(element, contentEditableElement) {
5731      if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) {
5732        return;
5733      }
5734
5735      var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI });
5736      if (!list) {
5737        return;
5738      }
5739
5740      var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild;
5741      if (!listIsFirstChildOfContentEditable) {
5742        return;
5743      }
5744
5745      var hasOnlyOneListItem = list.childNodes.length <= 1;
5746      if (!hasOnlyOneListItem) {
5747        return;
5748      }
5749
5750      var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true;
5751      if (!onlyListItemIsEmpty) {
5752        return;
5753      }
5754
5755      list.parentNode.removeChild(list);
5756    };
5757
5758    return function(composer) {
5759      dom.observe(composer.element, "keydown", function(event) {
5760        if (event.keyCode !== wysihtml5.BACKSPACE_KEY) {
5761          return;
5762        }
5763
5764        var element = composer.selection.getSelectedNode();
5765        clearIfNecessary(element, composer.element);
5766      });
5767    };
5768  })();
5769
5770})(wysihtml5);
5771// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
5772//
5773// In Firefox this:
5774//      var d = document.createElement("div");
5775//      d.innerHTML ='<a href="~"></a>';
5776//      d.innerHTML;
5777// will result in:
5778//      <a href="%7E"></a>
5779// which is wrong
5780(function(wysihtml5) {
5781  var TILDE_ESCAPED = "%7E";
5782  wysihtml5.quirks.getCorrectInnerHTML = function(element) {
5783    var innerHTML = element.innerHTML;
5784    if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
5785      return innerHTML;
5786    }
5787    
5788    var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
5789        url,
5790        urlToSearch,
5791        length,
5792        i;
5793    for (i=0, length=elementsWithTilde.length; i<length; i++) {
5794      url         = elementsWithTilde[i].href || elementsWithTilde[i].src;
5795      urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
5796      innerHTML   = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
5797    }
5798    return innerHTML;
5799  };
5800})(wysihtml5);/**
5801 * Some browsers don't insert line breaks when hitting return in a contentEditable element
5802 *    - Opera & IE insert new <p> on return
5803 *    - Chrome & Safari insert new <div> on return
5804 *    - Firefox inserts <br> on return (yippie!)
5805 *
5806 * @param {Element} element
5807 *
5808 * @example
5809 *    wysihtml5.quirks.insertLineBreakOnReturn(element);
5810 */
5811(function(wysihtml5) {
5812  var dom                                           = wysihtml5.dom,
5813      USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS  = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
5814      LIST_TAGS                                     = ["UL", "OL", "MENU"];
5815  
5816  wysihtml5.quirks.insertLineBreakOnReturn = function(composer) {
5817    function unwrap(selectedNode) {
5818      var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
5819      if (!parentElement) {
5820        return;
5821      }
5822
5823      var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE);
5824      dom.insert(invisibleSpace).before(parentElement);
5825      dom.replaceWithChildNodes(parentElement);
5826      composer.selection.selectNode(invisibleSpace);
5827    }
5828
5829    function keyDown(event) {
5830      var keyCode = event.keyCode;
5831      if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) {
5832        return;
5833      }
5834
5835      var element         = event.target,
5836          selectedNode    = composer.selection.getSelectedNode(),
5837          blockElement    = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4);
5838      if (blockElement) {
5839        // Some browsers create <p> elements after leaving a list
5840        // check after keydown of backspace and return whether a <p> got inserted and unwrap it
5841        if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) {
5842          setTimeout(function() {
5843            var selectedNode = composer.selection.getSelectedNode(),
5844                list,
5845                div;
5846            if (!selectedNode) {
5847              return;
5848            }
5849
5850            list = dom.getParentElement(selectedNode, {
5851              nodeName: LIST_TAGS
5852            }, 2);
5853
5854            if (list) {
5855              return;
5856            }
5857
5858            unwrap(selectedNode);
5859          }, 0);
5860        } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) {
5861          setTimeout(function() {
5862            unwrap(composer.selection.getSelectedNode());
5863          }, 0);
5864        } 
5865        return;
5866      }
5867
5868      if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
5869        composer.commands.exec("insertLineBreak");
5870        event.preventDefault();
5871      }
5872    }
5873    
5874    // keypress doesn't fire when you hit backspace
5875    dom.observe(composer.element.ownerDocument, "keydown", keyDown);
5876  };
5877})(wysihtml5);/**
5878 * Force rerendering of a given element
5879 * Needed to fix display misbehaviors of IE
5880 *
5881 * @param {Element} element The element object which needs to be rerendered
5882 * @example
5883 *    wysihtml5.quirks.redraw(document.body);
5884 */
5885(function(wysihtml5) {
5886  var CLASS_NAME = "wysihtml5-quirks-redraw";
5887  
5888  wysihtml5.quirks.redraw = function(element) {
5889    wysihtml5.dom.addClass(element, CLASS_NAME);
5890    wysihtml5.dom.removeClass(element, CLASS_NAME);
5891    
5892    // Following hack is needed for firefox to make sure that image resize handles are properly removed
5893    try {
5894      var doc = element.ownerDocument;
5895      doc.execCommand("italic", false, null);
5896      doc.execCommand("italic", false, null);
5897    } catch(e) {}
5898  };
5899})(wysihtml5);/**
5900 * Selection API
5901 *
5902 * @example
5903 *    var selection = new wysihtml5.Selection(editor);
5904 */
5905(function(wysihtml5) {
5906  var dom = wysihtml5.dom;
5907  
5908  function _getCumulativeOffsetTop(element) {
5909    var top = 0;
5910    if (element.parentNode) {
5911      do {
5912        top += element.offsetTop || 0;
5913        element = element.offsetParent;
5914      } while (element);
5915    }
5916    return top;
5917  }
5918  
5919  wysihtml5.Selection = Base.extend(
5920    /** @scope wysihtml5.Selection.prototype */ {
5921    constructor: function(editor) {
5922      // Make sure that our external range library is initialized
5923      window.rangy.init();
5924      
5925      this.editor   = editor;
5926      this.composer = editor.composer;
5927      this.doc      = this.composer.doc;
5928    },
5929    
5930    /**
5931     * Get the current selection as a bookmark to be able to later restore it
5932     *
5933     * @return {Object} An object that represents the current selection
5934     */
5935    getBookmark: function() {
5936      var range = this.getRange();
5937      return range && range.cloneRange();
5938    },
5939
5940    /**
5941     * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
5942     *
5943     * @param {Object} bookmark An object that represents the current selection
5944     */
5945    setBookmark: function(bookmark) {
5946      if (!bookmark) {
5947        return;
5948      }
5949
5950      this.setSelection(bookmark);
5951    },
5952
5953    /**
5954     * Set the caret in front of the given node
5955     *
5956     * @param {Object} node The element or text node where to position the caret in front of
5957     * @example
5958     *    selection.setBefore(myElement);
5959     */
5960    setBefore: function(node) {
5961      var range = rangy.createRange(this.doc);
5962      range.setStartBefore(node);
5963      range.setEndBefore(node);
5964      return this.setSelection(range);
5965    },
5966
5967    /**
5968     * Set the caret after the given node
5969     *
5970     * @param {Object} node The element or text node where to position the caret in front of
5971     * @example
5972     *    selection.setBefore(myElement);
5973     */
5974    setAfter: function(node) {
5975      var range = rangy.createRange(this.doc);
5976      range.setStartAfter(node);
5977      range.setEndAfter(node);
5978      return this.setSelection(range);
5979    },
5980
5981    /**
5982     * Ability to select/mark nodes
5983     *
5984     * @param {Element} node The node/element to select
5985     * @example
5986     *    selection.selectNode(document.getElementById("my-image"));
5987     */
5988    selectNode: function(node) {
5989      var range           = rangy.createRange(this.doc),
5990          isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
5991          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
5992          content         = isElement ? node.innerHTML : node.data,
5993          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
5994          displayStyle    = dom.getStyle("display").from(node),
5995          isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
5996
5997      if (isEmpty && isElement && canHaveHTML) {
5998        // Make sure that caret is visible in node by inserting a zero width no breaking space
5999        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
6000      }
6001
6002      if (canHaveHTML) {
6003        range.selectNodeContents(node);
6004      } else {
6005        range.selectNode(node);
6006      }
6007
6008      if (canHaveHTML && isEmpty && isElement) {
6009        range.collapse(isBlockElement);
6010      } else if (canHaveHTML && isEmpty) {
6011        range.setStartAfter(node);
6012        range.setEndAfter(node);
6013      }
6014
6015      this.setSelection(range);
6016    },
6017
6018    /**
6019     * Get the node which contains the selection
6020     *
6021     * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
6022     * @return {Object} The node that contains the caret
6023     * @example
6024     *    var nodeThatContainsCaret = selection.getSelectedNode();
6025     */
6026    getSelectedNode: function(controlRange) {
6027      var selection,
6028          range;
6029
6030      if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
6031        range = this.doc.selection.createRange();
6032        if (range && range.length) {
6033          return range.item(0);
6034        }
6035      }
6036
6037      selection = this.getSelection(this.doc);
6038      if (selection.focusNode === selection.anchorNode) {
6039        return selection.focusNode;
6040      } else {
6041        range = this.getRange(this.doc);
6042        return range ? range.commonAncestorContainer : this.doc.body;
6043      }
6044    },
6045
6046    executeAndRestore: function(method, restoreScrollPosition) {
6047      var body                  = this.doc.body,
6048          oldScrollTop          = restoreScrollPosition && body.scrollTop,
6049          oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
6050          className             = "_wysihtml5-temp-placeholder",
6051          placeholderHTML       = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
6052          range                 = this.getRange(this.doc),
6053          newRange;
6054      
6055      // Nothing selected, execute and say goodbye
6056      if (!range) {
6057        method(body, body);
6058        return;
6059      }
6060      
6061      var node = range.createContextualFragment(placeholderHTML);
6062      range.insertNode(node);
6063      
6064      // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
6065      try {
6066        method(range.startContainer, range.endContainer);
6067      } catch(e3) {
6068        setTimeout(function() { throw e3; }, 0);
6069      }
6070      
6071      caretPlaceholder = this.doc.querySelector("." + className);
6072      if (caretPlaceholder) {
6073        newRange = rangy.createRange(this.doc);
6074        newRange.selectNode(caretPlaceholder);
6075        newRange.deleteContents();
6076        this.setSelection(newRange);
6077      } else {
6078        // fallback for when all hell breaks loose
6079        body.focus();
6080      }
6081
6082      if (restoreScrollPosition) {
6083        body.scrollTop  = oldScrollTop;
6084        body.scrollLeft = oldScrollLeft;
6085      }
6086
6087      // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
6088      try {
6089        caretPlaceholder.parentNode.removeChild(caretPlaceholder);
6090      } catch(e4) {}
6091    },
6092
6093    /**
6094     * Different approach of preserving the selection (doesn't modify the dom)
6095     * Takes all text nodes in the selection and saves the selection position in the first and last one
6096     */
6097    executeAndRestoreSimple: function(method) {
6098      var range = this.getRange(),
6099          body  = this.doc.body,
6100          newRange,
6101          firstNode,
6102          lastNode,
6103          textNodes,
6104          rangeBackup;
6105
6106      // Nothing selected, execute and say goodbye
6107      if (!range) {
6108        method(body, body);
6109        return;
6110      }
6111
6112      textNodes = range.getNodes([3]);
6113      firstNode = textNodes[0] || range.startContainer;
6114      lastNode  = textNodes[textNodes.length - 1] || range.endContainer;
6115
6116      rangeBackup = {
6117        collapsed:      range.collapsed,
6118        startContainer: firstNode,
6119        startOffset:    firstNode === range.startContainer ? range.startOffset : 0,
6120        endContainer:   lastNode,
6121        endOffset:      lastNode === range.endContainer ? range.endOffset : lastNode.length
6122      };
6123
6124      try {
6125        method(range.startContainer, range.endContainer);
6126      } catch(e) {
6127        setTimeout(function() { throw e; }, 0);
6128      }
6129
6130      newRange = rangy.createRange(this.doc);
6131      try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {}
6132      try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {}
6133      try { this.setSelection(newRange); } catch(e3) {}
6134    },
6135
6136    /**
6137     * Insert html at the caret position and move the cursor after the inserted html
6138     *
6139     * @param {String} html HTML string to insert
6140     * @example
6141     *    selection.insertHTML("<p>foobar</p>");
6142     */
6143    insertHTML: function(html) {
6144      var range     = rangy.createRange(this.doc),
6145          node      = range.createContextualFragment(html),
6146          lastChild = node.lastChild;
6147      this.insertNode(node);
6148      if (lastChild) {
6149        this.setAfter(lastChild);
6150      }
6151    },
6152
6153    /**
6154     * Insert a node at the caret position and move the cursor behind it
6155     *
6156     * @param {Object}
6156 node HTML string to insert
6157     * @example
6158     *    selection.insertNode(document.createTextNode("foobar"));
6159     */
6160    insertNode: function(node) {
6161      var range = this.getRange();
6162      if (range) {
6163        range.insertNode(node);
6164      }
6165    },
6166
6167    /**
6168     * Wraps current selection with the given node
6169     *
6170     * @param {Object} node The node to surround the selected elements with
6171     */
6172    surround: function(node) {
6173      var range = this.getRange();
6174      if (!range) {
6175        return;
6176      }
6177
6178      try {
6179        // This only works when the range boundaries are not overlapping other elements
6180        range.surroundContents(node);
6181        this.selectNode(node);
6182      } catch(e) {
6183        // fallback
6184        node.appendChild(range.extractContents());
6185        range.insertNode(node);
6186      }
6187    },
6188
6189    /**
6190     * Scroll the current caret position into the view
6191     * FIXME: This is a bit hacky, there might be a smarter way of doing this
6192     *
6193     * @example
6194     *    selection.scrollIntoView();
6195     */
6196    scrollIntoView: function() {
6197      var doc           = this.doc,
6198          hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
6199          tempElement   = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
6200            var element = doc.createElement("span");
6201            // The element needs content in order to be able to calculate it's position properly
6202            element.innerHTML = wysihtml5.INVISIBLE_SPACE;
6203            return element;
6204          })(),
6205          offsetTop;
6206
6207      if (hasScrollBars) {
6208        this.insertNode(tempElement);
6209        offsetTop = _getCumulativeOffsetTop(tempElement);
6210        tempElement.parentNode.removeChild(tempElement);
6211        if (offsetTop > doc.body.scrollTop) {
6212          doc.body.scrollTop = offsetTop;
6213        }
6214      }
6215    },
6216
6217    /**
6218     * Select line where the caret is in
6219     */
6220    selectLine: function() {
6221      if (wysihtml5.browser.supportsSelectionModify()) {
6222        this._selectLine_W3C();
6223      } else if (this.doc.selection) {
6224        this._selectLine_MSIE();
6225      }
6226    },
6227
6228    /**
6229     * See https://developer.mozilla.org/en/DOM/Selection/modify
6230     */
6231    _selectLine_W3C: function() {
6232      var win       = this.doc.defaultView,
6233          selection = win.getSelection();
6234      selection.modify("extend", "left", "lineboundary");
6235      selection.modify("extend", "right", "lineboundary");
6236    },
6237
6238    _selectLine_MSIE: function() {
6239      var range       = this.doc.selection.createRange(),
6240          rangeTop    = range.boundingTop,
6241          rangeHeight = range.boundingHeight,
6242          scrollWidth = this.doc.body.scrollWidth,
6243          rangeBottom,
6244          rangeEnd,
6245          measureNode,
6246          i,
6247          j;
6248
6249      if (!range.moveToPoint) {
6250        return;
6251      }
6252
6253      if (rangeTop === 0) {
6254        // Don't know why, but when the selection ends at the end of a line
6255        // range.boundingTop is 0
6256        measureNode = this.doc.createElement("span");
6257        this.insertNode(measureNode);
6258        rangeTop = measureNode.offsetTop;
6259        measureNode.parentNode.removeChild(measureNode);
6260      }
6261
6262      rangeTop += 1;
6263
6264      for (i=-10; i<scrollWidth; i+=2) {
6265        try {
6266          range.moveToPoint(i, rangeTop);
6267          break;
6268        } catch(e1) {}
6269      }
6270
6271      // Investigate the following in order to handle multi line selections
6272      // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
6273      rangeBottom = rangeTop;
6274      rangeEnd = this.doc.selection.createRange();
6275      for (j=scrollWidth; j>=0; j--) {
6276        try {
6277          rangeEnd.moveToPoint(j, rangeBottom);
6278          break;
6279        } catch(e2) {}
6280      }
6281
6282      range.setEndPoint("EndToEnd", rangeEnd);
6283      range.select();
6284    },
6285
6286    getText: function() {
6287      var selection = this.getSelection();
6288      return selection ? selection.toString() : "";
6289    },
6290
6291    getNodes: function(nodeType, filter) {
6292      var range = this.getRange();
6293      if (range) {
6294        return range.getNodes([nodeType], filter);
6295      } else {
6296        return [];
6297      }
6298    },
6299    
6300    getRange: function() {
6301      var selection = this.getSelection();
6302      return selection && selection.rangeCount && selection.getRangeAt(0);
6303    },
6304
6305    getSelection: function() {
6306      return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
6307    },
6308
6309    setSelection: function(range) {
6310      var win       = this.doc.defaultView || this.doc.parentWindow,
6311          selection = rangy.getSelection(win);
6312      return selection.setSingleRange(range);
6313    }
6314  });
6315  
6316})(wysihtml5);
6317/**
6318 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
6319 * http://code.google.com/p/rangy/
6320 *
6321 * changed in order to be able ...
6322 *    - to use custom tags
6323 *    - to detect and replace similar css classes via reg exp
6324 */
6325(function(wysihtml5, rangy) {
6326  var defaultTagName = "span";
6327  
6328  var REG_EXP_WHITE_SPACE = /\s+/g;
6329  
6330  function hasClass(el, cssClass, regExp) {
6331    if (!el.className) {
6332      return false;
6333    }
6334    
6335    var matchingClassNames = el.className.match(regExp) || [];
6336    return matchingClassNames[matchingClassNames.length - 1] === cssClass;
6337  }
6338
6339  function addClass(el, cssClass, regExp) {
6340    if (el.className) {
6341      removeClass(el, regExp);
6342      el.className += " " + cssClass;
6343    } else {
6344      el.className = cssClass;
6345    }
6346  }
6347
6348  function removeClass(el, regExp) {
6349    if (el.className) {
6350      el.className = el.className.replace(regExp, "");
6351    }
6352  }
6353  
6354  function hasSameClasses(el1, el2) {
6355    return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
6356  }
6357
6358  function replaceWithOwnChildren(el) {
6359    var parent = el.parentNode;
6360    while (el.firstChild) {
6361      parent.insertBefore(el.firstChild, el);
6362    }
6363    parent.removeChild(el);
6364  }
6365
6366  function elementsHaveSameNonClassAttributes(el1, el2) {
6367    if (el1.attributes.length != el2.attributes.length) {
6368      return false;
6369    }
6370    for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
6371      attr1 = el1.attributes[i];
6372      name = attr1.name;
6373      if (name != "class") {
6374        attr2 = el2.attributes.getNamedItem(name);
6375        if (attr1.specified != attr2.specified) {
6376          return false;
6377        }
6378        if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
6379          return false;
6380        }
6381      }
6382    }
6383    return true;
6384  }
6385
6386  function isSplitPoint(node, offset) {
6387    if (rangy.dom.isCharacterDataNode(node)) {
6388      if (offset == 0) {
6389        return !!node.previousSibling;
6390      } else if (offset == node.length) {
6391        return !!node.nextSibling;
6392      } else {
6393        return true;
6394      }
6395    }
6396
6397    return offset > 0 && offset < node.childNodes.length;
6398  }
6399
6400  function splitNodeAt(node, descendantNode, descendantOffset) {
6401    var newNode;
6402    if (rangy.dom.isCharacterDataNode(descendantNode)) {
6403      if (descendantOffset == 0) {
6404        descendantOffset = rangy.dom.getNodeIndex(descendantNode);
6405        descendantNode = descendantNode.parentNode;
6406      } else if (descendantOffset == descendantNode.length) {
6407        descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
6408        descendantNode = descendantNode.parentNode;
6409      } else {
6410        newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
6411      }
6412    }
6413    if (!newNode) {
6414      newNode = descendantNode.cloneNode(false);
6415      if (newNode.id) {
6416        newNode.removeAttribute("id");
6417      }
6418      var child;
6419      while ((child = descendantNode.childNodes[descendantOffset])) {
6420        newNode.appendChild(child);
6421      }
6422      rangy.dom.insertAfter(newNode, descendantNode);
6423    }
6424    return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode));
6425  }
6426  
6427  function Merge(firstNode) {
6428    this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
6429    this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
6430    this.textNodes = [this.firstTextNode];
6431  }
6432
6433  Merge.prototype = {
6434    doMerge: function() {
6435      var textBits = [], textNode, parent, text;
6436      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6437        textNode = this.textNodes[i];
6438        parent = textNode.parentNode;
6439        textBits[i] = textNode.data;
6440        if (i) {
6441          parent.removeChild(textNode);
6442          if (!parent.hasChildNodes()) {
6443            parent.parentNode.removeChild(parent);
6444          }
6445        }
6446      }
6447      this.firstTextNode.data = text = textBits.join("");
6448      return text;
6449    },
6450
6451    getLength: function() {
6452      var i = this.textNodes.length, len = 0;
6453      while (i--) {
6454        len += this.textNodes[i].length;
6455      }
6456      return len;
6457    },
6458
6459    toString: function() {
6460      var textBits = [];
6461      for (var i = 0, len = this.textNodes.length; i < len; ++i) {
6462        textBits[i] = "'" + this.textNodes[i].data + "'";
6463      }
6464      return "[Merge(" + textBits.join(",") + ")]";
6465    }
6466  };
6467
6468  function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) {
6469    this.tagNames = tagNames || [defaultTagName];
6470    this.cssClass = cssClass || "";
6471    this.similarClassRegExp = similarClassRegExp;
6472    this.normalize = normalize;
6473    this.applyToAnyTagName = false;
6474  }
6475
6476  HTMLApplier.prototype = {
6477    getAncestorWithClass: function(node) {
6478      var cssClassMatch;
6479      while (node) {
6480        cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true;
6481        if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
6482          return node;
6483        }
6484        node = node.parentNode;
6485      }
6486      return false;
6487    },
6488
6489    // Normalizes nodes after applying a CSS class to a Range.
6490    postApply: function(textNodes, range) {
6491      var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
6492
6493      var merges = [], currentMerge;
6494
6495      var rangeStartNode = firstNode, rangeEndNode = lastNode;
6496      var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
6497
6498      var textNode, precedingTextNode;
6499
6500      for (var i = 0, len = textNodes.length; i < len; ++i) {
6501        textNode = textNodes[i];
6502        precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
6503        if (precedingTextNode) {
6504          if (!currentMerge) {
6505            currentMerge = new Merge(precedingTextNode);
6506            merges.push(currentMerge);
6507          }
6508          currentMerge.textNodes.push(textNode);
6509          if (textNode === firstNode) {
6510            rangeStartNode = currentMerge.firstTextNode;
6511            rangeStartOffset = rangeStartNode.length;
6512          }
6513          if (textNode === lastNode) {
6514            rangeEndNode = currentMerge.firstTextNode;
6515            rangeEndOffset = currentMerge.getLength();
6516          }
6517        } else {
6518          currentMerge = null;
6519        }
6520      }
6521
6522      // Test whether the first node after the range needs merging
6523      var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
6524      if (nextTextNode) {
6525        if (!currentMerge) {
6526          currentMerge = new Merge(lastNode);
6527          merges.push(currentMerge);
6528        }
6529        currentMerge.textNodes.push(nextTextNode);
6530      }
6531
6532      // Do the merges
6533      if (merges.length) {
6534        for (i = 0, len = merges.length; i < len; ++i) {
6535          merges[i].doMerge();
6536        }
6537        // Set the range boundaries
6538        range.setStart(rangeStartNode, rangeStartOffset);
6539        range.setEnd(rangeEndNode, rangeEndOffset);
6540      }
6541    },
6542    
6543    getAdjacentMergeableTextNode: function(node, forward) {
6544        var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
6545        var el = isTextNode ? node.parentNode : node;
6546        var adjacentNode;
6547        var propName = forward ? "nextSibling" : "previousSibling";
6548        if (isTextNode) {
6549          // Can merge if the node's previous/next sibling is a text node
6550          adjacentNode = node[propName];
6551          if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
6552            return adjacentNode;
6553          }
6554        } else {
6555          // Compare element with its sibling
6556          adjacentNode = el[propName];
6557          if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
6558            return adjacentNode[forward ? "firstChild" : "lastChild"];
6559          }
6560        }
6561        return null;
6562    },
6563    
6564    areElementsMergeable: function(el1, el2) {
6565      return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
6566        && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
6567        && hasSameClasses(el1, el2)
6568        && elementsHaveSameNonClassAttributes(el1, el2);
6569    },
6570
6571    createContainer: function(doc) {
6572      var el = doc.createElement(this.tagNames[0]);
6573      if (this.cssClass) {
6574        el.className = this.cssClass;
6575      }
6576      return el;
6577    },
6578
6579    applyToTextNode: function(textNode) {
6580      var parent = textNode.parentNode;
6581      if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
6582        if (this.cssClass) {
6583          addClass(parent, this.cssClass, this.similarClassRegExp);
6584        }
6585      } else {
6586        var el = this.createContainer(rangy.dom.getDocument(textNode));
6587        textNode.parentNode.insertBefore(el, textNode);
6588        el.appendChild(textNode);
6589      }
6590    },
6591
6592    isRemovable: function(el) {
6593      return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass;
6594    },
6595
6596    undoToTextNode: function(textNode, range, ancestorWithClass) {
6597      if (!range.containsNode(ancestorWithClass)) {
6598        // Split out the portion of the ancestor from which we can remove the CSS class
6599        var ancestorRange = range.cloneRange();
6600        ancestorRange.selectNode(ancestorWithClass);
6601
6602        if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
6603          splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset);
6604          range.setEndAfter(ancestorWithClass);
6605        }
6606        if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
6607          ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset);
6608        }
6609      }
6610      
6611      if (this.similarClassRegExp) {
6612        removeClass(ancestorWithClass, this.similarClassRegExp);
6613      }
6614      if (this.isRemovable(ancestorWithClass)) {
6615        replaceWithOwnChildren(ancestorWithClass);
6616      }
6617    },
6618
6619    applyToRange: function(range) {
6620        var textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6621        if (!textNodes.length) {
6622          try {
6623            var node = this.createContainer(range.endContainer.ownerDocument);
6624            range.surroundContents(node);
6625            this.selectNode(range, node);
6626            return;
6627          } catch(e) {}
6628        }
6629        
6630        range.splitBoundaries();
6631        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6632        
6633        if (textNodes.length) {
6634          var textNode;
6635
6636          for (var i = 0, len = textNodes.length; i < len; ++i) {
6637            textNode = textNodes[i];
6638            if (!this.getAncestorWithClass(textNode)) {
6639              this.applyToTextNode(textNode);
6640            }
6641          }
6642          
6643          range.setStart(textNodes[0], 0);
6644          textNode = textNodes[textNodes.length - 1];
6645          range.setEnd(textNode, textNode.length);
6646          
6647          if (this.normalize) {
6648            this.postApply(textNodes, range);
6649          }
6650        }
6651    },
6652
6653    undoToRange: function(range) {
6654      var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass;
6655      if (textNodes.length) {
6656        range.splitBoundaries();
6657        textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6658      } else {
6659        var doc = range.endContainer.ownerDocument,
6660            node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
6661        range.insertNode(node);
6662        range.selectNode(node);
6663        textNodes = [node];
6664      }
6665      
6666      for (var i = 0, len = textNodes.length; i < len; ++i) {
6667        textNode = textNodes[i];
6668        ancestorWithClass = this.getAncestorWithClass(textNode);
6669        if (ancestorWithClass) {
6670          this.undoToTextNode(textNode, range, ancestorWithClass);
6671        }
6672      }
6673      
6674      if (len == 1) {
6675        this.selectNode(range, textNodes[0]);
6676      } else {
6677        range.setStart(textNodes[0], 0);
6678        textNode = textNodes[textNodes.length - 1];
6679        range.setEnd(textNode, textNode.length);
6680
6681        if (this.normalize) {
6682          this.postApply(textNodes, range);
6683        }
6684      }
6685    },
6686    
6687    selectNode: function(range, node) {
6688      var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
6689          canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
6690          content         = isElement ? node.innerHTML : node.data,
6691          isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
6692
6693      if (isEmpty && isElement && canHaveHTML) {
6694        // Make sure that caret is visible in node by inserting a zero width no breaking space
6695        try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
6696      }
6697      range.selectNodeContents(node);
6698      if (isEmpty && isElement) {
6699        range.collapse(false);
6700      } else if (isEmpty) {
6701        range.setStartAfter(node);
6702        range.setEndAfter(node);
6703      }
6704    },
6705    
6706    getTextSelectedByRange: function(textNode, range) {
6707      var textRange = range.cloneRange();
6708      textRange.selectNodeContents(textNode);
6709
6710      var intersectionRange = textRange.intersection(range);
6711      var text = intersectionRange ? intersectionRange.toString() : "";
6712      textRange.detach();
6713
6714      return text;
6715    },
6716
6717    isAppliedToRange: function(range) {
6718      var ancestors = [],
6719          ancestor,
6720          textNodes = range.getNodes([wysihtml5.TEXT_NODE]);
6721      if (!textNodes.length) {
6722        ancestor = this.getAncestorWithClass(range.startContainer);
6723        return ancestor ? [ancestor] : false;
6724      }
6725      
6726      for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
6727        selectedText = this.getTextSelectedByRange(textNodes[i], range);
6728        ancestor = this.getAncestorWithClass(textNodes[i]);
6729        if (selectedText != "" && !ancestor) {
6730          return false;
6731        } else {
6732          ancestors.push(ancestor);
6733        }
6734      }
6735      return ancestors;
6736    },
6737
6738    toggleRange: function(range) {
6739      if (this.isAppliedToRange(range)) {
6740        this.undoToRange(range);
6741      } else {
6742        this.applyToRange(range);
6743      }
6744    }
6745  };
6746
6747  wysihtml5.selection.HTMLApplier = HTMLApplier;
6748  
6749})(wysihtml5, rangy);/**
6750 * Rich Text Query/Formatting Commands
6751 * 
6752 * @example
6753 *    var commands = new wysihtml5.Commands(editor);
6754 */
6755wysihtml5.Commands = Base.extend(
6756  /** @scope wysihtml5.Commands.prototype */ {
6757  constructor: function(editor) {
6758    this.editor   = editor;
6759    this.composer = editor.composer;
6760    this.doc      = this.composer.doc;
6761  },
6762  
6763  /**
6764   * Check whether the browser supports the given command
6765   *
6766   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6767   * @example
6768   *    commands.supports("createLink");
6769   */
6770  support: function(command) {
6771    return wysihtml5.browser.supportsCommand(this.doc, command);
6772  },
6773  
6774  /**
6775   * Check whether the browser supports the given command
6776   *
6777   * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
6778   * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
6779   * @example
6780   *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
6781   */
6782  exec: function(command, value) {
6783    var obj     = wysihtml5.commands[command],
6784        args    = wysihtml5.lang.array(arguments).get(),
6785        method  = obj && obj.exec,
6786        result  = null;
6787    
6788    this.editor.fire("beforecommand:composer");
6789    
6790    if (method) {
6791      args.unshift(this.composer);
6792      result = method.apply(obj, args);
6793    } else {
6794      try {
6795        // try/catch for buggy firefox
6796        result = this.doc.execCommand(command, false, value);
6797      } catch(e) {}
6798    }
6799    
6800    this.editor.fire("aftercommand:composer");
6801    return result;
6802  },
6803  
6804  /**
6805   * Check whether the current command is active
6806   * If the caret is within a bold text, then calling this with command "bold" should return true
6807   *
6808   * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
6809   * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
6810   * @return {Boolean} Whether the command is active
6811   * @example
6812   *    var isCurrentSelectionBold = commands.state("bold");
6813   */
6814  state: function(command, commandValue) {
6815    var obj     = wysihtml5.commands[command],
6816        args    = wysihtml5.lang.array(arguments).get(),
6817        method  = obj && obj.state;
6818    if (method) {
6819      args.unshift(this.composer);
6820      return method.apply(obj, args);
6821    } else {
6822      try {
6823        // try/catch for buggy firefox
6824        return this.doc.queryCommandState(command);
6825      } catch(e) {
6826        return false;
6827      }
6828    }
6829  },
6830  
6831  /**
6832   * Get the current command's value
6833   *
6834   * @param {String} command The command string which to check (eg. "formatBlock")
6835   * @return {String} The command value
6836   * @example
6837   *    var currentBlockElement = commands.value("formatBlock");
6838   */
6839  value: function(command) {
6840    var obj     = wysihtml5.commands[command],
6841        method  = obj && obj.value;
6842    if (method) {
6843      return method.call(obj, this.composer, command);
6844    } else {
6845      try {
6846        // try/catch for buggy firefox
6847        return this.doc.queryCommandValue(command);
6848      } catch(e) {
6849        return null;
6850      }
6851    }
6852  }
6853});
6854(function(wysihtml5) {
6855  var undef;
6856  
6857  wysihtml5.commands.bold = {
6858    exec: function(composer, command) {
6859      return wysihtml5.commands.formatInline.exec(composer, command, "b");
6860    },
6861
6862    state: function(composer, command, color) {
6863      // element.ownerDocument.queryCommandState("bold") results:
6864      // firefox: only <b>
6865      // chrome:  <b>, <strong>, <h1>, <h2>, ...
6866      // ie:      <b>, <strong>
6867      // opera:   <b>, <strong>
6868      return wysihtml5.commands.formatInline.state(composer, command, "b");
6869    },
6870
6871    value: function() {
6872      return undef;
6873    }
6874  };
6875})(wysihtml5);
6876
6877(function(wysihtml5) {
6878  var undef,
6879      NODE_NAME = "A",
6880      dom       = wysihtml5.dom;
6881  
6882  function _removeFormat(composer, anchors) {
6883    var length  = anchors.length,
6884        i       = 0,
6885        anchor,
6886        codeElement,
6887        textContent;
6888    for (; i<length; i++) {
6889      anchor      = anchors[i];
6890      codeElement = dom.getParentElement(anchor, { nodeName: "code" });
6891      textContent = dom.getTextContent(anchor);
6892
6893      // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
6894      // else replace <a> with its childNodes
6895      if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
6896        // <code> element is used to prevent later auto-linking of the content
6897        codeElement = dom.renameElement(anchor, "code");
6898      } else {
6899        dom.replaceWithChildNodes(anchor);
6900      }
6901    }
6902  }
6903
6904  function _format(composer, attributes) {
6905    var doc             = composer.doc,
6906        tempClass       = "_wysihtml5-temp-" + (+new Date()),
6907        tempClassRegExp = /non-matching-class/g,
6908        i               = 0,
6909        length,
6910        anchors,
6911        anchor,
6912        hasElementChild,
6913        isEmpty,
6914        elementToSetCaretAfter,
6915        textContent,
6916        whiteSpace,
6917        j;
6918    wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp);
6919    anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
6920    length  = anchors.length;
6921    for (; i<length; i++) {
6922      anchor = anchors[i];
6923      anchor.removeAttribute("class");
6924      for (j in attributes) {
6925        anchor.setAttribute(j, attributes[j]);
6926      }
6927    }
6928
6929    elementToSetCaretAfter = anchor;
6930    if (length === 1) {
6931      textContent = dom.getTextContent(anchor);
6932      hasElementChild = !!anchor.querySelector("*");
6933      isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
6934      if (!hasElementChild && isEmpty) {
6935        dom.setTextContent(anchor, attributes.text || anchor.href);
6936        whiteSpace = doc.createTextNode(" ");
6937        composer.selection.setAfter(anchor);
6938        composer.selection.insertNode(whiteSpace);
6939        elementToSetCaretAfter = whiteSpace;
6940      }
6941    }
6942    composer.selection.setAfter(elementToSetCaretAfter);
6943  }
6944  
6945  wysihtml5.commands.createLink = {
6946    /**
6947     * TODO: Use HTMLApplier or formatInline here
6948     *
6949     * Turns selection into a link
6950     * If selection is already a link, it removes the link and wraps it with a <code> element
6951     * The <code> element is needed to avoid auto linking
6952     * 
6953     * @example
6954     *    // either ...
6955     *    wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
6956     *    // ... or ...
6957     *    wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
6958     */
6959    exec: function(composer, command, value) {
6960      var anchors = this.state(composer, command);
6961      if (anchors) {
6962        // Selection contains links
6963        composer.selection.executeAndRestore(function() {
6964          _removeFormat(composer, anchors);
6965        });
6966      } else {
6967        // Create links
6968        value = typeof(value) === "object" ? value : { href: value };
6969        _format(composer, value);
6970      }
6971    },
6972
6973    state: function(composer, command) {
6974      return wysihtml5.commands.formatInline.state(composer, command, "A");
6975    },
6976
6977    value: function() {
6978      return undef;
6979    }
6980  };
6981})(wysihtml5);/**
6982 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
6983 * which we don't want
6984 * Instead we set a css class
6985 */
6986(function(wysihtml5) {
6987  var undef,
6988      REG_EXP = /wysiwyg-font-size-[a-z\-]+/g;
6989  
6990  wysihtml5.commands.fontSize = {
6991    exec: function(composer, command, size) {
6992      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6993    },
6994
6995    state: function(composer, command, size) {
6996      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
6997    },
6998
6999    value: function() {
7000      return undef;
7001    }
7002  };
7003})(wysihtml5);
7004/**
7005 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
7006 * which we don't want
7007 * Instead we set a css class
7008 */
7009(function(wysihtml5) {
7010  var undef,
7011      REG_EXP = /wysiwyg-color-[a-z]+/g;
7012  
7013  wysihtml5.commands.foreColor = {
7014    exec: function(composer, command, color) {
7015      return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
7016    },
7017
7018    state: function(composer, command, color) {
7019      return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
7020    },
7021
7022    value: function() {
7023      return undef;
7024    }
7025  };
7026})(wysihtml5);(function(wysihtml5) {
7027  var undef,
7028      dom                     = wysihtml5.dom,
7029      DEFAULT_NODE_NAME       = "DIV",
7030      // Following elements are grouped
7031      // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
7032      // instead of creating a H4 within a H1 which would result in semantically invalid html
7033      BLOCK_ELEMENTS_GROUP    = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME];
7034  
7035  /**
7036   * Remove similiar classes (based on classRegExp)
7037   * and add the desired class name
7038   */
7039  function _addClass(element, className, classRegExp) {
7040    if (element.className) {
7041      _removeClass(element, classRegExp);
7042      element.className += " " + className;
7043    } else {
7044      element.className = className;
7045    }
7046  }
7047
7048  function _removeClass(element, classRegExp) {
7049    element.className = element.className.replace(classRegExp, "");
7050  }
7051
7052  /**
7053   * Check whether given node is a text node and whether it's empty
7054   */
7055  function _isBlankTextNode(node) {
7056    return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim();
7057  }
7058
7059  /**
7060   * Returns previous sibling node that is not a blank text node
7061   */
7062  function _getPreviousSiblingThatIsNotBlank(node) {
7063    var previousSibling = node.previousSibling;
7064    while (previousSibling && _isBlankTextNode(previousSibling)) {
7065      previousSibling = previousSibling.previousSibling;
7066    }
7067    return previousSibling;
7068  }
7069
7070  /**
7071   * Returns next sibling node that is not a blank text node
7072   */
7073  function _getNextSiblingThatIsNotBlank(node) {
7074    var nextSibling = node.nextSibling;
7075    while (nextSibling && _isBlankTextNode(nextSibling)) {
7076      nextSibling = nextSibling.nextSibling;
7077    }
7078    return nextSibling;
7079  }
7080
7081  /**
7082   * Adds line breaks before and after the given node if the previous and next siblings
7083   * aren't already causing a visual line break (block element or <br>)
7084   */
7085  function _addLineBreakBeforeAndAfter(node) {
7086    var doc             = node.ownerDocument,
7087        nextSibling     = _getNextSiblingThatIsNotBlank(node),
7088        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7089
7090    if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
7091      node.parentNode.insertBefore(doc.createElement("br"), nextSibling);
7092    }
7093    if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
7094      node.parentNode.insertBefore(doc.createElement("br"), node);
7095    }
7096  }
7097
7098  /**
7099   * Removes line breaks before and after the given node
7100   */
7101  function _removeLineBreakBeforeAndAfter(node) {
7102    var nextSibling     = _getNextSiblingThatIsNotBlank(node),
7103        previousSibling = _getPreviousSiblingThatIsNotBlank(node);
7104
7105    if (nextSibling && _isLineBreak(nextSibling)) {
7106      nextSibling.parentNode.removeChild(nextSibling);
7107    }
7108    if (previousSibling && _isLineBreak(previousSibling)) {
7109      previousSibling.parentNode.removeChild(previousSibling);
7110    }
7111  }
7112
7113  function _removeLastChildIfLineBreak(node) {
7114    var lastChild = node.lastChild;
7115    if (lastChild && _isLineBreak(lastChild)) {
7116      lastChild.parentNode.removeChild(lastChild);
7117    }
7118  }
7119
7120  function _isLineBreak(node) {
7121    return node.nodeName === "BR";
7122  }
7123
7124  /**
7125   * Checks whether the elment causes a visual line break
7126   * (<br> or block elements)
7127   */
7128  function _isLineBreakOrBlockElement(element) {
7129    if (_isLineBreak(element)) {
7130      return true;
7131    }
7132
7133    if (dom.getStyle("display").from(element) === "block") {
7134      return true;
7135    }
7136
7137    return false;
7138  }
7139
7140  /**
7141   * Execute native query command
7142   * and if necessary modify the inserted node's className
7143   */
7144  function _execCommand(doc, command, nodeName, className) {
7145    if (className) {
7146      var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
7147        var target = event.target,
7148            displayStyle;
7149        if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
7150          return;
7151        }
7152        displayStyle = dom.getStyle("display").from(target);
7153        if (displayStyle.substr(0, 6) !== "inline") {
7154          // Make sure that only block elements receive the given class
7155          target.className += " " + className;
7156        }
7157      });
7158    }
7159    doc.execCommand(command, false, nodeName);
7160    if (eventListener) {
7161      eventListener.stop();
7162    }
7163  }
7164
7165  function _selectLineAndWrap(composer, element) {
7166    composer.selection.selectLine();
7167    composer.selection.surround(element);
7168    _removeLineBreakBeforeAndAfter(element);
7169    _removeLastChildIfLineBreak(element);
7170    composer.selection.selectNode(element);
7171  }
7172
7173  function _hasClasses(element) {
7174    return !!wysihtml5.lang.string(element.className).trim();
7175  }
7176  
7177  wysihtml5.commands.formatBlock = {
7178    exec: function(composer, command, nodeName, className, classRegExp) {
7179      var doc          = composer.doc,
7180          blockElement = this.state(composer, command, nodeName, className, classRegExp),
7181          selectedNode;
7182
7183      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7184
7185      if (blockElement) {
7186        composer.selection.executeAndRestoreSimple(function() {
7187          if (classRegExp) {
7188            _removeClass(blockElement, classRegExp);
7189          }
7190          var hasClasses = _hasClasses(blockElement);
7191          if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) {
7192            // Insert a line break afterwards and beforewards when there are siblings
7193            // that are not of type line break or block element
7194            _addLineBreakBeforeAndAfter(blockElement);
7195            dom.replaceWithChildNodes(blockElement);
7196          } else if (hasClasses) {
7197            // Make sure that styling is kept by renaming the element to <div> and copying over the class name
7198            dom.renameElement(blockElement, DEFAULT_NODE_NAME);
7199          }
7200        });
7201        return;
7202      }
7203
7204      // Find similiar block element and rename it (<h2 class="foo"></h2>  =>  <h1 class="foo"></h1>)
7205      if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
7206        selectedNode = composer.selection.getSelectedNode();
7207        blockElement = dom.getParentElement(selectedNode, {
7208          nodeName: BLOCK_ELEMENTS_GROUP
7209        });
7210
7211        if (blockElement) {
7212          composer.selection.executeAndRestoreSimple(function() {
7213            // Rename current block element to new block element and add class
7214            if (nodeName) {
7215              blockElement = dom.renameElement(blockElement, nodeName);
7216            }
7217            if (className) {
7218              _addClass(blockElement, className, classRegExp);
7219            }
7220          });
7221          return;
7222        }
7223      }
7224
7225      if (composer.commands.support(command)) {
7226        _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className);
7227        return;
7228      }
7229
7230      blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME);
7231      if (className) {
7232        blockElement.className = className;
7233      }
7234      _selectLineAndWrap(composer, blockElement);
7235    },
7236
7237    state: function(composer, command, nodeName, className, classRegExp) {
7238      nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
7239      var selectedNode = composer.selection.getSelectedNode();
7240      return dom.getParentElement(selectedNode, {
7241        nodeName:     nodeName,
7242        className:    className,
7243        classRegExp:  classRegExp
7244      });
7245    },
7246
7247    value: function() {
7248      return undef;
7249    }
7250  };
7251})(wysihtml5);/**
7252 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
7253 *
7254 *   #1 caret in unformatted text:
7255 *      abcdefg|
7256 *   output:
7257 *      abcdefg<b>|</b>
7258 *   
7259 *   #2 unformatted text selected:
7260 *      abc|deg|h
7261 *   output:
7262 *      abc<b>|deg|</b>h
7263 *   
7264 *   #3 unformatted text selected across boundaries:
7265 *      ab|c <span>defg|h</span>
7266 *   output:
7267 *      ab<b>|c </b><span><b>defg</b>|h</span>
7268 *
7269 *   #4 formatted text entirely selected
7270 *      <b>|abc|</b>
7271 *   output:
7272 *      |abc|
7273 *
7274 *   #5 formatted text partially selected
7275 *      <b>ab|c|</b>
7276 *   output:
7277 *      <b>ab</b>|c|
7278 *
7279 *   #6 formatted text selected across boundaries
7280 *      <span>ab|c</span> <b>de|fgh</b>
7281 *   output:
7282 *      <span>ab|c</span> de|<b>fgh</b>
7283 */
7284(function(wysihtml5) {
7285  var undef,
7286      // Treat <b> as <strong> and vice versa
7287      ALIAS_MAPPING = {
7288        "strong": "b",
7289        "em":     "i",
7290        "b":      "strong",
7291        "i":      "em"
7292      },
7293      htmlApplier = {};
7294  
7295  function _getTagNames(tagName) {
7296    var alias = ALIAS_MAPPING[tagName];
7297    return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
7298  }
7299  
7300  function _getApplier(tagName, className, classRegExp) {
7301    var identifier = tagName + ":" + className;
7302    if (!htmlApplier[identifier]) {
7303      htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true);
7304    }
7305    return htmlApplier[identifier];
7306  }
7307  
7308  wysihtml5.commands.formatInline = {
7309    exec: function(composer, command, tagName, className, classRegExp) {
7310      var range = composer.selection.getRange();
7311      if (!range) {
7312        return false;
7313      }
7314      _getApplier(tagName, className, classRegExp).toggleRange(range);
7315      composer.selection.setSelection(range);
7316    },
7317
7318    state: function(composer, command, tagName, className, classRegExp) {
7319      var doc           = composer.doc,
7320          aliasTagName  = ALIAS_MAPPING[tagName] || tagName,
7321          range;
7322
7323      // Check whether the document contains a node with the desired tagName
7324      if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
7325          !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
7326        return false;
7327      }
7328
7329       // Check whether the document contains a node with the desired className
7330      if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
7331         return false;
7332      }
7333
7334      range = composer.selection.getRange();
7335      if (!range) {
7336        return false;
7337      }
7338
7339      return _getApplier(tagName, className, classRegExp).isAppliedToRange(range);
7340    },
7341
7342    value: function() {
7343      return undef;
7344    }
7345  };
7346})(wysihtml5);(function(wysihtml5) {
7347  var undef;
7348  
7349  wysihtml5.commands.insertHTML = {
7350    exec: function(composer, command, html) {
7351      if (composer.commands.support(command)) {
7352        composer.doc.execCommand(command, false, html);
7353      } else {
7354        composer.selection.insertHTML(html);
7355      }
7356    },
7357
7358    state: function() {
7359      return false;
7360    },
7361
7362    value: function() {
7363      return undef;
7364    }
7365  };
7366})(wysihtml5);(function(wysihtml5) {
7367  var NODE_NAME = "IMG";
7368  
7369  wysihtml5.commands.insertImage = {
7370    /**
7371     * Inserts an <img>
7372     * If selection is already an image link, it removes it
7373     * 
7374     * @example
7375     *    // either ...
7376     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
7377     *    // ... or ...
7378     *    wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
7379     */
7380    exec: function(composer, command, value) {
7381      value = typeof(value) === "object" ? value : { src: value };
7382
7383      var doc     = composer.doc,
7384          image   = this.state(composer),
7385          textNode,
7386          i,
7387          parent;
7388
7389      if (image) {
7390        // Image already selected, set the caret before it and delete it
7391        composer.selection.setBefore(image);
7392        parent = image.parentNode;
7393        parent.removeChild(image);
7394
7395        // and it's parent <a> too if it hasn't got any other relevant child nodes
7396        wysihtml5.dom.removeEmptyTextNodes(parent);
7397        if (parent.nodeName === "A" && !parent.firstChild) {
7398          composer.selection.setAfter(parent);
7399          parent.parentNode.removeChild(parent);
7400        }
7401
7402        // firefox and ie sometimes don't remove the image handles, even though the image got removed
7403        wysihtml5.quirks.redraw(composer.element);
7404        return;
7405      }
7406
7407      image = doc.createElement(NODE_NAME);
7408
7409      for (i in value) {
7410        image[i] = value[i];
7411      }
7412
7413      composer.selection.insertNode(image);
7414      if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
7415        textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
7416        composer.selection.insertNode(textNode);
7417        composer.selection.setAfter(textNode);
7418      } else {
7419        composer.selection.setAfter(image);
7420      }
7421    },
7422
7423    state: function(composer) {
7424      var doc = composer.doc,
7425          selectedNode,
7426          text,
7427          imagesInSelection;
7428
7429      if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
7430        return false;
7431      }
7432
7433      selectedNode = composer.selection.getSelectedNode();
7434      if (!selectedNode) {
7435        return false;
7436      }
7437
7438      if (selectedNode.nodeName === NODE_NAME) {
7439        // This works perfectly in IE
7440        return selectedNode;
7441      }
7442
7443      if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
7444        return false;
7445      }
7446
7447      text = composer.selection.getText();
7448      text = wysihtml5.lang.string(text).trim();
7449      if (text) {
7450        return false;
7451      }
7452
7453      imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
7454        return node.nodeName === "IMG";
7455      });
7456
7457      if (imagesInSelection.length !== 1) {
7458        return false;
7459      }
7460
7461      return imagesInSelection[0];
7462    },
7463
7464    value: function(composer) {
7465      var image = this.state(composer);
7466      return image && image.src;
7467    }
7468  };
7469})(wysihtml5);(function(wysihtml5) {
7470  var undef,
7471      LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
7472  
7473  wysihtml5.commands.insertLineBreak = {
7474    exec: function(composer, command) {
7475      if (composer.commands.support(command)) {
7476        composer.doc.execCommand(command, false, null);
7477        if (!wysihtml5.browser.autoScrollsToCaret()) {
7478          composer.selection.scrollIntoView();
7479        }
7480      } else {
7481        composer.commands.exec("insertHTML", LINE_BREAK);
7482      }
7483    },
7484
7485    state: function() {
7486      return false;
7487    },
7488
7489    value: function() {
7490      return undef;
7491    }
7492  };
7493})(wysihtml5);(function(wysihtml5) {
7494  var undef;
7495  
7496  wysihtml5.commands.insertOrderedList = {
7497    exec: function(composer, command) {
7498      var doc           = composer.doc,
7499          selectedNode  = composer.selection.getSelectedNode(),
7500          list          = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
7501          otherList     = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
7502          tempClassName =  "_wysihtml5-temp-" + new Date().getTime(),
7503          isEmpty,
7504          tempElement;
7505      
7506      if (composer.commands.support(command)) {
7507        doc.execCommand(command, false, null);
7508        return;
7509      }
7510      
7511      if (list) {
7512        // Unwrap list
7513        // <ol><li>foo</li><li>bar</li></ol>
7514        // becomes:
7515        // foo<br>bar<br>
7516        composer.selection.executeAndRestoreSimple(function() {
7517          wysihtml5.dom.resolveList(list);
7518        });
7519      } else if (otherList) {
7520        // Turn an unordered list into an ordered list
7521        // <ul><li>foo</li><li>bar</li></ul>
7522        // becomes:
7523        // <ol><li>foo</li><li>bar</li></ol>
7524        composer.selection.executeAndRestoreSimple(function() {
7525          wysihtml5.dom.renameElement(otherList, "ol");
7526        });
7527      } else {
7528        // Create list
7529        composer.commands.exec("formatBlock", "div", tempClassName);
7530        tempElement = doc.querySelector("." + tempClassName);
7531        isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7532        composer.selection.executeAndRestoreSimple(function() {
7533          list = wysihtml5.dom.convertToList(tempElement, "ol");
7534        });
7535        if (isEmpty) {
7536          composer.selection.selectNode(list.querySelector("li"));
7537        }
7538      }
7539    },
7540    
7541    state: function(composer) {
7542      var selectedNode = composer.selection.getSelectedNode();
7543      return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" });
7544    },
7545
7546    value: function() {
7547      return undef;
7548    }
7549  };
7550})(wysihtml5);(function(wysihtml5) {
7551  var undef;
7552  
7553  wysihtml5.commands.insertUnorderedList = {
7554    exec: function(composer, command) {
7555      var doc           = composer.doc,
7556          selectedNode  = composer.selection.getSelectedNode(),
7557          list          = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }),
7558          otherList     = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }),
7559          tempClassName =  "_wysihtml5-temp-" + new Date().getTime(),
7560          isEmpty,
7561          tempElement;
7562      
7563      if (composer.commands.support(command)) {
7564        doc.execCommand(command, false, null);
7565        return;
7566      }
7567      
7568      if (list) {
7569        // Unwrap list
7570        // <ul><li>foo</li><li>bar</li></ul>
7571        // becomes:
7572        // foo<br>bar<br>
7573        composer.selection.executeAndRestoreSimple(function() {
7574          wysihtml5.dom.resolveList(list);
7575        });
7576      } else if (otherList) {
7577        // Turn an ordered list into an unordered list
7578        // <ol><li>foo</li><li>bar</li></ol>
7579        // becomes:
7580        // <ul><li>foo</li><li>bar</li></ul>
7581        composer.selection.executeAndRestoreSimple(function() {
7582          wysihtml5.dom.renameElement(otherList, "ul");
7583        });
7584      } else {
7585        // Create list
7586        composer.commands.exec("formatBlock", "div", tempClassName);
7587        tempElement = doc.querySelector("." + tempClassName);
7588        isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE;
7589        composer.selection.executeAndRestoreSimple(function() {
7590          list = wysihtml5.dom.convertToList(tempElement, "ul");
7591        });
7592        if (isEmpty) {
7593          composer.selection.selectNode(list.querySelector("li"));
7594        }
7595      }
7596    },
7597    
7598    state: function(composer) {
7599      var selectedNode = composer.selection.getSelectedNode();
7600      return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" });
7601    },
7602
7603    value: function() {
7604      return undef;
7605    }
7606  };
7607})(wysihtml5);(function(wysihtml5) {
7608  var undef;
7609  
7610  wysihtml5.commands.italic = {
7611    exec: function(composer, command) {
7612      return wysihtml5.commands.formatInline.exec(composer, command, "i");
7613    },
7614
7615    state: function(composer, command, color) {
7616      // element.ownerDocument.queryCommandState("italic") results:
7617      // firefox: only <i>
7618      // chrome:  <i>, <em>, <blockquote>, ...
7619      // ie:      <i>, <em>
7620      // opera:   only <i>
7621      return wysihtml5.commands.formatInline.state(composer, command, "i");
7622    },
7623
7624    value: function() {
7625      return undef;
7626    }
7627  };
7628})(wysihtml5);(function(wysihtml5) {
7629  var undef,
7630      CLASS_NAME  = "wysiwyg-text-align-center",
7631      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7632  
7633  wysihtml5.commands.justifyCenter = {
7634    exec: function(composer, command) {
7635      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7636    },
7637
7638    state: function(composer, command) {
7639      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7640    },
7641
7642    value: function() {
7643      return undef;
7644    }
7645  };
7646})(wysihtml5);(function(wysihtml5) {
7647  var undef,
7648      CLASS_NAME  = "wysiwyg-text-align-left",
7649      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7650  
7651  wysihtml5.commands.justifyLeft = {
7652    exec: function(composer, command) {
7653      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7654    },
7655
7656    state: function(composer, command) {
7657      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7658    },
7659
7660    value: function() {
7661      return undef;
7662    }
7663  };
7664})(wysihtml5);(function(wysihtml5) {
7665  var undef,
7666      CLASS_NAME  = "wysiwyg-text-align-right",
7667      REG_EXP     = /wysiwyg-text-align-[a-z]+/g;
7668  
7669  wysihtml5.commands.justifyRight = {
7670    exec: function(composer, command) {
7671      return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7672    },
7673
7674    state: function(composer, command) {
7675      return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
7676    },
7677
7678    value: function() {
7679      return undef;
7680    }
7681  };
7682})(wysihtml5);(function(wysihtml5) {
7683  var undef;
7684  wysihtml5.commands.underline = {
7685    exec: function(composer, command) {
7686      return wysihtml5.commands.formatInline.exec(composer, command, "u");
7687    },
7688
7689    state: function(composer, command) {
7690      return wysihtml5.commands.formatInline.state(composer, command, "u");
7691    },
7692
7693    value: function() {
7694      return undef;
7695    }
7696  };
7697})(wysihtml5);/**
7698 * Undo Manager for wysihtml5
7699 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
7700 */
7701(function(wysihtml5) {
7702  var Z_KEY               = 90,
7703      Y_KEY               = 89,
7704      BACKSPACE_KEY       = 8,
7705      DELETE_KEY          = 46,
7706      MAX_HISTORY_ENTRIES = 40,
7707      UNDO_HTML           = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7708      REDO_HTML           = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
7709      dom                 = wysihtml5.dom;
7710  
7711  function cleanTempElements(doc) {
7712    var tempElement;
7713    while (tempElement = doc.querySelector("._wysihtml5-temp")) {
7714      tempElement.parentNode.removeChild(tempElement);
7715    }
7716  }
7717  
7718  wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
7719    /** @scope wysihtml5.UndoManager.prototype */ {
7720    constructor: function(editor) {
7721      this.editor = editor;
7722      this.composer = editor.composer;
7723      this.element = this.composer.element;
7724      this.history = [this.composer.getValue()];
7725      this.position = 1;
7726      
7727      // Undo manager currently only supported in browsers who have the insertHTML command (not IE)
7728      if (this.composer.commands.support("insertHTML")) {
7729        this._observe();
7730      }
7731    },
7732    
7733    _observe: function() {
7734      var that      = this,
7735          doc       = this.composer.sandbox.getDocument(),
7736          lastKey;
7737          
7738      // Catch CTRL+Z and CTRL+Y
7739      dom.observe(this.element, "keydown", function(event) {
7740        if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
7741          return;
7742        }
7743        
7744        var keyCode = event.keyCode,
7745            isUndo = keyCode === Z_KEY && !event.shiftKey,
7746            isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
7747        
7748        if (isUndo) {
7749          that.undo();
7750          event.preventDefault();
7751        } else if (isRedo) {
7752          that.redo();
7753          event.preventDefault();
7754        }
7755      });
7756      
7757      // Catch delete and backspace
7758      dom.observe(this.element, "keydown", function(event) {
7759        var keyCode = event.keyCode;
7760        if (keyCode === lastKey) {
7761          return;
7762        }
7763        
7764        lastKey = keyCode;
7765        
7766        if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
7767          that.transact();
7768        }
7769      });
7770      
7771      // Now this is very hacky:
7772      // These days browsers don't offer a undo/redo event which we could hook into
7773      // to be notified when the user hits undo/redo in the contextmenu.
7774      // Therefore we simply insert two elements as soon as the contextmenu gets opened.
7775      // The last element being inserted will be immediately be removed again by a exexCommand("undo")
7776      //  => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu
7777      //  => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu
7778      if (wysihtml5.browser.hasUndoInContextMenu()) {
7779        var interval, observed, cleanUp = function() {
7780          cleanTempElements(doc);
7781          clearInterval(interval);
7782        };
7783        
7784        dom.observe(this.element, "contextmenu", function() {
7785          cleanUp();
7786          that.composer.selection.executeAndRestoreSimple(function() {
7787            if (that.element.lastChild) {
7788              that.composer.selection.setAfter(that.element.lastChild);
7789            }
7790
7791            // enable undo button in context menu
7792            doc.execCommand("insertHTML", false, UNDO_HTML);
7793            // enable redo button in context menu
7794            doc.execCommand("insertHTML", false, REDO_HTML);
7795            doc.execCommand("undo", false, null);
7796          });
7797
7798          interval = setInterval(function() {
7799            if (doc.getElementById("_wysihtml5-redo")) {
7800              cleanUp();
7801              that.redo();
7802            } else if (!doc.getElementById("_wysihtml5-undo")) {
7803              cleanUp();
7804              that.undo();
7805            }
7806          }, 400);
7807
7808          if (!observed) {
7809            observed = true;
7810            dom.observe(document, "mousedown", cleanUp);
7811            dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp);
7812          }
7813        });
7814      }
7815      
7816      this.editor
7817        .observe("newword:composer", function() {
7818          that.transact();
7819        })
7820        
7821        .observe("beforecommand:composer", function() {
7822          that.transact();
7823        });
7824    },
7825    
7826    transact: function() {
7827      var previousHtml  = this.history[this.position - 1],
7828          currentHtml   = this.composer.getValue();
7829      
7830      if (currentHtml == previousHtml) {
7831        return;
7832      }
7833      
7834      var length = this.history.length = this.position;
7835      if (length > MAX_HISTORY_ENTRIES) {
7836        this.history.shift();
7837        this.position--;
7838      }
7839      
7840      this.position++;
7841      this.history.push(currentHtml);
7842    },
7843    
7844    undo: function() {
7845      this.transact();
7846      
7847      if (this.position <= 1) {
7848        return;
7849      }
7850      
7851      this.set(this.history[--this.position - 1]);
7852      this.editor.fire("undo:composer");
7853    },
7854    
7855    redo: function() {
7856      if (this.position >= this.history.length) {
7857        return;
7858      }
7859      
7860      this.set(this.history[++this.position - 1]);
7861      this.editor.fire("redo:composer");
7862    },
7863    
7864    set: function(html) {
7865      this.composer.setValue(html);
7866      this.editor.focus(true);
7867    }
7868  });
7869})(wysihtml5);
7870/**
7871 * TODO: the following methods still need unit test coverage
7872 */
7873wysihtml5.views.View = Base.extend(
7874  /** @scope wysihtml5.views.View.prototype */ {
7875  constructor: function(parent, textareaElement, config) {
7876    this.parent   = parent;
7877    this.element  = textareaElement;
7878    this.config   = config;
7879    
7880    this._observeViewChange();
7881  },
7882  
7883  _observeViewChange: function() {
7884    var that = this;
7885    this.parent.observe("beforeload", function() {
7886      that.parent.observe("change_view", function(view) {
7887        if (view === that.name) {
7888          that.parent.currentView = that;
7889          that.show();
7890          // Using tiny delay here to make sure that the placeholder is set before focusing
7891          setTimeout(function() { that.focus(); }, 0);
7892        } else {
7893          that.hide();
7894        }
7895      });
7896    });
7897  },
7898  
7899  focus: function() {
7900    if (this.element.ownerDocument.querySelector(":focus") === this.element) {
7901      return;
7902    }
7903    
7904    try { this.element.focus(); } catch(e) {}
7905  },
7906  
7907  hide: function() {
7908    this.element.style.display = "none";
7909  },
7910  
7911  show: function() {
7912    this.element.style.display = "";
7913  },
7914  
7915  disable: function() {
7916    this.element.setAttribute("disabled", "disabled");
7917  },
7918  
7919  enable: function() {
7920    this.element.removeAttribute("disabled");
7921  }
7922});(function(wysihtml5) {
7923  var dom       = wysihtml5.dom,
7924      browser   = wysihtml5.browser;
7925  
7926  wysihtml5.views.Composer = wysihtml5.views.View.extend(
7927    /** @scope wysihtml5.views.Composer.prototype */ {
7928    name: "composer",
7929
7930    // Needed for firefox in order to display a proper caret in an empty contentEditable
7931    CARET_HACK: "<br>",
7932
7933    constructor: function(parent, textareaElement, config) {
7934      this.base(parent, textareaElement, config);
7935      this.textarea = this.parent.textarea;
7936      this._initSandbox();
7937    },
7938
7939    clear: function() {
7940      this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
7941    },
7942
7943    getValue: function(parse) {
7944      var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
7945      
7946      if (parse) {
7947        value = this.parent.parse(value);
7948      }
7949
7950      // Replace all "zero width no breaking space" chars
7951      // which are used as hacks to enable some functionalities
7952      // Also remove all CARET hacks that somehow got left
7953      value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by("");
7954
7955      return value;
7956    },
7957
7958    setValue: function(html, parse) {
7959      if (parse) {
7960        html = this.parent.parse(html);
7961      }
7962      this.element.innerHTML = html;
7963    },
7964
7965    show: function() {
7966      this.iframe.style.display = this._displayStyle || "";
7967
7968      // Firefox needs this, otherwise contentEditable becomes uneditable
7969      this.disable();
7970      this.enable();
7971    },
7972
7973    hide: function() {
7974      this._displayStyle = dom.getStyle("display").from(this.iframe);
7975      if (this._displayStyle === "none") {
7976        this._displayStyle = null;
7977      }
7978      this.iframe.style.display = "none";
7979    },
7980
7981    disable: function() {
7982      this.element.removeAttribute("contentEditable");
7983      this.base();
7984    },
7985
7986    enable: function() {
7987      this.element.setAttribute("contentEditable", "true");
7988      this.base();
7989    },
7990
7991    focus: function(setToEnd) {
7992      // IE 8 fires the focus event after .focus()
7993      // This is needed by our simulate_placeholder.js to work
7994      // therefore we clear it ourselves this time
7995      if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
7996        this.clear();
7997      }
7998      
7999      this.base();
8000      
8001      var lastChild = this.element.lastChild;
8002      if (setToEnd && lastChild) {
8003        if (lastChild.nodeName === "BR") {
8004          this.selection.setBefore(this.element.lastChild);
8005        } else {
8006          this.selection.setAfter(this.element.lastChild);
8007        }
8008      }
8009    },
8010
8011    getTextContent: function() {
8012      return dom.getTextContent(this.element);
8013    },
8014
8015    hasPlaceholderSet: function() {
8016      return this.getTextContent() == this.textarea.element.getAttribute("placeholder");
8017    },
8018
8019    isEmpty: function() {
8020      var innerHTML               = this.element.innerHTML,
8021          elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";
8022      return innerHTML === ""              || 
8023             innerHTML === this.CARET_HACK ||
8024             this.hasPlaceholderSet()      ||
8025             (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue));
8026    },
8027
8028    _initSandbox: function() {
8029      var that = this;
8030      
8031      this.sandbox = new dom.Sandbox(function() {
8032        that._create();
8033      }, {
8034        stylesheets:  this.config.stylesheets
8035      });
8036      this.iframe  = this.sandbox.getIframe();
8037
8038      // Create hidden field which tells the server after submit, that the user used an wysiwyg editor
8039      var hiddenField = document.createElement("input");
8040      hiddenField.type   = "hidden";
8041      hiddenField.name   = "_wysihtml5_mode";
8042      hiddenField.value  = 1;
8043
8044      // Store reference to current wysihtml5 instance on the textarea element
8045      var textareaElement = this.textarea.element;
8046      dom.insert(this.iframe).after(textareaElement);
8047      dom.insert(hiddenField).after(textareaElement);
8048    },
8049
8050    _create: function() {
8051      var that = this;
8052      
8053      this.doc                = this.sandbox.getDocument();
8054      this.element            = this.doc.body;
8055      this.textarea           = this.parent.textarea;
8056      this.element.innerHTML  = this.textarea.getValue(true);
8057      this.enable();
8058      
8059      // Make sure our selection handler is ready
8060      this.selection = new wysihtml5.Selection(this.parent);
8061      
8062      // Make sure commands dispatcher is ready
8063      this.commands  = new wysihtml5.Commands(this.parent);
8064
8065      dom.copyAttributes([
8066        "className", "spellcheck", "title", "lang", "dir", "accessKey"
8067      ]).from(this.textarea.element).to(this.element);
8068      
8069      dom.addClass(this.element, this.config.composerClassName);
8070
8071      // Make the editor look like the original textarea, by syncing styles
8072      if (this.config.style) {
8073        this.style();
8074      }
8075
8076      this.observe();
8077
8078      var name = this.config.name;
8079      if (name) {
8080        dom.addClass(this.element, name);
8081        dom.addClass(this.iframe, name);
8082      }
8083
8084      // Simulate html5 placeholder attribute on contentEditable element
8085      var placeholderText = typeof(this.config.placeholder) === "string"
8086        ? this.config.placeholder
8087        : this.textarea.element.getAttribute("placeholder");
8088      if (placeholderText) {
8089        dom.simulatePlaceholder(this.parent, this, placeholderText);
8090      }
8091      
8092      // Make sure that the browser avoids using inline styles whenever possible
8093      this.commands.exec("styleWithCSS", false);
8094
8095      this._initAutoLinking();
8096      this._initObjectResizing();
8097      this._initUndoManager();
8098
8099      // Simulate html5 autofocus on contentEditable element
8100      if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) {
8101        setTimeout(function() { that.focus(); }, 100);
8102      }
8103
8104      wysihtml5.quirks.insertLineBreakOnReturn(this);
8105
8106      // IE sometimes leaves a single paragraph, which can't be removed by the user
8107      if (!browser.clearsContentEditableCorrectly()) {
8108        wysihtml5.quirks.ensureProperClearing(this);
8109      }
8110
8111      if (!browser.clearsListsInContentEditableCorrectly()) {
8112        wysihtml5.quirks.ensureProperClearingOfLists(this);
8113      }
8114
8115      // Set up a sync that makes sure that textarea and editor have the same content
8116      if (this.initSync && this.config.sync) {
8117        this.initSync();
8118      }
8119
8120      // Okay hide the textarea, we are ready to go
8121      this.textarea.hide();
8122
8123      // Fire global (before-)load event
8124      this.parent.fire("beforeload").fire("load");
8125    },
8126
8127    _initAutoLinking: function() {
8128      var that                           = this,
8129          supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
8130          supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
8131      if (supportsDisablingOfAutoLinking) {
8132        this.commands.exec("autoUrlDetect", false);
8133      }
8134
8135      if (!this.config.autoLink) {
8136        return;
8137      }
8138
8139      // Only do the auto linking by ourselves when the browser doesn't support auto linking
8140      // OR when he supports auto linking but we were able to turn it off (IE9+)
8141      if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
8142        this.parent.observe("newword:composer", function() {
8143          that.selection.executeAndRestore(function(startContainer, endContainer) {
8144            dom.autoLink(endContainer.parentNode);
8145          });
8146        });
8147      }
8148
8149      // Assuming we have the following:
8150      //  <a href="http://www.google.de">http://www.google.de</a>
8151      // If a user now changes the url in the innerHTML we want to make sure that
8152      // it's synchronized with the href attribute (as long as the innerHTML is still a url)
8153      var // Use a live NodeList to check whether there are any links in the document
8154          links           = this.sandbox.getDocument().getElementsByTagName("a"),
8155          // The autoLink helper method reveals a reg exp to detect correct urls
8156          urlRegExp       = dom.autoLink.URL_REG_EXP,
8157          getTextContent  = function(element) {
8158            var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
8159            if (textContent.substr(0, 4) === "www.") {
8160              textContent = "http://" + textContent;
8161            }
8162            return textContent;
8163          };
8164
8165      dom.observe(this.element, "keydown", function(event) {
8166        if (!links.length) {
8167          return;
8168        }
8169
8170        var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
8171            link         = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
8172            textContent;
8173
8174        if (!link) {
8175          return;
8176        }
8177
8178        textContent = getTextContent(link);
8179        // keydown is fired before the actual content is changed
8180        // therefore we set a timeout to change the href
8181        setTimeout(function() {
8182          var newTextContent = getTextContent(link);
8183          if (newTextContent === textContent) {
8184            return;
8185          }
8186
8187          // Only set href when new href looks like a valid url
8188          if (newTextContent.match(urlRegExp)) {
8189            link.setAttribute("href", newTextContent);
8190          }
8191        }, 0);
8192      });
8193    },
8194
8195    _initObjectResizing: function() {
8196      var properties        = ["width", "height"],
8197          propertiesLength  = properties.length,
8198          element           = this.element;
8199      
8200      this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);
8201      
8202      if (this.config.allowObjectResizing) {
8203         // IE sets inline styles after resizing objects
8204         // The following lines make sure that the width/height css properties
8205         // are copied over to the width/height attributes
8206        if (browser.supportsEvent("resizeend")) {
8207          dom.observe(element, "resizeend", function(event) {
8208            var target = event.target || event.srcElement,
8209                style  = target.style,
8210                i      = 0,
8211                property;
8212            for(; i<propertiesLength; i++) {
8213              property = properties[i];
8214              if (style[property]) {
8215                target.setAttribute(property, parseInt(style[property], 10));
8216                style[property] = "";
8217              }
8218            }
8219            // After resizing IE sometimes forgets to remove the old resize handles
8220            wysihtml5.quirks.redraw(element);
8221          });
8222        }
8223      } else {
8224        if (browser.supportsEvent("resizestart")) {
8225          dom.observe(element, "resizestart", function(event) { event.preventDefault(); });
8226        }
8227      }
8228    },
8229    
8230    _initUndoManager: function() {
8231      new wysihtml5.UndoManager(this.parent);
8232    }
8233  });
8234})(wysihtml5);(function(wysihtml5) {
8235  var dom             = wysihtml5.dom,
8236      doc             = document,
8237      win             = window,
8238      HOST_TEMPLATE   = doc.createElement("div"),
8239      /**
8240       * Styles to copy from textarea to the composer element
8241       */
8242      TEXT_FORMATTING = [
8243        "background-color",
8244        "color", "cursor",
8245        "font-family", "font-size", "font-style", "font-variant", "font-weight",
8246        "line-height", "letter-spacing",
8247        "text-align", "text-decoration", "text-indent", "text-rendering",
8248        "word-break", "word-wrap", "word-spacing"
8249      ],
8250      /**
8251       * Styles to copy from textarea to the iframe
8252       */
8253      BOX_FORMATTING = [
8254        "background-color",
8255        "border-collapse",
8256        "border-bottom-color", "border-bottom-style", "border-bottom-width",
8257        "border-left-color", "border-left-style", "border-left-width",
8258        "border-right-color", "border-right-style", "border-right-width",
8259        "border-top-color", "border-top-style", "border-top-width",
8260        "clear", "display", "float",
8261        "margin-bottom", "margin-left", "margin-right", "margin-top",
8262        "outline-color", "outline-offset", "outline-width", "outline-style",
8263        "padding-left", "padding-right", "padding-top", "padding-bottom",
8264        "position", "top", "left", "right", "bottom", "z-index",
8265        "vertical-align", "text-align",
8266        "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
8267        "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
8268        "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
8269        "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
8270        "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
8271        "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
8272        "width", "height"
8273      ],
8274      /**
8275       * Styles to sync while the window gets resized
8276       */
8277      RESIZE_STYLE = [
8278        "width", "height",
8279        "top", "left", "right", "bottom"
8280      ],
8281      ADDITIONAL_CSS_RULES = [
8282        "html             { height: 100%; }",
8283        "body             { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }",
8284        "._wysihtml5-temp { display: none; }",
8285        wysihtml5.browser.isGecko ?
8286          "body.placeholder { color: graytext !important; }" : 
8287          "body.placeholder { color: #a9a9a9 !important; }",
8288        "body[disabled]   { background-color: #eee !important; color: #999 !important; cursor: default !important; }",
8289        // Ensure that user see's broken images and can delete them
8290        "img:-moz-broken  { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
8291      ];
8292  
8293  /**
8294   * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
8295   * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
8296   *
8297   * Other browsers need a more hacky way: (pssst don't tell my mama)
8298   * In order to prevent the element being scrolled into view when focusing it, we simply
8299   * move it out of the scrollable area, focus it, and reset it's position
8300   */
8301  var focusWithoutScrolling = function(element) {
8302    if (element.setActive) {
8303      // Following line could cause a js error when the textarea is invisible
8304      // See https://github.com/xing/wysihtml5/issues/9
8305      try { element.setActive(); } catch(e) {}
8306    } else {
8307      var elementStyle = element.style,
8308          originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
8309          originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
8310          originalStyles = {
8311            position:         elementStyle.position,
8312            top:              elementStyle.top,
8313            left:             elementStyle.left,
8314            WebkitUserSelect: elementStyle.WebkitUserSelect
8315          };
8316      
8317      dom.setStyles({
8318        position:         "absolute",
8319        top:              "-99999px",
8320        left:             "-99999px",
8321        // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
8322        WebkitUserSelect: "none"
8323      }).on(element);
8324      
8325      element.focus();
8326      
8327      dom.setStyles(originalStyles).on(element);
8328      
8329      if (win.scrollTo) {
8330        // Some browser extensions unset this method to prevent annoyances
8331        // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
8332        // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
8333        win.scrollTo(originalScrollLeft, originalScrollTop);
8334      }
8335    }
8336  };
8337  
8338  
8339  wysihtml5.views.Composer.prototype.style = function() {
8340    var that                  = this,
8341        originalActiveElement = doc.querySelector(":focus"),
8342        textareaElement       = this.textarea.element,
8343        hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
8344        originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder");
8345    this.focusStylesHost      = this.focusStylesHost  || HOST_TEMPLATE.cloneNode(false);
8346    this.blurStylesHost       = this.blurStylesHost   || HOST_TEMPLATE.cloneNode(false);
8347  
8348    // Remove placeholder before copying (as the placeholder has an affect on the computed style)
8349    if (hasPlaceholder) {
8350      textareaElement.removeAttribute("placeholder");
8351    }
8352  
8353    if (textareaElement === originalActiveElement) {
8354      textareaElement.blur();
8355    }
8356  
8357    // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
8358    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost);
8359  
8360    // --------- editor styles ---------
8361    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
8362  
8363    // --------- apply standard rules ---------
8364    dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
8365  
8366    // --------- :focus styles ---------
8367    focusWithoutScrolling(textareaElement);
8368    dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8369    dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
8370  
8371    // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
8372    // this is needed for when the change_view event is fired where the iframe is hidden and then
8373    // the blur event fires and re-displays it
8374    var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
8375  
8376    // --------- restore focus ---------
8377    if (originalActiveElement) {
8378      originalActiveElement.focus();
8379    } else {
8380      textareaElement.blur();
8381    }
8382  
8383    // --------- restore placeholder ---------
8384    if (hasPlaceholder) {
8385      textareaElement.setAttribute("placeholder", originalPlaceholder);
8386    }
8387  
8388    // When copying styles, we only get the computed style which is never returned in percent unit
8389    // Therefore we've to recalculate style onresize
8390    if (!wysihtml5.browser.hasCurrentStyleProperty()) {
8391      var winObserver = dom.observe(win, "resize", function() {
8392        // Remove event listener if composer doesn't exist anymore
8393        if (!dom.contains(document.documentElement, that.iframe)) {
8394          winObserver.stop();
8395          return;
8396        }
8397        var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement),
8398            originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe);
8399        textareaElement.style.display = "";
8400        that.iframe.style.display = "none";
8401        dom.copyStyles(RESIZE_STYLE)
8402          .from(textareaElement)
8403          .to(that.iframe)
8404          .andTo(that.focusStylesHost)
8405          .andTo(that.blurStylesHost);
8406        that.iframe.style.display = originalComposerDisplayStyle;
8407        textareaElement.style.display = originalTextareaDisplayStyle;
8408      });
8409    }
8410  
8411    // --------- Sync focus/blur styles ---------
8412    this.parent.observe("focus:composer", function() {
8413      dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe);
8414      dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
8415    });
8416
8417    this.parent.observe("blur:composer", function() {
8418      dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe);
8419      dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
8420    });
8421  
8422    return this;
8423  };
8424})(wysihtml5);/**
8425 * Taking care of events
8426 *  - Simulating 'change' event on contentEditable element
8427 *  - Handling drag & drop logic
8428 *  - Catch paste events
8429 *  - Dispatch proprietary newword:composer event
8430 *  - Keyboard shortcuts
8431 */
8432(function(wysihtml5) {
8433  var dom       = wysihtml5.dom,
8434      browser   = wysihtml5.browser,
8435      /**
8436       * Map keyCodes to query commands
8437       */
8438      shortcuts = {
8439        "66": "bold",     // B
8440        "73": "italic",   // I
8441        "85": "underline" // U
8442      };
8443  
8444  wysihtml5.views.Composer.prototype.observe = function() {
8445    var that                = this,
8446        state               = this.getValue(),
8447        iframe              = this.sandbox.getIframe(),
8448        element             = this.element,
8449        focusBlurElement    = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(),
8450        // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same
8451        pasteEvents         = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"];
8452
8453    // --------- destroy:composer event ---------
8454    dom.observe(iframe, "DOMNodeRemoved", function() {
8455      clearInterval(domNodeRemovedInterval);
8456      that.parent.fire("destroy:composer");
8457    });
8458
8459    // DOMNodeRemoved event is not supported in IE 8
8460    var domNodeRemovedInterval = setInterval(function() {
8461      if (!dom.contains(document.documentElement, iframe)) {
8462        clearInterval(domNodeRemovedInterval);
8463        that.parent.fire("destroy:composer");
8464      }
8465    }, 250);
8466
8467
8468    // --------- Focus & blur logic ---------
8469    dom.observe(focusBlurElement, "focus", function() {
8470      that.parent.fire("focus").fire("focus:composer");
8471
8472      // Delay storing of state until all focus handler are fired
8473      // especially the one which resets the placeholder
8474      setTimeout(function() { state = that.getValue(); }, 0);
8475    });
8476
8477    dom.observe(focusBlurElement, "blur", function() {
8478      if (state !== that.getValue()) {
8479        that.parent.fire("change").fire("change:composer");
8480      }
8481      that.parent.fire("blur").fire("blur:composer");
8482    });
8483    
8484    if (wysihtml5.browser.isIos()) {
8485      // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus
8486      // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
8487      // We prevent that by focusing a temporary input element which immediately loses focus
8488      dom.observe(element, "blur", function() {
8489        var input = element.ownerDocument.createElement("input"),
8490            originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
8491            originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
8492        try {
8493          that.selection.insertNode(input);
8494        } catch(e) {
8495          element.appendChild(input);
8496        }
8497        input.focus();
8498        input.parentNode.removeChild(input);
8499        
8500        window.scrollTo(originalScrollLeft, originalScrollTop);
8501      });
8502    }
8503
8504    // --------- Drag & Drop logic ---------
8505    dom.observe(element, "dragenter", function() {
8506      that.parent.fire("unset_placeholder");
8507    });
8508
8509    if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) {
8510      dom.observe(element, ["dragover", "dragenter"], function(event) {
8511        event.preventDefault();
8512      });
8513    }
8514
8515    dom.observe(element, pasteEvents, function(event) {
8516      var dataTransfer = event.dataTransfer,
8517          data;
8518
8519      if (dataTransfer && browser.supportsDataTransfer()) {
8520        data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain");
8521      }
8522      if (data) {
8523        element.focus();
8524        that.commands.exec("insertHTML", data);
8525        that.parent.fire("paste").fire("paste:composer");
8526        event.stopPropagation();
8527        event.preventDefault();
8528      } else {
8529        setTimeout(function() {
8530          that.parent.fire("paste").fire("paste:composer");
8531        }, 0);
8532      }
8533    });
8534
8535    // --------- neword event ---------
8536    dom.observe(element, "keyup", function(event) {
8537      var keyCode = event.keyCode;
8538      if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
8539        that.parent.fire("newword:composer");
8540      }
8541    });
8542
8543    this.parent.observe("paste:composer", function() {
8544      setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
8545    });
8546
8547    // --------- Make sure that images are selected when clicking on them ---------
8548    if (!browser.canSelectImagesInContentEditable()) {
8549      dom.observe(element, "mousedown", function(event) {
8550        var target = event.target;
8551        if (target.nodeName === "IMG") {
8552          that.selection.selectNode(target);
8553          event.preventDefault();
8554        }
8555      });
8556    }
8557    
8558    // --------- Shortcut logic ---------
8559    dom.observe(element, "keydown", function(event) {
8560      var keyCode  = event.keyCode,
8561          command  = shortcuts[keyCode];
8562      if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
8563        that.commands.exec(command);
8564        event.preventDefault();
8565      }
8566    });
8567
8568    // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
8569    dom.observe(element, "keydown", function(event) {
8570      var target  = that.selection.getSelectedNode(true),
8571          keyCode = event.keyCode,
8572          parent;
8573      if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
8574        parent = target.parentNode;
8575        // delete the <img>
8576        parent.removeChild(target);
8577        // and it's parent <a> too if it hasn't got any other child nodes
8578        if (parent.nodeName === "A" && !parent.firstChild) {
8579          parent.parentNode.removeChild(parent);
8580        }
8581
8582        setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
8583        event.preventDefault();
8584      }
8585    });
8586
8587    // --------- Show url in tooltip when hovering links or images ---------
8588    var titlePrefixes = {
8589      IMG: "Image: ",
8590      A:   "Link: "
8591    };
8592    
8593    dom.observe(element, "mouseover", function(event) {
8594      var target   = event.target,
8595          nodeName = target.nodeName,
8596          title;
8597      if (nodeName !== "A" && nodeName !== "IMG") {
8598        return;
8599      }
8600      var hasTitle = target.hasAttribute("title");
8601      if(!hasTitle){
8602        title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
8603        target.setAttribute("title", title);
8604      }
8605    });
8606  };
8607})(wysihtml5);/**
8608 * Class that takes care that the value of the composer and the textarea is always in sync
8609 */
8610(function(wysihtml5) {
8611  var INTERVAL = 400;
8612  
8613  wysihtml5.views.Synchronizer = Base.extend(
8614    /** @scope wysihtml5.views.Synchronizer.prototype */ {
8615
8616    constructor: function(editor, textarea, composer) {
8617      this.editor   = editor;
8618      this.textarea = textarea;
8619      this.composer = composer;
8620
8621      this._observe();
8622    },
8623
8624    /**
8625     * Sync html from composer to textarea
8626     * Takes care of placeholders
8627     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
8628     */
8629    fromComposerToTextarea: function(shouldParseHtml) {
8630      this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml);
8631    },
8632
8633    /**
8634     * Sync value of textarea to composer
8635     * Takes care of placeholders
8636     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
8637     */
8638    fromTextareaToComposer: function(shouldParseHtml) {
8639      var textareaValue = this.textarea.getValue();
8640      if (textareaValue) {
8641        this.composer.setValue(textareaValue, shouldParseHtml);
8642      } else {
8643        this.composer.clear();
8644        this.editor.fire("set_placeholder");
8645      }
8646    },
8647
8648    /**
8649     * Invoke syncing based on view state
8650     * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
8651     */
8652    sync: function(shouldParseHtml) {
8653      if (this.editor.currentView.name === "textarea") {
8654        this.fromTextareaToComposer(shouldParseHtml);
8655      } else {
8656        this.fromComposerToTextarea(shouldParseHtml);
8657      }
8658    },
8659
8660    /**
8661     * Initializes interval-based syncing
8662     * also makes sure that on-submit the composer's content is synced with the textarea
8663     * immediately when the form gets submitted
8664     */
8665    _observe: function() {
8666      var interval,
8667          that          = this,
8668          form          = this.textarea.element.form,
8669          startInterval = function() {
8670            interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
8671          },
8672          stopInterval  = function() {
8673            clearInterval(interval);
8674            interval = null;
8675          };
8676
8677      startInterval();
8678
8679      if (form) {
8680        // If the textarea is in a form make sure that after onreset and onsubmit the composer
8681        // has the correct state
8682        wysihtml5.dom.observe(form, "submit", function() {
8683          that.sync(true);
8684        });
8685        wysihtml5.dom.observe(form, "reset", function() {
8686          setTimeout(function() { that.fromTextareaToComposer(); }, 0);
8687        });
8688      }
8689
8690      this.editor.observe("change_view", function(view) {
8691        if (view === "composer" && !interval) {
8692          that.fromTextareaToComposer(true);
8693          startInterval();
8694        } else if (view === "textarea") {
8695          that.fromComposerToTextarea(true);
8696          stopInterval();
8697        }
8698      });
8699
8700      this.editor.observe("destroy:composer", stopInterval);
8701    }
8702  });
8703})(wysihtml5);
8704wysihtml5.views.Textarea = wysihtml5.views.View.extend(
8705  /** @scope wysihtml5.views.Textarea.prototype */ {
8706  name: "textarea",
8707  
8708  constructor: function(parent, textareaElement, config) {
8709    this.base(parent, textareaElement, config);
8710    
8711    this._observe();
8712  },
8713  
8714  clear: function() {
8715    this.element.value = "";
8716  },
8717  
8718  getValue: function(parse) {
8719    var value = this.isEmpty() ? "" : this.element.value;
8720    if (parse) {
8721      value = this.parent.parse(value);
8722    }
8723    return value;
8724  },
8725  
8726  setValue: function(html, parse) {
8727    if (parse) {
8728      html = this.parent.parse(html);
8729    }
8730    this.element.value = html;
8731  },
8732  
8733  hasPlaceholderSet: function() {
8734    var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
8735        placeholderText     = this.element.getAttribute("placeholder") || null,
8736        value               = this.element.value,
8737        isEmpty             = !value;
8738    return (supportsPlaceholder && isEmpty) || (value === placeholderText);
8739  },
8740  
8741  isEmpty: function() {
8742    return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
8743  },
8744  
8745  _observe: function() {
8746    var element = this.element,
8747        parent  = this.parent,
8748        eventMapping = {
8749          focusin:  "focus",
8750          focusout: "blur"
8751        },
8752        /**
8753         * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
8754         * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
8755         */
8756        events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
8757    
8758    parent.observe("beforeload", function() {
8759      wysihtml5.dom.observe(element, events, function(event) {
8760        var eventName = eventMapping[event.type] || event.type;
8761        parent.fire(eventName).fire(eventName + ":textarea");
8762      });
8763      
8764      wysihtml5.dom.observe(element, ["paste", "drop"], function() {
8765        setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
8766      });
8767    });
8768  }
8769});/**
8770 * Toolbar Dialog
8771 *
8772 * @param {Element} link The toolbar link which causes the dialog to show up
8773 * @param {Element} container The dialog container
8774 *
8775 * @example
8776 *    <!-- Toolbar link -->
8777 *    <a data-wysihtml5-command="insertImage">insert an image</a>
8778 *
8779 *    <!-- Dialog -->
8780 *    <div data-wysihtml5-dialog="insertImage" style="display: none;">
8781 *      <label>
8782 *        URL: <input data-wysihtml5-dialog-field="src" value="http://">
8783 *      </label>
8784 *      <label>
8785 *        Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
8786 *      </label>
8787 *    </div>
8788 *
8789 *    <script>
8790 *      var dialog = new wysihtml5.toolbar.Dialog(
8791 *        document.querySelector("[data-wysihtml5-command='insertImage']"),
8792 *        document.querySelector("[data-wysihtml5-dialog='insertImage']")
8793 *      );
8794 *      dialog.observe("save", function(attributes) {
8795 *        // do something
8796 *      });
8797 *    </script>
8798 */
8799(function(wysihtml5) {
8800  var dom                     = wysihtml5.dom,
8801      CLASS_NAME_OPENED       = "wysihtml5-command-dialog-opened",
8802      SELECTOR_FORM_ELEMENTS  = "input, select, textarea",
8803      SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
8804      ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
8805      
8806  
8807  wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
8808    /** @scope wysihtml5.toolbar.Dialog.prototype */ {
8809    constructor: function(link, container) {
8810      this.link       = link;
8811      this.container  = container;
8812    },
8813
8814    _observe: function() {
8815      if (this._observed) {
8816        return;
8817      }
8818      
8819      var that = this,
8820          callbackWrapper = function(event) {
8821            var attributes = that._serialize();
8822            if (attributes == that.elementToChange) {
8823              that.fire("edit", attributes);
8824            } else {
8825              that.fire("save", attributes);
8826            }
8827            that.hide();
8828            event.preventDefault();
8829            event.stopPropagation();
8830          };
8831
8832      dom.observe(that.link, "click", function(event) {
8833        if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
8834          setTimeout(function() { that.hide(); }, 0);
8835        }
8836      });
8837
8838      dom.observe(this.container, "keydown", function(event) {
8839        var keyCode = event.keyCode;
8840        if (keyCode === wysihtml5.ENTER_KEY) {
8841          callbackWrapper(event);
8842        }
8843        if (keyCode === wysihtml5.ESCAPE_KEY) {
8844          that.hide();
8845        }
8846      });
8847
8848      dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
8849
8850      dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
8851        that.fire("cancel");
8852        that.hide();
8853        event.preventDefault();
8854        event.stopPropagation();
8855      });
8856
8857      var formElements  = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
8858          i             = 0,
8859          length        = formElements.length,
8860          _clearInterval = function() { clearInterval(that.interval); };
8861      for (; i<length; i++) {
8862        dom.observe(formElements[i], "change", _clearInterval);
8863      }
8864
8865      this._observed = true;
8866    },
8867
8868    /**
8869     * Grabs all fields in the dialog and puts them in key=>value style in an object which
8870     * then gets returned
8871     */
8872    _serialize: function() {
8873      var data    = this.elementToChange || {},
8874          fields  = this.container.querySelectorAll(SELECTOR_FIELDS),
8875          length  = fields.length,
8876          i       = 0;
8877      for (; i<length; i++) {
8878        data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
8879      }
8880      return data;
8881    },
8882
8883    /**
8884     * Takes the attributes of the "elementToChange"
8885     * and inserts them in their corresponding dialog input fields
8886     * 
8887     * Assume the "elementToChange" looks like this:
8888     *    <a href="http://www.google.com" target="_blank">foo</a>
8889     *
8890     * and we have the following dialog:
8891     *    <input type="text" data-wysihtml5-dialog-field="href" value="">
8892     *    <input type="text" data-wysihtml5-dialog-field="target" value="">
8893     * 
8894     * after calling _interpolate() the dialog will look like this
8895     *    <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
8896     *    <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
8897     *
8898     * Basically it adopted the attribute values into the corresponding input fields
8899     *
8900     */
8901    _interpolate: function(avoidHiddenFields) {
8902      var field,
8903          fieldName,
8904          newValue,
8905          focusedElement = document.querySelector(":focus"),
8906          fields         = this.container.querySelectorAll(SELECTOR_FIELDS),
8907          length         = fields.length,
8908          i              = 0;
8909      for (; i<length; i++) {
8910        field = fields[i];
8911        
8912        // Never change elements where the user is currently typing in
8913        if (field === focusedElement) {
8914          continue;
8915        }
8916        
8917        // Don't update hidden fields
8918        // See https://github.com/xing/wysihtml5/pull/14
8919        if (avoidHiddenFields && field.type === "hidden") {
8920          continue;
8921        }
8922        
8923        fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
8924        newValue  = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue;
8925        field.value = newValue;
8926      }
8927    },
8928
8929    /**
8930     * Show the dialog element
8931     */
8932    show: function(elementToChange) {
8933      var that        = this,
8934          firstField  = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
8935      this.elementToChange = elementToChange;
8936      this._observe();
8937      this._interpolate();
8938      if (elementToChange) {
8939        this.interval = setInterval(function() { that._interpolate(true); }, 500);
8940      }
8941      dom.addClass(this.link, CLASS_NAME_OPENED);
8942      this.container.style.display = "";
8943      this.fire("show");
8944      if (firstField && !elementToChange) {
8945        try {
8946          firstField.focus();
8947        } catch(e) {}
8948      }
8949    },
8950
8951    /**
8952     * Hide the dialog element
8953     */
8954    hide: function() {
8955      clearInterval(this.interval);
8956      this.elementToChange = null;
8957      dom.removeClass(this.link, CLASS_NAME_OPENED);
8958      this.container.style.display = "none";
8959      this.fire("hide");
8960    }
8961  });
8962})(wysihtml5);
8963/**
8964 * Converts speech-to-text and inserts this into the editor
8965 * As of now (2011/03/25) this only is supported in Chrome >= 11
8966 *
8967 * Note that it sends the recorded audio to the google speech recognition api:
8968 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
8969 *
8970 * Current HTML5 draft can be found here
8971 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
8972 * 
8973 * "Accessing Google Speech API Chrome 11"
8974 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
8975 */
8976(function(wysihtml5) {
8977  var dom = wysihtml5.dom;
8978  
8979  var linkStyles = {
8980    position: "relative"
8981  };
8982  
8983  var wrapperStyles = {
8984    left:     0,
8985    margin:   0,
8986    opacity:  0,
8987    overflow: "hidden",
8988    padding:  0,
8989    position: "absolute",
8990    top:      0,
8991    zIndex:   1
8992  };
8993  
8994  var inputStyles = {
8995    cursor:     "inherit",
8996    fontSize:   "50px",
8997    height:     "50px",
8998    marginTop:  "-25px",
8999    outline:    0,
9000    padding:    0,
9001    position:   "absolute",
9002    right:      "-4px",
9003    top:        "50%"
9004  };
9005  
9006  var inputAttributes = {
9007    "x-webkit-speech": "",
9008    "speech":          ""
9009  };
9010  
9011  wysihtml5.toolbar.Speech = function(parent, link) {
9012    var input = document.createElement("input");
9013    if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
9014      link.style.display = "none";
9015      return;
9016    }
9017    
9018    var wrapper = document.createElement("div");
9019    
9020    wysihtml5.lang.object(wrapperStyles).merge({
9021      width:  link.offsetWidth  + "px",
9022      height: link.offsetHeight + "px"
9023    });
9024    
9025    dom.insert(input).into(wrapper);
9026    dom.insert(wrapper).into(link);
9027    
9028    dom.setStyles(inputStyles).on(input);
9029    dom.setAttributes(inputAttributes).on(input)
9030    
9031    dom.setStyles(wrapperStyles).on(wrapper);
9032    dom.setStyles(linkStyles).on(link);
9033    
9034    var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
9035    dom.observe(input, eventName, function() {
9036      parent.execCommand("insertText", input.value);
9037      input.value = "";
9038    });
9039    
9040    dom.observe(input, "click", function(event) {
9041      if (dom.hasClass(link, "wysihtml5-command-disabled")) {
9042        event.preventDefault();
9043      }
9044      
9045      event.stopPropagation();
9046    });
9047  };
9048})(wysihtml5);/**
9049 * Toolbar
9050 *
9051 * @param {Object} parent Reference to instance of Editor instance
9052 * @param {Element} container Reference to the toolbar container element
9053 *
9054 * @example
9055 *    <div id="toolbar">
9056 *      <a data-wysihtml5-command="createLink">insert link</a>
9057 *      <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
9058 *    </div>
9059 *
9060 *    <script>
9061 *      var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
9062 *    </script>
9063 */
9064(function(wysihtml5) {
9065  var CLASS_NAME_COMMAND_DISABLED   = "wysihtml5-command-disabled",
9066      CLASS_NAME_COMMANDS_DISABLED  = "wysihtml5-commands-disabled",
9067      CLASS_NAME_COMMAND_ACTIVE     = "wysihtml5-command-active",
9068      CLASS_NAME_ACTION_ACTIVE      = "wysihtml5-action-active",
9069      dom                           = wysihtml5.dom;
9070  
9071  wysihtml5.toolbar.Toolbar = Base.extend(
9072    /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
9073    constructor: function(editor, container) {
9074      this.editor     = editor;
9075      this.container  = typeof(container) === "string" ? document.getElementById(container) : container;
9076      this.composer   = editor.composer;
9077
9078      this._getLinks("command");
9079      this._getLinks("action");
9080
9081      this._observe();
9082      this.show();
9083      
9084      var speechInputLinks  = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
9085          length            = speechInputLinks.length,
9086          i                 = 0;
9087      for (; i<length; i++) {
9088        new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
9089      }
9090    },
9091
9092    _getLinks: function(type) {
9093      var links   = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
9094          length  = links.length,
9095          i       = 0,
9096          mapping = this[type + "Mapping"] = {},
9097          link,
9098          group,
9099          name,
9100          value,
9101          dialog;
9102      for (; i<length; i++) {
9103        link    = links[i];
9104        name    = link.getAttribute("data-wysihtml5-" + type);
9105        value   = link.getAttribute("data-wysihtml5-" + type + "-value");
9106        group   = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
9107        dialog  = this._getDialog(link, name);
9108        
9109        mapping[name + ":" + value] = {
9110          link:   link,
9111          group:  group,
9112          name:   name,
9113          value:  value,
9114          dialog: dialog,
9115          state:  false
9116        };
9117      }
9118    },
9119
9120    _getDialog: function(link, command) {
9121      var that          = this,
9122          dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
9123          dialog,
9124          caretBookmark;
9125      
9126      if (dialogElement) {
9127        dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
9128
9129        dialog.observe("show", function() {
9130          caretBookmark = that.composer.selection.getBookmark();
9131
9132          that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9133        });
9134
9135        dialog.observe("save", function(attributes) {
9136          if (caretBookmark) {
9137            that.composer.selection.setBookmark(caretBookmark);
9138          }
9139          that._execCommand(command, attributes);
9140          
9141          that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9142        });
9143
9144        dialog.observe("cancel", function() {
9145          that.editor.focus(false);
9146          that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
9147        });
9148      }
9149      return dialog;
9150    },
9151
9152    /**
9153     * @example
9154     *    var toolbar = new wysihtml5.Toolbar();
9155     *    // Insert a <blockquote> element or wrap current selection in <blockquote>
9156     *    toolbar.execCommand("formatBlock", "blockquote");
9157     */
9158    execCommand: function(command, commandValue) {
9159      if (this.commandsDisabled) {
9160        return;
9161      }
9162
9163      var commandObj = this.commandMapping[command + ":" + commandValue];
9164
9165      // Show dialog when available
9166      if (commandObj && commandObj.dialog && !commandObj.state) {
9167        commandObj.dialog.show();
9168      } else {
9169        this._execCommand(command, commandValue);
9170      }
9171    },
9172
9173    _execCommand: function(command, commandValue) {
9174      // Make sure that composer is focussed (false => don't move caret to the end)
9175      this.editor.focus(false);
9176
9177      this.composer.commands.exec(command, commandValue);
9178      this._updateLinkStates();
9179    },
9180
9181    execAction: function(action) {
9182      var editor = this.editor;
9183      switch(action) {
9184        case "change_view":
9185          if (editor.currentView === editor.textarea) {
9186            editor.fire("change_view", "composer");
9187          } else {
9188            editor.fire("change_view", "textarea");
9189          }
9190          break;
9191      }
9192    },
9193
9194    _observe: function() {
9195      var that      = this,
9196          editor    = this.editor,
9197          container = this.container,
9198          links     = this.commandLinks.concat(this.actionLinks),
9199          length    = links.length,
9200          i         = 0;
9201      
9202      for (; i<length; i++) {
9203        // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
9204        // (you know, a:link { ... } doesn't match anchors with missing href attribute)
9205        dom.setAttributes({
9206          href:         "javascript:;",
9207          unselectable: "on"
9208        }).on(links[i]);
9209      }
9210
9211      // Needed for opera
9212      dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); });
9213      
9214      dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
9215        var link          = this,
9216            command       = link.getAttribute("data-wysihtml5-command"),
9217            commandValue  = link.getAttribute("data-wysihtml5-command-value");
9218        that.execCommand(command, commandValue);
9219        event.preventDefault();
9220      });
9221
9222      dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
9223        var action = this.getAttribute("data-wysihtml5-action");
9224        that.execAction(action);
9225        event.preventDefault();
9226      });
9227
9228      editor.observe("focus:composer", function() {
9229        that.bookmark = null;
9230        clearInterval(that.interval);
9231        that.interval = setInterval(function() { that._updateLinkStates(); }, 500);
9232      });
9233
9234      editor.observe("blur:composer", function() {
9235        clearInterval(that.interval);
9236      });
9237
9238      editor.observe("destroy:composer", function() {
9239        clearInterval(that.interval);
9240      });
9241
9242      editor.observe("change_view", function(currentView) {
9243        // Set timeout needed in order to let the blur event fire first
9244        setTimeout(function() {
9245          that.commandsDisabled = (currentView !== "composer");
9246          that._updateLinkStates();
9247          if (that.commandsDisabled) {
9248            dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
9249          } else {
9250            dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
9251          }
9252        }, 0);
9253      });
9254    },
9255
9256    _updateLinkStates: function() {
9257      var element           = this.composer.element,
9258          commandMapping    = this.commandMapping,
9259          actionMapping     = this.actionMapping,
9260          i,
9261          state,
9262          action,
9263          command;
9264      // every millisecond counts... this is executed quite often
9265      for (i in commandMapping) {
9266        command = commandMapping[i];
9267        if (this.commandsDisabled) {
9268          state = false;
9269          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9270          if (command.group) {
9271            dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9272          }
9273          if (command.dialog) {
9274            command.dialog.hide();
9275          }
9276        } else {
9277          state = this.composer.commands.state(command.name, command.value);
9278          if (wysihtml5.lang.object(state).isArray()) {
9279            // Grab first and only object/element in state array, otherwise convert state into boolean
9280            // to avoid showing a dialog for multiple selected elements which may have different attributes
9281            // eg. when two links with different href are selected, the state will be an array consisting of both link elements
9282            // but the dialog interface can only update one
9283            state = state.length === 1 ? state[0] : true;
9284          }
9285          dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
9286          if (command.group) {
9287            dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
9288          }
9289        }
9290
9291        if (command.state === state) {
9292          continue;
9293        }
9294
9295        command.state = state;
9296        if (state) {
9297          dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9298          if (command.group) {
9299            dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9300          }
9301          if (command.dialog) {
9302            if (typeof(state) === "object") {
9303              command.dialog.show(state);
9304            } else {
9305              command.dialog.hide();
9306            }
9307          }
9308        } else {
9309          dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
9310          if (command.group) {
9311            dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
9312          }
9313          if (command.dialog) {
9314            command.dialog.hide();
9315          }
9316        }
9317      }
9318      
9319      for (i in actionMapping) {
9320        action = actionMapping[i];
9321        
9322        if (action.name === "change_view") {
9323          action.state = this.editor.currentView === this.editor.textarea;
9324          if (action.state) {
9325            dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);
9326          } else {
9327            dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
9328          }
9329        }
9330      }
9331    },
9332
9333    show: function() {
9334      this.container.style.display = "";
9335    },
9336
9337    hide: function() {
9338      this.container.style.display = "none";
9339    }
9340  });
9341  
9342})(wysihtml5);
9343/**
9344 * WYSIHTML5 Editor
9345 *
9346 * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface
9347 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
9348 *
9349 * @events
9350 *    load
9351 *    beforeload (for internal use only)
9352 *    focus
9353 *    focus:composer
9354 *    focus:textarea
9355 *    blur
9356 *    blur:composer
9357 *    blur:textarea
9358 *    change
9359 *    change:composer
9360 *    change:textarea
9361 *    paste
9362 *    paste:composer
9363 *    paste:textarea
9364 *    newword:composer
9365 *    destroy:composer
9366 *    undo:composer
9367 *    redo:composer
9368 *    beforecommand:composer
9369 *    aftercommand:composer
9370 *    change_view
9371 */
9372(function(wysihtml5) {
9373  var undef;
9374  
9375  var defaultConfig = {
9376    // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body 
9377    name:                 undef,
9378    // Whether the editor should look like the textarea (by adopting styles)
9379    style:                true,
9380    // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
9381    toolbar:              undef,
9382    // Whether urls, entered by the user should automatically become clickable-links
9383    autoLink:             true,
9384    // Object which includes parser rules to apply when html gets inserted via copy & paste
9385    // See parser_rules/*.js for examples
9386    parserRules:          { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
9387    // Parser method to use when the user inserts content via copy & paste
9388    parser:               wysihtml5.dom.parse,
9389    // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
9390    composerClassName:    "wysihtml5-editor",
9391    // Class name to add to the body when the wysihtml5 editor is supported
9392    bodyClassName:        "wysihtml5-supported",
9393    // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
9394    stylesheets:          [],
9395    // Placeholder text to use, defaults to the placeholder attribute on the textarea element
9396    placeholderText:      undef,
9397    // Whether the composer should allow the user to manually resize images, tables etc.
9398    allowObjectResizing:  true,
9399    // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
9400    supportTouchDevices:  true
9401  };
9402  
9403  wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
9404    /** @scope wysihtml5.Editor.prototype */ {
9405    constructor: function(textareaElement, config) {
9406      this.textareaElement  = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement;
9407      this.config           = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
9408      this.textarea         = new wysihtml5.views.Textarea(this, this.textareaElement, this.config);
9409      this.currentView      = this.textarea;
9410      this._isCompatible    = wysihtml5.browser.supported();
9411      
9412      // Sort out unsupported/unwanted browsers here
9413      if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
9414        var that = this;
9415        setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
9416        return;
9417      }
9418      
9419      // Add class name to body, to indicate that the editor is supported
9420      wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
9421      
9422      this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config);
9423      this.currentView = this.composer;
9424      
9425      if (typeof(this.config.parser) === "function") {
9426        this._initParser();
9427      }
9428      
9429      this.observe("beforeload", function() {
9430        this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
9431        if (this.config.toolbar) {
9432          this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar);
9433        }
9434      });
9435      
9436      try {
9437        console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");
9438      } catch(e) {}
9439    },
9440    
9441    isCompatible: function() {
9442      return this._isCompatible;
9443    },
9444
9445    clear: function() {
9446      this.currentView.clear();
9447      return this;
9448    },
9449
9450    getValue: function(parse) {
9451      return this.currentView.getValue(parse);
9452    },
9453
9454    setValue: function(html, parse) {
9455      if (!html) {
9456        return this.clear();
9457      }
9458      this.currentView.setValue(html, parse);
9459      return this;
9460    },
9461
9462    focus: function(setToEnd) {
9463      this.currentView.focus(setToEnd);
9464      return this;
9465    },
9466
9467    /**
9468     * Deactivate editor (make it readonly)
9469     */
9470    disable: function() {
9471      this.currentView.disable();
9472      return this;
9473    },
9474    
9475    /**
9476     * Activate editor
9477     */
9478    enable: function() {
9479      this.currentView.enable();
9480      return this;
9481    },
9482    
9483    isEmpty: function() {
9484      return this.currentView.isEmpty();
9485    },
9486    
9487    hasPlaceholderSet: function() {
9488      return this.currentView.hasPlaceholderSet();
9489    },
9490    
9491    parse: function(htmlOrElement) {
9492      var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true);
9493      if (typeof(htmlOrElement) === "object") {
9494        wysihtml5.quirks.redraw(htmlOrElement);
9495      }
9496      return returnValue;
9497    },
9498    
9499    /**
9500     * Prepare html parser logic
9501     *  - Observes for paste and drop
9502     */
9503    _initParser: function() {
9504      this.observe("paste:composer", function() {
9505        var keepScrollPosition  = true,
9506            that                = this;
9507        that.composer.selection.executeAndRestore(function() {
9508          wysihtml5.quirks.cleanPastedHTML(that.composer.element);
9509          that.parse(that.composer.element);
9510        }, keepScrollPosition);
9511      });
9512      
9513      this.observe("paste:textarea", function() {
9514        var value   = this.textarea.getValue(),
9515            newValue;
9516        newValue = this.parse(value);
9517        this.textarea.setValue(newValue);
9518      });
9519    }
9520  });
9521})(wysihtml5);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.