Browse code

add the editor and the posts part

Dario Rodriguez authored on 26/06/2014 22:41:33
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,9521 @@
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
+ */
11
+var 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
+ */
44
+window['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
+})();
329
+rangy.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) : "";
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;
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 && (isNonTextPartiallySelected(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", "focusNode"] &&
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;
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();
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
+
3252
+var Base = function() {
3253
+	// dummy
3254
+};
3255
+
3256
+Base.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
+
3300
+Base.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
3352
+Base = 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
+ */
3387
+wysihtml5.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);
4091
+wysihtml5.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
+ */
4132
+wysihtml5.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
+ */
4221
+wysihtml5.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
+ */
4347
+wysihtml5.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
+ */
4413
+wysihtml5.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
+ */
4484
+wysihtml5.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
+ */
4556
+wysihtml5.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);
4607
+wysihtml5.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
+ */
4648
+wysihtml5.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
+ */
4744
+wysihtml5.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
+ */
5146
+wysihtml5.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
+ */
5183
+wysihtml5.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
+ */
5205
+wysihtml5.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 = "wysihtml5-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
+ */
5652
+wysihtml5.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} 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
+ */
6755
+wysihtml5.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
+ */
7873
+wysihtml5.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);
8704
+wysihtml5.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);