Fix some bugs when testing opensds ansible
[stor4nfv.git] / src / ceph / src / pybind / mgr / dashboard / static / AdminLTE-2.3.7 / plugins / bootstrap-wysihtml5 / bootstrap3-wysihtml5.all.js
1 // TODO: in future try to replace most inline compability checks with polyfills for code readability 
2
3 // element.textContent polyfill.
4 // Unsupporting browsers: IE8
5
6 if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) {
7         (function() {
8                 var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");
9                 Object.defineProperty(Element.prototype, "textContent",
10                         {
11                                 get: function() {
12                                         return innerText.get.call(this);
13                                 },
14                                 set: function(s) {
15                                         return innerText.set.call(this, s);
16                                 }
17                         }
18                 );
19         })();
20 }
21
22 // isArray polyfill for ie8
23 if(!Array.isArray) {
24   Array.isArray = function(arg) {
25     return Object.prototype.toString.call(arg) === '[object Array]';
26   };
27 };/**
28  * @license wysihtml5x v0.4.15
29  * https://github.com/Edicy/wysihtml5
30  *
31  * Author: Christopher Blum (https://github.com/tiff)
32  * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
33  *
34  * Copyright (C) 2012 XING AG
35  * Licensed under the MIT license (MIT)
36  *
37  */
38 var wysihtml5 = {
39   version: "0.4.15",
40
41   // namespaces
42   commands:   {},
43   dom:        {},
44   quirks:     {},
45   toolbar:    {},
46   lang:       {},
47   selection:  {},
48   views:      {},
49
50   INVISIBLE_SPACE: "\uFEFF",
51
52   EMPTY_FUNCTION: function() {},
53
54   ELEMENT_NODE: 1,
55   TEXT_NODE:    3,
56
57   BACKSPACE_KEY:  8,
58   ENTER_KEY:      13,
59   ESCAPE_KEY:     27,
60   SPACE_KEY:      32,
61   DELETE_KEY:     46
62 };
63 ;/**
64  * Rangy, a cross-browser JavaScript range and selection library
65  * http://code.google.com/p/rangy/
66  *
67  * Copyright 2014, Tim Down
68  * Licensed under the MIT license.
69  * Version: 1.3alpha.20140804
70  * Build date: 4 August 2014
71  */
72
73 (function(factory, global) {
74     if (typeof define == "function" && define.amd) {
75         // AMD. Register as an anonymous module.
76         define(factory);
77 /*
78     TODO: look into this properly.
79     
80     } else if (typeof exports == "object") {
81         // Node/CommonJS style for Browserify
82         module.exports = factory;
83 */
84     } else {
85         // No AMD or CommonJS support so we place Rangy in a global variable
86         global.rangy = factory();
87     }
88 })(function() {
89
90     var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
91
92     // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
93     // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
94     var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
95         "commonAncestorContainer"];
96
97     // Minimal set of methods required for DOM Level 2 Range compliance
98     var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
99         "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
100         "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
101
102     var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
103
104     // Subset of TextRange's full set of methods that we're interested in
105     var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
106         "setEndPoint", "getBoundingClientRect"];
107
108     /*----------------------------------------------------------------------------------------------------------------*/
109
110     // Trio of functions taken from Peter Michaux's article:
111     // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
112     function isHostMethod(o, p) {
113         var t = typeof o[p];
114         return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
115     }
116
117     function isHostObject(o, p) {
118         return !!(typeof o[p] == OBJECT && o[p]);
119     }
120
121     function isHostProperty(o, p) {
122         return typeof o[p] != UNDEFINED;
123     }
124
125     // Creates a convenience function to save verbose repeated calls to tests functions
126     function createMultiplePropertyTest(testFunc) {
127         return function(o, props) {
128             var i = props.length;
129             while (i--) {
130                 if (!testFunc(o, props[i])) {
131                     return false;
132                 }
133             }
134             return true;
135         };
136     }
137
138     // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
139     var areHostMethods = createMultiplePropertyTest(isHostMethod);
140     var areHostObjects = createMultiplePropertyTest(isHostObject);
141     var areHostProperties = createMultiplePropertyTest(isHostProperty);
142
143     function isTextRange(range) {
144         return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
145     }
146
147     function getBody(doc) {
148         return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
149     }
150
151     var modules = {};
152
153     var api = {
154         version: "1.3alpha.20140804",
155         initialized: false,
156         supported: true,
157
158         util: {
159             isHostMethod: isHostMethod,
160             isHostObject: isHostObject,
161             isHostProperty: isHostProperty,
162             areHostMethods: areHostMethods,
163             areHostObjects: areHostObjects,
164             areHostProperties: areHostProperties,
165             isTextRange: isTextRange,
166             getBody: getBody
167         },
168
169         features: {},
170
171         modules: modules,
172         config: {
173             alertOnFail: true,
174             alertOnWarn: false,
175             preferTextRange: false,
176             autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
177         }
178     };
179
180     function consoleLog(msg) {
181         if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
182             window.console.log(msg);
183         }
184     }
185
186     function alertOrLog(msg, shouldAlert) {
187         if (shouldAlert) {
188             window.alert(msg);
189         } else  {
190             consoleLog(msg);
191         }
192     }
193
194     function fail(reason) {
195         api.initialized = true;
196         api.supported = false;
197         alertOrLog("Rangy is not supported on this page in your browser. Reason: " + reason, api.config.alertOnFail);
198     }
199
200     api.fail = fail;
201
202     function warn(msg) {
203         alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
204     }
205
206     api.warn = warn;
207
208     // Add utility extend() method
209     if ({}.hasOwnProperty) {
210         api.util.extend = function(obj, props, deep) {
211             var o, p;
212             for (var i in props) {
213                 if (props.hasOwnProperty(i)) {
214                     o = obj[i];
215                     p = props[i];
216                     if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
217                         api.util.extend(o, p, true);
218                     }
219                     obj[i] = p;
220                 }
221             }
222             // Special case for toString, which does not show up in for...in loops in IE <= 8
223             if (props.hasOwnProperty("toString")) {
224                 obj.toString = props.toString;
225             }
226             return obj;
227         };
228     } else {
229         fail("hasOwnProperty not supported");
230     }
231
232     // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
233     (function() {
234         var el = document.createElement("div");
235         el.appendChild(document.createElement("span"));
236         var slice = [].slice;
237         var toArray;
238         try {
239             if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
240                 toArray = function(arrayLike) {
241                     return slice.call(arrayLike, 0);
242                 };
243             }
244         } catch (e) {}
245
246         if (!toArray) {
247             toArray = function(arrayLike) {
248                 var arr = [];
249                 for (var i = 0, len = arrayLike.length; i < len; ++i) {
250                     arr[i] = arrayLike[i];
251                 }
252                 return arr;
253             };
254         }
255
256         api.util.toArray = toArray;
257     })();
258
259
260     // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
261     // normalization of event properties
262     var addListener;
263     if (isHostMethod(document, "addEventListener")) {
264         addListener = function(obj, eventType, listener) {
265             obj.addEventListener(eventType, listener, false);
266         };
267     } else if (isHostMethod(document, "attachEvent")) {
268         addListener = function(obj, eventType, listener) {
269             obj.attachEvent("on" + eventType, listener);
270         };
271     } else {
272         fail("Document does not have required addEventListener or attachEvent method");
273     }
274
275     api.util.addListener = addListener;
276
277     var initListeners = [];
278
279     function getErrorDesc(ex) {
280         return ex.message || ex.description || String(ex);
281     }
282
283     // Initialization
284     function init() {
285         if (api.initialized) {
286             return;
287         }
288         var testRange;
289         var implementsDomRange = false, implementsTextRange = false;
290
291         // First, perform basic feature tests
292
293         if (isHostMethod(document, "createRange")) {
294             testRange = document.createRange();
295             if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
296                 implementsDomRange = true;
297             }
298         }
299
300         var body = getBody(document);
301         if (!body || body.nodeName.toLowerCase() != "body") {
302             fail("No body element found");
303             return;
304         }
305
306         if (body && isHostMethod(body, "createTextRange")) {
307             testRange = body.createTextRange();
308             if (isTextRange(testRange)) {
309                 implementsTextRange = true;
310             }
311         }
312
313         if (!implementsDomRange && !implementsTextRange) {
314             fail("Neither Range nor TextRange are available");
315             return;
316         }
317
318         api.initialized = true;
319         api.features = {
320             implementsDomRange: implementsDomRange,
321             implementsTextRange: implementsTextRange
322         };
323
324         // Initialize modules
325         var module, errorMessage;
326         for (var moduleName in modules) {
327             if ( (module = modules[moduleName]) instanceof Module ) {
328                 module.init(module, api);
329             }
330         }
331
332         // Call init listeners
333         for (var i = 0, len = initListeners.length; i < len; ++i) {
334             try {
335                 initListeners[i](api);
336             } catch (ex) {
337                 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
338                 consoleLog(errorMessage);
339             }
340         }
341     }
342
343     // Allow external scripts to initialize this library in case it's loaded after the document has loaded
344     api.init = init;
345
346     // Execute listener immediately if already initialized
347     api.addInitListener = function(listener) {
348         if (api.initialized) {
349             listener(api);
350         } else {
351             initListeners.push(listener);
352         }
353     };
354
355     var shimListeners = [];
356
357     api.addShimListener = function(listener) {
358         shimListeners.push(listener);
359     };
360
361     function shim(win) {
362         win = win || window;
363         init();
364
365         // Notify listeners
366         for (var i = 0, len = shimListeners.length; i < len; ++i) {
367             shimListeners[i](win);
368         }
369     }
370
371     api.shim = api.createMissingNativeApi = shim;
372
373     function Module(name, dependencies, initializer) {
374         this.name = name;
375         this.dependencies = dependencies;
376         this.initialized = false;
377         this.supported = false;
378         this.initializer = initializer;
379     }
380
381     Module.prototype = {
382         init: function() {
383             var requiredModuleNames = this.dependencies || [];
384             for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
385                 moduleName = requiredModuleNames[i];
386
387                 requiredModule = modules[moduleName];
388                 if (!requiredModule || !(requiredModule instanceof Module)) {
389                     throw new Error("required module '" + moduleName + "' not found");
390                 }
391
392                 requiredModule.init();
393
394                 if (!requiredModule.supported) {
395                     throw new Error("required module '" + moduleName + "' not supported");
396                 }
397             }
398             
399             // Now run initializer
400             this.initializer(this);
401         },
402         
403         fail: function(reason) {
404             this.initialized = true;
405             this.supported = false;
406             throw new Error("Module '" + this.name + "' failed to load: " + reason);
407         },
408
409         warn: function(msg) {
410             api.warn("Module " + this.name + ": " + msg);
411         },
412
413         deprecationNotice: function(deprecated, replacement) {
414             api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " +
415                 replacement + " instead");
416         },
417
418         createError: function(msg) {
419             return new Error("Error in Rangy " + this.name + " module: " + msg);
420         }
421     };
422     
423     function createModule(isCore, name, dependencies, initFunc) {
424         var newModule = new Module(name, dependencies, function(module) {
425             if (!module.initialized) {
426                 module.initialized = true;
427                 try {
428                     initFunc(api, module);
429                     module.supported = true;
430                 } catch (ex) {
431                     var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
432                     consoleLog(errorMessage);
433                 }
434             }
435         });
436         modules[name] = newModule;
437     }
438
439     api.createModule = function(name) {
440         // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
441         var initFunc, dependencies;
442         if (arguments.length == 2) {
443             initFunc = arguments[1];
444             dependencies = [];
445         } else {
446             initFunc = arguments[2];
447             dependencies = arguments[1];
448         }
449
450         var module = createModule(false, name, dependencies, initFunc);
451
452         // Initialize the module immediately if the core is already initialized
453         if (api.initialized) {
454             module.init();
455         }
456     };
457
458     api.createCoreModule = function(name, dependencies, initFunc) {
459         createModule(true, name, dependencies, initFunc);
460     };
461
462     /*----------------------------------------------------------------------------------------------------------------*/
463
464     // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
465
466     function RangePrototype() {}
467     api.RangePrototype = RangePrototype;
468     api.rangePrototype = new RangePrototype();
469
470     function SelectionPrototype() {}
471     api.selectionPrototype = new SelectionPrototype();
472
473     /*----------------------------------------------------------------------------------------------------------------*/
474
475     // Wait for document to load before running tests
476
477     var docReady = false;
478
479     var loadHandler = function(e) {
480         if (!docReady) {
481             docReady = true;
482             if (!api.initialized && api.config.autoInitialize) {
483                 init();
484             }
485         }
486     };
487
488     // Test whether we have window and document objects that we will need
489     if (typeof window == UNDEFINED) {
490         fail("No window found");
491         return;
492     }
493     if (typeof document == UNDEFINED) {
494         fail("No document found");
495         return;
496     }
497
498     if (isHostMethod(document, "addEventListener")) {
499         document.addEventListener("DOMContentLoaded", loadHandler, false);
500     }
501
502     // Add a fallback in case the DOMContentLoaded event isn't supported
503     addListener(window, "load", loadHandler);
504
505     /*----------------------------------------------------------------------------------------------------------------*/
506     
507     // DOM utility methods used by Rangy
508     api.createCoreModule("DomUtil", [], function(api, module) {
509         var UNDEF = "undefined";
510         var util = api.util;
511
512         // Perform feature tests
513         if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
514             module.fail("document missing a Node creation method");
515         }
516
517         if (!util.isHostMethod(document, "getElementsByTagName")) {
518             module.fail("document missing getElementsByTagName method");
519         }
520
521         var el = document.createElement("div");
522         if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
523                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
524             module.fail("Incomplete Element implementation");
525         }
526
527         // innerHTML is required for Range's createContextualFragment method
528         if (!util.isHostProperty(el, "innerHTML")) {
529             module.fail("Element is missing innerHTML property");
530         }
531
532         var textNode = document.createTextNode("test");
533         if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
534                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
535                 !util.areHostProperties(textNode, ["data"]))) {
536             module.fail("Incomplete Text Node implementation");
537         }
538
539         /*----------------------------------------------------------------------------------------------------------------*/
540
541         // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
542         // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
543         // contains just the document as a single element and the value searched for is the document.
544         var arrayContains = /*Array.prototype.indexOf ?
545             function(arr, val) {
546                 return arr.indexOf(val) > -1;
547             }:*/
548
549             function(arr, val) {
550                 var i = arr.length;
551                 while (i--) {
552                     if (arr[i] === val) {
553                         return true;
554                     }
555                 }
556                 return false;
557             };
558
559         // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
560         function isHtmlNamespace(node) {
561             var ns;
562             return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
563         }
564
565         function parentElement(node) {
566             var parent = node.parentNode;
567             return (parent.nodeType == 1) ? parent : null;
568         }
569
570         function getNodeIndex(node) {
571             var i = 0;
572             while( (node = node.previousSibling) ) {
573                 ++i;
574             }
575             return i;
576         }
577
578         function getNodeLength(node) {
579             switch (node.nodeType) {
580                 case 7:
581                 case 10:
582                     return 0;
583                 case 3:
584                 case 8:
585                     return node.length;
586                 default:
587                     return node.childNodes.length;
588             }
589         }
590
591         function getCommonAncestor(node1, node2) {
592             var ancestors = [], n;
593             for (n = node1; n; n = n.parentNode) {
594                 ancestors.push(n);
595             }
596
597             for (n = node2; n; n = n.parentNode) {
598                 if (arrayContains(ancestors, n)) {
599                     return n;
600                 }
601             }
602
603             return null;
604         }
605
606         function isAncestorOf(ancestor, descendant, selfIsAncestor) {
607             var n = selfIsAncestor ? descendant : descendant.parentNode;
608             while (n) {
609                 if (n === ancestor) {
610                     return true;
611                 } else {
612                     n = n.parentNode;
613                 }
614             }
615             return false;
616         }
617
618         function isOrIsAncestorOf(ancestor, descendant) {
619             return isAncestorOf(ancestor, descendant, true);
620         }
621
622         function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
623             var p, n = selfIsAncestor ? node : node.parentNode;
624             while (n) {
625                 p = n.parentNode;
626                 if (p === ancestor) {
627                     return n;
628                 }
629                 n = p;
630             }
631             return null;
632         }
633
634         function isCharacterDataNode(node) {
635             var t = node.nodeType;
636             return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
637         }
638
639         function isTextOrCommentNode(node) {
640             if (!node) {
641                 return false;
642             }
643             var t = node.nodeType;
644             return t == 3 || t == 8 ; // Text or Comment
645         }
646
647         function insertAfter(node, precedingNode) {
648             var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
649             if (nextNode) {
650                 parent.insertBefore(node, nextNode);
651             } else {
652                 parent.appendChild(node);
653             }
654             return node;
655         }
656
657         // Note that we cannot use splitText() because it is bugridden in IE 9.
658         function splitDataNode(node, index, positionsToPreserve) {
659             var newNode = node.cloneNode(false);
660             newNode.deleteData(0, index);
661             node.deleteData(index, node.length - index);
662             insertAfter(newNode, node);
663
664             // Preserve positions
665             if (positionsToPreserve) {
666                 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
667                     // Handle case where position was inside the portion of node after the split point
668                     if (position.node == node && position.offset > index) {
669                         position.node = newNode;
670                         position.offset -= index;
671                     }
672                     // Handle the case where the position is a node offset within node's parent
673                     else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
674                         ++position.offset;
675                     }
676                 }
677             }
678             return newNode;
679         }
680
681         function getDocument(node) {
682             if (node.nodeType == 9) {
683                 return node;
684             } else if (typeof node.ownerDocument != UNDEF) {
685                 return node.ownerDocument;
686             } else if (typeof node.document != UNDEF) {
687                 return node.document;
688             } else if (node.parentNode) {
689                 return getDocument(node.parentNode);
690             } else {
691                 throw module.createError("getDocument: no document found for node");
692             }
693         }
694
695         function getWindow(node) {
696             var doc = getDocument(node);
697             if (typeof doc.defaultView != UNDEF) {
698                 return doc.defaultView;
699             } else if (typeof doc.parentWindow != UNDEF) {
700                 return doc.parentWindow;
701             } else {
702                 throw module.createError("Cannot get a window object for node");
703             }
704         }
705
706         function getIframeDocument(iframeEl) {
707             if (typeof iframeEl.contentDocument != UNDEF) {
708                 return iframeEl.contentDocument;
709             } else if (typeof iframeEl.contentWindow != UNDEF) {
710                 return iframeEl.contentWindow.document;
711             } else {
712                 throw module.createError("getIframeDocument: No Document object found for iframe element");
713             }
714         }
715
716         function getIframeWindow(iframeEl) {
717             if (typeof iframeEl.contentWindow != UNDEF) {
718                 return iframeEl.contentWindow;
719             } else if (typeof iframeEl.contentDocument != UNDEF) {
720                 return iframeEl.contentDocument.defaultView;
721             } else {
722                 throw module.createError("getIframeWindow: No Window object found for iframe element");
723             }
724         }
725
726         // This looks bad. Is it worth it?
727         function isWindow(obj) {
728             return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
729         }
730
731         function getContentDocument(obj, module, methodName) {
732             var doc;
733
734             if (!obj) {
735                 doc = document;
736             }
737
738             // Test if a DOM node has been passed and obtain a document object for it if so
739             else if (util.isHostProperty(obj, "nodeType")) {
740                 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
741                     getIframeDocument(obj) : getDocument(obj);
742             }
743
744             // Test if the doc parameter appears to be a Window object
745             else if (isWindow(obj)) {
746                 doc = obj.document;
747             }
748
749             if (!doc) {
750                 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
751             }
752
753             return doc;
754         }
755
756         function getRootContainer(node) {
757             var parent;
758             while ( (parent = node.parentNode) ) {
759                 node = parent;
760             }
761             return node;
762         }
763
764         function comparePoints(nodeA, offsetA, nodeB, offsetB) {
765             // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
766             var nodeC, root, childA, childB, n;
767             if (nodeA == nodeB) {
768                 // Case 1: nodes are the same
769                 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
770             } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
771                 // Case 2: node C (container B or an ancestor) is a child node of A
772                 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
773             } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
774                 // Case 3: node C (container A or an ancestor) is a child node of B
775                 return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
776             } else {
777                 root = getCommonAncestor(nodeA, nodeB);
778                 if (!root) {
779                     throw new Error("comparePoints error: nodes have no common ancestor");
780                 }
781
782                 // Case 4: containers are siblings or descendants of siblings
783                 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
784                 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
785
786                 if (childA === childB) {
787                     // This shouldn't be possible
788                     throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
789                 } else {
790                     n = root.firstChild;
791                     while (n) {
792                         if (n === childA) {
793                             return -1;
794                         } else if (n === childB) {
795                             return 1;
796                         }
797                         n = n.nextSibling;
798                     }
799                 }
800             }
801         }
802
803         /*----------------------------------------------------------------------------------------------------------------*/
804
805         // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
806         var crashyTextNodes = false;
807
808         function isBrokenNode(node) {
809             var n;
810             try {
811                 n = node.parentNode;
812                 return false;
813             } catch (e) {
814                 return true;
815             }
816         }
817
818         (function() {
819             var el = document.createElement("b");
820             el.innerHTML = "1";
821             var textNode = el.firstChild;
822             el.innerHTML = "<br>";
823             crashyTextNodes = isBrokenNode(textNode);
824
825             api.features.crashyTextNodes = crashyTextNodes;
826         })();
827
828         /*----------------------------------------------------------------------------------------------------------------*/
829
830         function inspectNode(node) {
831             if (!node) {
832                 return "[No node]";
833             }
834             if (crashyTextNodes && isBrokenNode(node)) {
835                 return "[Broken node]";
836             }
837             if (isCharacterDataNode(node)) {
838                 return '"' + node.data + '"';
839             }
840             if (node.nodeType == 1) {
841                 var idAttr = node.id ? ' id="' + node.id + '"' : "";
842                 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
843             }
844             return node.nodeName;
845         }
846
847         function fragmentFromNodeChildren(node) {
848             var fragment = getDocument(node).createDocumentFragment(), child;
849             while ( (child = node.firstChild) ) {
850                 fragment.appendChild(child);
851             }
852             return fragment;
853         }
854
855         var getComputedStyleProperty;
856         if (typeof window.getComputedStyle != UNDEF) {
857             getComputedStyleProperty = function(el, propName) {
858                 return getWindow(el).getComputedStyle(el, null)[propName];
859             };
860         } else if (typeof document.documentElement.currentStyle != UNDEF) {
861             getComputedStyleProperty = function(el, propName) {
862                 return el.currentStyle[propName];
863             };
864         } else {
865             module.fail("No means of obtaining computed style properties found");
866         }
867
868         function NodeIterator(root) {
869             this.root = root;
870             this._next = root;
871         }
872
873         NodeIterator.prototype = {
874             _current: null,
875
876             hasNext: function() {
877                 return !!this._next;
878             },
879
880             next: function() {
881                 var n = this._current = this._next;
882                 var child, next;
883                 if (this._current) {
884                     child = n.firstChild;
885                     if (child) {
886                         this._next = child;
887                     } else {
888                         next = null;
889                         while ((n !== this.root) && !(next = n.nextSibling)) {
890                             n = n.parentNode;
891                         }
892                         this._next = next;
893                     }
894                 }
895                 return this._current;
896             },
897
898             detach: function() {
899                 this._current = this._next = this.root = null;
900             }
901         };
902
903         function createIterator(root) {
904             return new NodeIterator(root);
905         }
906
907         function DomPosition(node, offset) {
908             this.node = node;
909             this.offset = offset;
910         }
911
912         DomPosition.prototype = {
913             equals: function(pos) {
914                 return !!pos && this.node === pos.node && this.offset == pos.offset;
915             },
916
917             inspect: function() {
918                 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
919             },
920
921             toString: function() {
922                 return this.inspect();
923             }
924         };
925
926         function DOMException(codeName) {
927             this.code = this[codeName];
928             this.codeName = codeName;
929             this.message = "DOMException: " + this.codeName;
930         }
931
932         DOMException.prototype = {
933             INDEX_SIZE_ERR: 1,
934             HIERARCHY_REQUEST_ERR: 3,
935             WRONG_DOCUMENT_ERR: 4,
936             NO_MODIFICATION_ALLOWED_ERR: 7,
937             NOT_FOUND_ERR: 8,
938             NOT_SUPPORTED_ERR: 9,
939             INVALID_STATE_ERR: 11,
940             INVALID_NODE_TYPE_ERR: 24
941         };
942
943         DOMException.prototype.toString = function() {
944             return this.message;
945         };
946
947         api.dom = {
948             arrayContains: arrayContains,
949             isHtmlNamespace: isHtmlNamespace,
950             parentElement: parentElement,
951             getNodeIndex: getNodeIndex,
952             getNodeLength: getNodeLength,
953             getCommonAncestor: getCommonAncestor,
954             isAncestorOf: isAncestorOf,
955             isOrIsAncestorOf: isOrIsAncestorOf,
956             getClosestAncestorIn: getClosestAncestorIn,
957             isCharacterDataNode: isCharacterDataNode,
958             isTextOrCommentNode: isTextOrCommentNode,
959             insertAfter: insertAfter,
960             splitDataNode: splitDataNode,
961             getDocument: getDocument,
962             getWindow: getWindow,
963             getIframeWindow: getIframeWindow,
964             getIframeDocument: getIframeDocument,
965             getBody: util.getBody,
966             isWindow: isWindow,
967             getContentDocument: getContentDocument,
968             getRootContainer: getRootContainer,
969             comparePoints: comparePoints,
970             isBrokenNode: isBrokenNode,
971             inspectNode: inspectNode,
972             getComputedStyleProperty: getComputedStyleProperty,
973             fragmentFromNodeChildren: fragmentFromNodeChildren,
974             createIterator: createIterator,
975             DomPosition: DomPosition
976         };
977
978         api.DOMException = DOMException;
979     });
980
981     /*----------------------------------------------------------------------------------------------------------------*/
982
983     // Pure JavaScript implementation of DOM Range
984     api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
985         var dom = api.dom;
986         var util = api.util;
987         var DomPosition = dom.DomPosition;
988         var DOMException = api.DOMException;
989
990         var isCharacterDataNode = dom.isCharacterDataNode;
991         var getNodeIndex = dom.getNodeIndex;
992         var isOrIsAncestorOf = dom.isOrIsAncestorOf;
993         var getDocument = dom.getDocument;
994         var comparePoints = dom.comparePoints;
995         var splitDataNode = dom.splitDataNode;
996         var getClosestAncestorIn = dom.getClosestAncestorIn;
997         var getNodeLength = dom.getNodeLength;
998         var arrayContains = dom.arrayContains;
999         var getRootContainer = dom.getRootContainer;
1000         var crashyTextNodes = api.features.crashyTextNodes;
1001
1002         /*----------------------------------------------------------------------------------------------------------------*/
1003
1004         // Utility functions
1005
1006         function isNonTextPartiallySelected(node, range) {
1007             return (node.nodeType != 3) &&
1008                    (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
1009         }
1010
1011         function getRangeDocument(range) {
1012             return range.document || getDocument(range.startContainer);
1013         }
1014
1015         function getBoundaryBeforeNode(node) {
1016             return new DomPosition(node.parentNode, getNodeIndex(node));
1017         }
1018
1019         function getBoundaryAfterNode(node) {
1020             return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1021         }
1022
1023         function insertNodeAtPosition(node, n, o) {
1024             var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1025             if (isCharacterDataNode(n)) {
1026                 if (o == n.length) {
1027                     dom.insertAfter(node, n);
1028                 } else {
1029                     n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1030                 }
1031             } else if (o >= n.childNodes.length) {
1032                 n.appendChild(node);
1033             } else {
1034                 n.insertBefore(node, n.childNodes[o]);
1035             }
1036             return firstNodeInserted;
1037         }
1038
1039         function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1040             assertRangeValid(rangeA);
1041             assertRangeValid(rangeB);
1042
1043             if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1044                 throw new DOMException("WRONG_DOCUMENT_ERR");
1045             }
1046
1047             var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1048                 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1049
1050             return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1051         }
1052
1053         function cloneSubtree(iterator) {
1054             var partiallySelected;
1055             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1056                 partiallySelected = iterator.isPartiallySelectedSubtree();
1057                 node = node.cloneNode(!partiallySelected);
1058                 if (partiallySelected) {
1059                     subIterator = iterator.getSubtreeIterator();
1060                     node.appendChild(cloneSubtree(subIterator));
1061                     subIterator.detach();
1062                 }
1063
1064                 if (node.nodeType == 10) { // DocumentType
1065                     throw new DOMException("HIERARCHY_REQUEST_ERR");
1066                 }
1067                 frag.appendChild(node);
1068             }
1069             return frag;
1070         }
1071
1072         function iterateSubtree(rangeIterator, func, iteratorState) {
1073             var it, n;
1074             iteratorState = iteratorState || { stop: false };
1075             for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1076                 if (rangeIterator.isPartiallySelectedSubtree()) {
1077                     if (func(node) === false) {
1078                         iteratorState.stop = true;
1079                         return;
1080                     } else {
1081                         // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1082                         // the node selected by the Range.
1083                         subRangeIterator = rangeIterator.getSubtreeIterator();
1084                         iterateSubtree(subRangeIterator, func, iteratorState);
1085                         subRangeIterator.detach();
1086                         if (iteratorState.stop) {
1087                             return;
1088                         }
1089                     }
1090                 } else {
1091                     // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1092                     // descendants
1093                     it = dom.createIterator(node);
1094                     while ( (n = it.next()) ) {
1095                         if (func(n) === false) {
1096                             iteratorState.stop = true;
1097                             return;
1098                         }
1099                     }
1100                 }
1101             }
1102         }
1103
1104         function deleteSubtree(iterator) {
1105             var subIterator;
1106             while (iterator.next()) {
1107                 if (iterator.isPartiallySelectedSubtree()) {
1108                     subIterator = iterator.getSubtreeIterator();
1109                     deleteSubtree(subIterator);
1110                     subIterator.detach();
1111                 } else {
1112                     iterator.remove();
1113                 }
1114             }
1115         }
1116
1117         function extractSubtree(iterator) {
1118             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1119
1120                 if (iterator.isPartiallySelectedSubtree()) {
1121                     node = node.cloneNode(false);
1122                     subIterator = iterator.getSubtreeIterator();
1123                     node.appendChild(extractSubtree(subIterator));
1124                     subIterator.detach();
1125                 } else {
1126                     iterator.remove();
1127                 }
1128                 if (node.nodeType == 10) { // DocumentType
1129                     throw new DOMException("HIERARCHY_REQUEST_ERR");
1130                 }
1131                 frag.appendChild(node);
1132             }
1133             return frag;
1134         }
1135
1136         function getNodesInRange(range, nodeTypes, filter) {
1137             var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1138             var filterExists = !!filter;
1139             if (filterNodeTypes) {
1140                 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1141             }
1142
1143             var nodes = [];
1144             iterateSubtree(new RangeIterator(range, false), function(node) {
1145                 if (filterNodeTypes && !regex.test(node.nodeType)) {
1146                     return;
1147                 }
1148                 if (filterExists && !filter(node)) {
1149                     return;
1150                 }
1151                 // Don't include a boundary container if it is a character data node and the range does not contain any
1152                 // of its character data. See issue 190.
1153                 var sc = range.startContainer;
1154                 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1155                     return;
1156                 }
1157
1158                 var ec = range.endContainer;
1159                 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1160                     return;
1161                 }
1162
1163                 nodes.push(node);
1164             });
1165             return nodes;
1166         }
1167
1168         function inspect(range) {
1169             var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1170             return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1171                     dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1172         }
1173
1174         /*----------------------------------------------------------------------------------------------------------------*/
1175
1176         // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1177
1178         function RangeIterator(range, clonePartiallySelectedTextNodes) {
1179             this.range = range;
1180             this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1181
1182
1183             if (!range.collapsed) {
1184                 this.sc = range.startContainer;
1185                 this.so = range.startOffset;
1186                 this.ec = range.endContainer;
1187                 this.eo = range.endOffset;
1188                 var root = range.commonAncestorContainer;
1189
1190                 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1191                     this.isSingleCharacterDataNode = true;
1192                     this._first = this._last = this._next = this.sc;
1193                 } else {
1194                     this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1195                         this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1196                     this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1197                         this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1198                 }
1199             }
1200         }
1201
1202         RangeIterator.prototype = {
1203             _current: null,
1204             _next: null,
1205             _first: null,
1206             _last: null,
1207             isSingleCharacterDataNode: false,
1208
1209             reset: function() {
1210                 this._current = null;
1211                 this._next = this._first;
1212             },
1213
1214             hasNext: function() {
1215                 return !!this._next;
1216             },
1217
1218             next: function() {
1219                 // Move to next node
1220                 var current = this._current = this._next;
1221                 if (current) {
1222                     this._next = (current !== this._last) ? current.nextSibling : null;
1223
1224                     // Check for partially selected text nodes
1225                     if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1226                         if (current === this.ec) {
1227                             (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1228                         }
1229                         if (this._current === this.sc) {
1230                             (current = current.cloneNode(true)).deleteData(0, this.so);
1231                         }
1232                     }
1233                 }
1234
1235                 return current;
1236             },
1237
1238             remove: function() {
1239                 var current = this._current, start, end;
1240
1241                 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1242                     start = (current === this.sc) ? this.so : 0;
1243                     end = (current === this.ec) ? this.eo : current.length;
1244                     if (start != end) {
1245                         current.deleteData(start, end - start);
1246                     }
1247                 } else {
1248                     if (current.parentNode) {
1249                         current.parentNode.removeChild(current);
1250                     } else {
1251                     }
1252                 }
1253             },
1254
1255             // Checks if the current node is partially selected
1256             isPartiallySelectedSubtree: function() {
1257                 var current = this._current;
1258                 return isNonTextPartiallySelected(current, this.range);
1259             },
1260
1261             getSubtreeIterator: function() {
1262                 var subRange;
1263                 if (this.isSingleCharacterDataNode) {
1264                     subRange = this.range.cloneRange();
1265                     subRange.collapse(false);
1266                 } else {
1267                     subRange = new Range(getRangeDocument(this.range));
1268                     var current = this._current;
1269                     var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1270
1271                     if (isOrIsAncestorOf(current, this.sc)) {
1272                         startContainer = this.sc;
1273                         startOffset = this.so;
1274                     }
1275                     if (isOrIsAncestorOf(current, this.ec)) {
1276                         endContainer = this.ec;
1277                         endOffset = this.eo;
1278                     }
1279
1280                     updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1281                 }
1282                 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1283             },
1284
1285             detach: function() {
1286                 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1287             }
1288         };
1289
1290         /*----------------------------------------------------------------------------------------------------------------*/
1291
1292         var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1293         var rootContainerNodeTypes = [2, 9, 11];
1294         var readonlyNodeTypes = [5, 6, 10, 12];
1295         var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1296         var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1297
1298         function createAncestorFinder(nodeTypes) {
1299             return function(node, selfIsAncestor) {
1300                 var t, n = selfIsAncestor ? node : node.parentNode;
1301                 while (n) {
1302                     t = n.nodeType;
1303                     if (arrayContains(nodeTypes, t)) {
1304                         return n;
1305                     }
1306                     n = n.parentNode;
1307                 }
1308                 return null;
1309             };
1310         }
1311
1312         var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1313         var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1314         var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1315
1316         function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1317             if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1318                 throw new DOMException("INVALID_NODE_TYPE_ERR");
1319             }
1320         }
1321
1322         function assertValidNodeType(node, invalidTypes) {
1323             if (!arrayContains(invalidTypes, node.nodeType)) {
1324                 throw new DOMException("INVALID_NODE_TYPE_ERR");
1325             }
1326         }
1327
1328         function assertValidOffset(node, offset) {
1329             if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1330                 throw new DOMException("INDEX_SIZE_ERR");
1331             }
1332         }
1333
1334         function assertSameDocumentOrFragment(node1, node2) {
1335             if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1336                 throw new DOMException("WRONG_DOCUMENT_ERR");
1337             }
1338         }
1339
1340         function assertNodeNotReadOnly(node) {
1341             if (getReadonlyAncestor(node, true)) {
1342                 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1343             }
1344         }
1345
1346         function assertNode(node, codeName) {
1347             if (!node) {
1348                 throw new DOMException(codeName);
1349             }
1350         }
1351
1352         function isOrphan(node) {
1353             return (crashyTextNodes && dom.isBrokenNode(node)) ||
1354                 !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1355         }
1356
1357         function isValidOffset(node, offset) {
1358             return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1359         }
1360
1361         function isRangeValid(range) {
1362             return (!!range.startContainer && !!range.endContainer &&
1363                     !isOrphan(range.startContainer) &&
1364                     !isOrphan(range.endContainer) &&
1365                     isValidOffset(range.startContainer, range.startOffset) &&
1366                     isValidOffset(range.endContainer, range.endOffset));
1367         }
1368
1369         function assertRangeValid(range) {
1370             if (!isRangeValid(range)) {
1371                 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1372             }
1373         }
1374
1375         /*----------------------------------------------------------------------------------------------------------------*/
1376
1377         // Test the browser's innerHTML support to decide how to implement createContextualFragment
1378         var styleEl = document.createElement("style");
1379         var htmlParsingConforms = false;
1380         try {
1381             styleEl.innerHTML = "<b>x</b>";
1382             htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1383         } catch (e) {
1384             // IE 6 and 7 throw
1385         }
1386
1387         api.features.htmlParsingConforms = htmlParsingConforms;
1388
1389         var createContextualFragment = htmlParsingConforms ?
1390
1391             // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1392             // discussion and base code for this implementation at issue 67.
1393             // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1394             // Thanks to Aleks Williams.
1395             function(fragmentStr) {
1396                 // "Let node the context object's start's node."
1397                 var node = this.startContainer;
1398                 var doc = getDocument(node);
1399
1400                 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1401                 // exception and abort these steps."
1402                 if (!node) {
1403                     throw new DOMException("INVALID_STATE_ERR");
1404                 }
1405
1406                 // "Let element be as follows, depending on node's interface:"
1407                 // Document, Document Fragment: null
1408                 var el = null;
1409
1410                 // "Element: node"
1411                 if (node.nodeType == 1) {
1412                     el = node;
1413
1414                 // "Text, Comment: node's parentElement"
1415                 } else if (isCharacterDataNode(node)) {
1416                     el = dom.parentElement(node);
1417                 }
1418
1419                 // "If either element is null or element's ownerDocument is an HTML document
1420                 // and element's local name is "html" and element's namespace is the HTML
1421                 // namespace"
1422                 if (el === null || (
1423                     el.nodeName == "HTML" &&
1424                     dom.isHtmlNamespace(getDocument(el).documentElement) &&
1425                     dom.isHtmlNamespace(el)
1426                 )) {
1427
1428                 // "let element be a new Element with "body" as its local name and the HTML
1429                 // namespace as its namespace.""
1430                     el = doc.createElement("body");
1431                 } else {
1432                     el = el.cloneNode(false);
1433                 }
1434
1435                 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1436                 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1437                 // "In either case, the algorithm must be invoked with fragment as the input
1438                 // and element as the context element."
1439                 el.innerHTML = fragmentStr;
1440
1441                 // "If this raises an exception, then abort these steps. Otherwise, let new
1442                 // children be the nodes returned."
1443
1444                 // "Let fragment be a new DocumentFragment."
1445                 // "Append all new children to fragment."
1446                 // "Return fragment."
1447                 return dom.fragmentFromNodeChildren(el);
1448             } :
1449
1450             // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1451             // previous versions of Rangy used (with the exception of using a body element rather than a div)
1452             function(fragmentStr) {
1453                 var doc = getRangeDocument(this);
1454                 var el = doc.createElement("body");
1455                 el.innerHTML = fragmentStr;
1456
1457                 return dom.fragmentFromNodeChildren(el);
1458             };
1459
1460         function splitRangeBoundaries(range, positionsToPreserve) {
1461             assertRangeValid(range);
1462
1463             var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1464             var startEndSame = (sc === ec);
1465
1466             if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1467                 splitDataNode(ec, eo, positionsToPreserve);
1468             }
1469
1470             if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1471                 sc = splitDataNode(sc, so, positionsToPreserve);
1472                 if (startEndSame) {
1473                     eo -= so;
1474                     ec = sc;
1475                 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1476                     eo++;
1477                 }
1478                 so = 0;
1479             }
1480             range.setStartAndEnd(sc, so, ec, eo);
1481         }
1482         
1483         function rangeToHtml(range) {
1484             assertRangeValid(range);
1485             var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1486             container.appendChild( range.cloneContents() );
1487             return container.innerHTML;
1488         }
1489
1490         /*----------------------------------------------------------------------------------------------------------------*/
1491
1492         var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1493             "commonAncestorContainer"];
1494
1495         var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1496         var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1497
1498         util.extend(api.rangePrototype, {
1499             compareBoundaryPoints: function(how, range) {
1500                 assertRangeValid(this);
1501                 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1502
1503                 var nodeA, offsetA, nodeB, offsetB;
1504                 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1505                 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1506                 nodeA = this[prefixA + "Container"];
1507                 offsetA = this[prefixA + "Offset"];
1508                 nodeB = range[prefixB + "Container"];
1509                 offsetB = range[prefixB + "Offset"];
1510                 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1511             },
1512
1513             insertNode: function(node) {
1514                 assertRangeValid(this);
1515                 assertValidNodeType(node, insertableNodeTypes);
1516                 assertNodeNotReadOnly(this.startContainer);
1517
1518                 if (isOrIsAncestorOf(node, this.startContainer)) {
1519                     throw new DOMException("HIERARCHY_REQUEST_ERR");
1520                 }
1521
1522                 // No check for whether the container of the start of the Range is of a type that does not allow
1523                 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1524                 // to add the node
1525
1526                 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1527                 this.setStartBefore(firstNodeInserted);
1528             },
1529
1530             cloneContents: function() {
1531                 assertRangeValid(this);
1532
1533                 var clone, frag;
1534                 if (this.collapsed) {
1535                     return getRangeDocument(this).createDocumentFragment();
1536                 } else {
1537                     if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1538                         clone = this.startContainer.cloneNode(true);
1539                         clone.data = clone.data.slice(this.startOffset, this.endOffset);
1540                         frag = getRangeDocument(this).createDocumentFragment();
1541                         frag.appendChild(clone);
1542                         return frag;
1543                     } else {
1544                         var iterator = new RangeIterator(this, true);
1545                         clone = cloneSubtree(iterator);
1546                         iterator.detach();
1547                     }
1548                     return clone;
1549                 }
1550             },
1551
1552             canSurroundContents: function() {
1553                 assertRangeValid(this);
1554                 assertNodeNotReadOnly(this.startContainer);
1555                 assertNodeNotReadOnly(this.endContainer);
1556
1557                 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1558                 // no non-text nodes.
1559                 var iterator = new RangeIterator(this, true);
1560                 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1561                         (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1562                 iterator.detach();
1563                 return !boundariesInvalid;
1564             },
1565
1566             surroundContents: function(node) {
1567                 assertValidNodeType(node, surroundNodeTypes);
1568
1569                 if (!this.canSurroundContents()) {
1570                     throw new DOMException("INVALID_STATE_ERR");
1571                 }
1572
1573                 // Extract the contents
1574                 var content = this.extractContents();
1575
1576                 // Clear the children of the node
1577                 if (node.hasChildNodes()) {
1578                     while (node.lastChild) {
1579                         node.removeChild(node.lastChild);
1580                     }
1581                 }
1582
1583                 // Insert the new node and add the extracted contents
1584                 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1585                 node.appendChild(content);
1586
1587                 this.selectNode(node);
1588             },
1589
1590             cloneRange: function() {
1591                 assertRangeValid(this);
1592                 var range = new Range(getRangeDocument(this));
1593                 var i = rangeProperties.length, prop;
1594                 while (i--) {
1595                     prop = rangeProperties[i];
1596                     range[prop] = this[prop];
1597                 }
1598                 return range;
1599             },
1600
1601             toString: function() {
1602                 assertRangeValid(this);
1603                 var sc = this.startContainer;
1604                 if (sc === this.endContainer && isCharacterDataNode(sc)) {
1605                     return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1606                 } else {
1607                     var textParts = [], iterator = new RangeIterator(this, true);
1608                     iterateSubtree(iterator, function(node) {
1609                         // Accept only text or CDATA nodes, not comments
1610                         if (node.nodeType == 3 || node.nodeType == 4) {
1611                             textParts.push(node.data);
1612                         }
1613                     });
1614                     iterator.detach();
1615                     return textParts.join("");
1616                 }
1617             },
1618
1619             // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1620             // been removed from Mozilla.
1621
1622             compareNode: function(node) {
1623                 assertRangeValid(this);
1624
1625                 var parent = node.parentNode;
1626                 var nodeIndex = getNodeIndex(node);
1627
1628                 if (!parent) {
1629                     throw new DOMException("NOT_FOUND_ERR");
1630                 }
1631
1632                 var startComparison = this.comparePoint(parent, nodeIndex),
1633                     endComparison = this.comparePoint(parent, nodeIndex + 1);
1634
1635                 if (startComparison < 0) { // Node starts before
1636                     return (endComparison > 0) ? n_b_a : n_b;
1637                 } else {
1638                     return (endComparison > 0) ? n_a : n_i;
1639                 }
1640             },
1641
1642             comparePoint: function(node, offset) {
1643                 assertRangeValid(this);
1644                 assertNode(node, "HIERARCHY_REQUEST_ERR");
1645                 assertSameDocumentOrFragment(node, this.startContainer);
1646
1647                 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1648                     return -1;
1649                 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1650                     return 1;
1651                 }
1652                 return 0;
1653             },
1654
1655             createContextualFragment: createContextualFragment,
1656
1657             toHtml: function() {
1658                 return rangeToHtml(this);
1659             },
1660
1661             // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1662             // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1663             intersectsNode: function(node, touchingIsIntersecting) {
1664                 assertRangeValid(this);
1665                 assertNode(node, "NOT_FOUND_ERR");
1666                 if (getDocument(node) !== getRangeDocument(this)) {
1667                     return false;
1668                 }
1669
1670                 var parent = node.parentNode, offset = getNodeIndex(node);
1671                 assertNode(parent, "NOT_FOUND_ERR");
1672
1673                 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1674                     endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1675
1676                 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1677             },
1678
1679             isPointInRange: function(node, offset) {
1680                 assertRangeValid(this);
1681                 assertNode(node, "HIERARCHY_REQUEST_ERR");
1682                 assertSameDocumentOrFragment(node, this.startContainer);
1683
1684                 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1685                        (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1686             },
1687
1688             // The methods below are non-standard and invented by me.
1689
1690             // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1691             intersectsRange: function(range) {
1692                 return rangesIntersect(this, range, false);
1693             },
1694
1695             // Sharing a boundary start-to-end or end-to-start does count as intersection.
1696             intersectsOrTouchesRange: function(range) {
1697                 return rangesIntersect(this, range, true);
1698             },
1699
1700             intersection: function(range) {
1701                 if (this.intersectsRange(range)) {
1702                     var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1703                         endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1704
1705                     var intersectionRange = this.cloneRange();
1706                     if (startComparison == -1) {
1707                         intersectionRange.setStart(range.startContainer, range.startOffset);
1708                     }
1709                     if (endComparison == 1) {
1710                         intersectionRange.setEnd(range.endContainer, range.endOffset);
1711                     }
1712                     return intersectionRange;
1713                 }
1714                 return null;
1715             },
1716
1717             union: function(range) {
1718                 if (this.intersectsOrTouchesRange(range)) {
1719                     var unionRange = this.cloneRange();
1720                     if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1721                         unionRange.setStart(range.startContainer, range.startOffset);
1722                     }
1723                     if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1724                         unionRange.setEnd(range.endContainer, range.endOffset);
1725                     }
1726                     return unionRange;
1727                 } else {
1728                     throw new DOMException("Ranges do not intersect");
1729                 }
1730             },
1731
1732             containsNode: function(node, allowPartial) {
1733                 if (allowPartial) {
1734                     return this.intersectsNode(node, false);
1735                 } else {
1736                     return this.compareNode(node) == n_i;
1737                 }
1738             },
1739
1740             containsNodeContents: function(node) {
1741                 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1742             },
1743
1744             containsRange: function(range) {
1745                 var intersection = this.intersection(range);
1746                 return intersection !== null && range.equals(intersection);
1747             },
1748
1749             containsNodeText: function(node) {
1750                 var nodeRange = this.cloneRange();
1751                 nodeRange.selectNode(node);
1752                 var textNodes = nodeRange.getNodes([3]);
1753                 if (textNodes.length > 0) {
1754                     nodeRange.setStart(textNodes[0], 0);
1755                     var lastTextNode = textNodes.pop();
1756                     nodeRange.setEnd(lastTextNode, lastTextNode.length);
1757                     return this.containsRange(nodeRange);
1758                 } else {
1759                     return this.containsNodeContents(node);
1760                 }
1761             },
1762
1763             getNodes: function(nodeTypes, filter) {
1764                 assertRangeValid(this);
1765                 return getNodesInRange(this, nodeTypes, filter);
1766             },
1767
1768             getDocument: function() {
1769                 return getRangeDocument(this);
1770             },
1771
1772             collapseBefore: function(node) {
1773                 this.setEndBefore(node);
1774                 this.collapse(false);
1775             },
1776
1777             collapseAfter: function(node) {
1778                 this.setStartAfter(node);
1779                 this.collapse(true);
1780             },
1781             
1782             getBookmark: function(containerNode) {
1783                 var doc = getRangeDocument(this);
1784                 var preSelectionRange = api.createRange(doc);
1785                 containerNode = containerNode || dom.getBody(doc);
1786                 preSelectionRange.selectNodeContents(containerNode);
1787                 var range = this.intersection(preSelectionRange);
1788                 var start = 0, end = 0;
1789                 if (range) {
1790                     preSelectionRange.setEnd(range.startContainer, range.startOffset);
1791                     start = preSelectionRange.toString().length;
1792                     end = start + range.toString().length;
1793                 }
1794
1795                 return {
1796                     start: start,
1797                     end: end,
1798                     containerNode: containerNode
1799                 };
1800             },
1801             
1802             moveToBookmark: function(bookmark) {
1803                 var containerNode = bookmark.containerNode;
1804                 var charIndex = 0;
1805                 this.setStart(containerNode, 0);
1806                 this.collapse(true);
1807                 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1808                 var nextCharIndex, i, childNodes;
1809
1810                 while (!stop && (node = nodeStack.pop())) {
1811                     if (node.nodeType == 3) {
1812                         nextCharIndex = charIndex + node.length;
1813                         if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1814                             this.setStart(node, bookmark.start - charIndex);
1815                             foundStart = true;
1816                         }
1817                         if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1818                             this.setEnd(node, bookmark.end - charIndex);
1819                             stop = true;
1820                         }
1821                         charIndex = nextCharIndex;
1822                     } else {
1823                         childNodes = node.childNodes;
1824                         i = childNodes.length;
1825                         while (i--) {
1826                             nodeStack.push(childNodes[i]);
1827                         }
1828                     }
1829                 }
1830             },
1831
1832             getName: function() {
1833                 return "DomRange";
1834             },
1835
1836             equals: function(range) {
1837                 return Range.rangesEqual(this, range);
1838             },
1839
1840             isValid: function() {
1841                 return isRangeValid(this);
1842             },
1843             
1844             inspect: function() {
1845                 return inspect(this);
1846             },
1847             
1848             detach: function() {
1849                 // In DOM4, detach() is now a no-op.
1850             }
1851         });
1852
1853         function copyComparisonConstantsToObject(obj) {
1854             obj.START_TO_START = s2s;
1855             obj.START_TO_END = s2e;
1856             obj.END_TO_END = e2e;
1857             obj.END_TO_START = e2s;
1858
1859             obj.NODE_BEFORE = n_b;
1860             obj.NODE_AFTER = n_a;
1861             obj.NODE_BEFORE_AND_AFTER = n_b_a;
1862             obj.NODE_INSIDE = n_i;
1863         }
1864
1865         function copyComparisonConstants(constructor) {
1866             copyComparisonConstantsToObject(constructor);
1867             copyComparisonConstantsToObject(constructor.prototype);
1868         }
1869
1870         function createRangeContentRemover(remover, boundaryUpdater) {
1871             return function() {
1872                 assertRangeValid(this);
1873
1874                 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1875
1876                 var iterator = new RangeIterator(this, true);
1877
1878                 // Work out where to position the range after content removal
1879                 var node, boundary;
1880                 if (sc !== root) {
1881                     node = getClosestAncestorIn(sc, root, true);
1882                     boundary = getBoundaryAfterNode(node);
1883                     sc = boundary.node;
1884                     so = boundary.offset;
1885                 }
1886
1887                 // Check none of the range is read-only
1888                 iterateSubtree(iterator, assertNodeNotReadOnly);
1889
1890                 iterator.reset();
1891
1892                 // Remove the content
1893                 var returnValue = remover(iterator);
1894                 iterator.detach();
1895
1896                 // Move to the new position
1897                 boundaryUpdater(this, sc, so, sc, so);
1898
1899                 return returnValue;
1900             };
1901         }
1902
1903         function createPrototypeRange(constructor, boundaryUpdater) {
1904             function createBeforeAfterNodeSetter(isBefore, isStart) {
1905                 return function(node) {
1906                     assertValidNodeType(node, beforeAfterNodeTypes);
1907                     assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1908
1909                     var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1910                     (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1911                 };
1912             }
1913
1914             function setRangeStart(range, node, offset) {
1915                 var ec = range.endContainer, eo = range.endOffset;
1916                 if (node !== range.startContainer || offset !== range.startOffset) {
1917                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
1918                     // is after the current end. In either case, collapse the range to the new position
1919                     if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1920                         ec = node;
1921                         eo = offset;
1922                     }
1923                     boundaryUpdater(range, node, offset, ec, eo);
1924                 }
1925             }
1926
1927             function setRangeEnd(range, node, offset) {
1928                 var sc = range.startContainer, so = range.startOffset;
1929                 if (node !== range.endContainer || offset !== range.endOffset) {
1930                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
1931                     // is after the current end. In either case, collapse the range to the new position
1932                     if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1933                         sc = node;
1934                         so = offset;
1935                     }
1936                     boundaryUpdater(range, sc, so, node, offset);
1937                 }
1938             }
1939
1940             // Set up inheritance
1941             var F = function() {};
1942             F.prototype = api.rangePrototype;
1943             constructor.prototype = new F();
1944
1945             util.extend(constructor.prototype, {
1946                 setStart: function(node, offset) {
1947                     assertNoDocTypeNotationEntityAncestor(node, true);
1948                     assertValidOffset(node, offset);
1949
1950                     setRangeStart(this, node, offset);
1951                 },
1952
1953                 setEnd: function(node, offset) {
1954                     assertNoDocTypeNotationEntityAncestor(node, true);
1955                     assertValidOffset(node, offset);
1956
1957                     setRangeEnd(this, node, offset);
1958                 },
1959
1960                 /**
1961                  * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1962                  * - Two parameters (node, offset) creates a collapsed range at that position
1963                  * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1964                  *   startOffset and ending at endOffset
1965                  * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1966                  *   startNode and ending at endOffset in endNode
1967                  */
1968                 setStartAndEnd: function() {
1969                     var args = arguments;
1970                     var sc = args[0], so = args[1], ec = sc, eo = so;
1971
1972                     switch (args.length) {
1973                         case 3:
1974                             eo = args[2];
1975                             break;
1976                         case 4:
1977                             ec = args[2];
1978                             eo = args[3];
1979                             break;
1980                     }
1981
1982                     boundaryUpdater(this, sc, so, ec, eo);
1983                 },
1984                 
1985                 setBoundary: function(node, offset, isStart) {
1986                     this["set" + (isStart ? "Start" : "End")](node, offset);
1987                 },
1988
1989                 setStartBefore: createBeforeAfterNodeSetter(true, true),
1990                 setStartAfter: createBeforeAfterNodeSetter(false, true),
1991                 setEndBefore: createBeforeAfterNodeSetter(true, false),
1992                 setEndAfter: createBeforeAfterNodeSetter(false, false),
1993
1994                 collapse: function(isStart) {
1995                     assertRangeValid(this);
1996                     if (isStart) {
1997                         boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1998                     } else {
1999                         boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
2000                     }
2001                 },
2002
2003                 selectNodeContents: function(node) {
2004                     assertNoDocTypeNotationEntityAncestor(node, true);
2005
2006                     boundaryUpdater(this, node, 0, node, getNodeLength(node));
2007                 },
2008
2009                 selectNode: function(node) {
2010                     assertNoDocTypeNotationEntityAncestor(node, false);
2011                     assertValidNodeType(node, beforeAfterNodeTypes);
2012
2013                     var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2014                     boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2015                 },
2016
2017                 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2018
2019                 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2020
2021                 canSurroundContents: function() {
2022                     assertRangeValid(this);
2023                     assertNodeNotReadOnly(this.startContainer);
2024                     assertNodeNotReadOnly(this.endContainer);
2025
2026                     // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2027                     // no non-text nodes.
2028                     var iterator = new RangeIterator(this, true);
2029                     var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2030                             (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2031                     iterator.detach();
2032                     return !boundariesInvalid;
2033                 },
2034
2035                 splitBoundaries: function() {
2036                     splitRangeBoundaries(this);
2037                 },
2038
2039                 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2040                     splitRangeBoundaries(this, positionsToPreserve);
2041                 },
2042
2043                 normalizeBoundaries: function() {
2044                     assertRangeValid(this);
2045
2046                     var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2047
2048                     var mergeForward = function(node) {
2049                         var sibling = node.nextSibling;
2050                         if (sibling && sibling.nodeType == node.nodeType) {
2051                             ec = node;
2052                             eo = node.length;
2053                             node.appendData(sibling.data);
2054                             sibling.parentNode.removeChild(sibling);
2055                         }
2056                     };
2057
2058                     var mergeBackward = function(node) {
2059                         var sibling = node.previousSibling;
2060                         if (sibling && sibling.nodeType == node.nodeType) {
2061                             sc = node;
2062                             var nodeLength = node.length;
2063                             so = sibling.length;
2064                             node.insertData(0, sibling.data);
2065                             sibling.parentNode.removeChild(sibling);
2066                             if (sc == ec) {
2067                                 eo += so;
2068                                 ec = sc;
2069                             } else if (ec == node.parentNode) {
2070                                 var nodeIndex = getNodeIndex(node);
2071                                 if (eo == nodeIndex) {
2072                                     ec = node;
2073                                     eo = nodeLength;
2074                                 } else if (eo > nodeIndex) {
2075                                     eo--;
2076                                 }
2077                             }
2078                         }
2079                     };
2080
2081                     var normalizeStart = true;
2082
2083                     if (isCharacterDataNode(ec)) {
2084                         if (ec.length == eo) {
2085                             mergeForward(ec);
2086                         }
2087                     } else {
2088                         if (eo > 0) {
2089                             var endNode = ec.childNodes[eo - 1];
2090                             if (endNode && isCharacterDataNode(endNode)) {
2091                                 mergeForward(endNode);
2092                             }
2093                         }
2094                         normalizeStart = !this.collapsed;
2095                     }
2096
2097                     if (normalizeStart) {
2098                         if (isCharacterDataNode(sc)) {
2099                             if (so == 0) {
2100                                 mergeBackward(sc);
2101                             }
2102                         } else {
2103                             if (so < sc.childNodes.length) {
2104                                 var startNode = sc.childNodes[so];
2105                                 if (startNode && isCharacterDataNode(startNode)) {
2106                                     mergeBackward(startNode);
2107                                 }
2108                             }
2109                         }
2110                     } else {
2111                         sc = ec;
2112                         so = eo;
2113                     }
2114
2115                     boundaryUpdater(this, sc, so, ec, eo);
2116                 },
2117
2118                 collapseToPoint: function(node, offset) {
2119                     assertNoDocTypeNotationEntityAncestor(node, true);
2120                     assertValidOffset(node, offset);
2121                     this.setStartAndEnd(node, offset);
2122                 }
2123             });
2124
2125             copyComparisonConstants(constructor);
2126         }
2127
2128         /*----------------------------------------------------------------------------------------------------------------*/
2129
2130         // Updates commonAncestorContainer and collapsed after boundary change
2131         function updateCollapsedAndCommonAncestor(range) {
2132             range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2133             range.commonAncestorContainer = range.collapsed ?
2134                 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2135         }
2136
2137         function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2138             range.startContainer = startContainer;
2139             range.startOffset = startOffset;
2140             range.endContainer = endContainer;
2141             range.endOffset = endOffset;
2142             range.document = dom.getDocument(startContainer);
2143
2144             updateCollapsedAndCommonAncestor(range);
2145         }
2146
2147         function Range(doc) {
2148             this.startContainer = doc;
2149             this.startOffset = 0;
2150             this.endContainer = doc;
2151             this.endOffset = 0;
2152             this.document = doc;
2153             updateCollapsedAndCommonAncestor(this);
2154         }
2155
2156         createPrototypeRange(Range, updateBoundaries);
2157
2158         util.extend(Range, {
2159             rangeProperties: rangeProperties,
2160             RangeIterator: RangeIterator,
2161             copyComparisonConstants: copyComparisonConstants,
2162             createPrototypeRange: createPrototypeRange,
2163             inspect: inspect,
2164             toHtml: rangeToHtml,
2165             getRangeDocument: getRangeDocument,
2166             rangesEqual: function(r1, r2) {
2167                 return r1.startContainer === r2.startContainer &&
2168                     r1.startOffset === r2.startOffset &&
2169                     r1.endContainer === r2.endContainer &&
2170                     r1.endOffset === r2.endOffset;
2171             }
2172         });
2173
2174         api.DomRange = Range;
2175     });
2176
2177     /*----------------------------------------------------------------------------------------------------------------*/
2178
2179     // Wrappers for the browser's native DOM Range and/or TextRange implementation 
2180     api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2181         var WrappedRange, WrappedTextRange;
2182         var dom = api.dom;
2183         var util = api.util;
2184         var DomPosition = dom.DomPosition;
2185         var DomRange = api.DomRange;
2186         var getBody = dom.getBody;
2187         var getContentDocument = dom.getContentDocument;
2188         var isCharacterDataNode = dom.isCharacterDataNode;
2189
2190
2191         /*----------------------------------------------------------------------------------------------------------------*/
2192
2193         if (api.features.implementsDomRange) {
2194             // This is a wrapper around the browser's native DOM Range. It has two aims:
2195             // - Provide workarounds for specific browser bugs
2196             // - provide convenient extensions, which are inherited from Rangy's DomRange
2197
2198             (function() {
2199                 var rangeProto;
2200                 var rangeProperties = DomRange.rangeProperties;
2201
2202                 function updateRangeProperties(range) {
2203                     var i = rangeProperties.length, prop;
2204                     while (i--) {
2205                         prop = rangeProperties[i];
2206                         range[prop] = range.nativeRange[prop];
2207                     }
2208                     // Fix for broken collapsed property in IE 9.
2209                     range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2210                 }
2211
2212                 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2213                     var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2214                     var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2215                     var nativeRangeDifferent = !range.equals(range.nativeRange);
2216
2217                     // Always set both boundaries for the benefit of IE9 (see issue 35)
2218                     if (startMoved || endMoved || nativeRangeDifferent) {
2219                         range.setEnd(endContainer, endOffset);
2220                         range.setStart(startContainer, startOffset);
2221                     }
2222                 }
2223
2224                 var createBeforeAfterNodeSetter;
2225
2226                 WrappedRange = function(range) {
2227                     if (!range) {
2228                         throw module.createError("WrappedRange: Range must be specified");
2229                     }
2230                     this.nativeRange = range;
2231                     updateRangeProperties(this);
2232                 };
2233
2234                 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2235
2236                 rangeProto = WrappedRange.prototype;
2237
2238                 rangeProto.selectNode = function(node) {
2239                     this.nativeRange.selectNode(node);
2240                     updateRangeProperties(this);
2241                 };
2242
2243                 rangeProto.cloneContents = function() {
2244                     return this.nativeRange.cloneContents();
2245                 };
2246
2247                 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2248                 // insertNode() is never delegated to the native range.
2249
2250                 rangeProto.surroundContents = function(node) {
2251                     this.nativeRange.surroundContents(node);
2252                     updateRangeProperties(this);
2253                 };
2254
2255                 rangeProto.collapse = function(isStart) {
2256                     this.nativeRange.collapse(isStart);
2257                     updateRangeProperties(this);
2258                 };
2259
2260                 rangeProto.cloneRange = function() {
2261                     return new WrappedRange(this.nativeRange.cloneRange());
2262                 };
2263
2264                 rangeProto.refresh = function() {
2265                     updateRangeProperties(this);
2266                 };
2267
2268                 rangeProto.toString = function() {
2269                     return this.nativeRange.toString();
2270                 };
2271
2272                 // Create test range and node for feature detection
2273
2274                 var testTextNode = document.createTextNode("test");
2275                 getBody(document).appendChild(testTextNode);
2276                 var range = document.createRange();
2277
2278                 /*--------------------------------------------------------------------------------------------------------*/
2279
2280                 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2281                 // correct for it
2282
2283                 range.setStart(testTextNode, 0);
2284                 range.setEnd(testTextNode, 0);
2285
2286                 try {
2287                     range.setStart(testTextNode, 1);
2288
2289                     rangeProto.setStart = function(node, offset) {
2290                         this.nativeRange.setStart(node, offset);
2291                         updateRangeProperties(this);
2292                     };
2293
2294                     rangeProto.setEnd = function(node, offset) {
2295                         this.nativeRange.setEnd(node, offset);
2296                         updateRangeProperties(this);
2297                     };
2298
2299                     createBeforeAfterNodeSetter = function(name) {
2300                         return function(node) {
2301                             this.nativeRange[name](node);
2302                             updateRangeProperties(this);
2303                         };
2304                     };
2305
2306                 } catch(ex) {
2307
2308                     rangeProto.setStart = function(node, offset) {
2309                         try {
2310                             this.nativeRange.setStart(node, offset);
2311                         } catch (ex) {
2312                             this.nativeRange.setEnd(node, offset);
2313                             this.nativeRange.setStart(node, offset);
2314                         }
2315                         updateRangeProperties(this);
2316                     };
2317
2318                     rangeProto.setEnd = function(node, offset) {
2319                         try {
2320                             this.nativeRange.setEnd(node, offset);
2321                         } catch (ex) {
2322                             this.nativeRange.setStart(node, offset);
2323                             this.nativeRange.setEnd(node, offset);
2324                         }
2325                         updateRangeProperties(this);
2326                     };
2327
2328                     createBeforeAfterNodeSetter = function(name, oppositeName) {
2329                         return function(node) {
2330                             try {
2331                                 this.nativeRange[name](node);
2332                             } catch (ex) {
2333                                 this.nativeRange[oppositeName](node);
2334                                 this.nativeRange[name](node);
2335                             }
2336                             updateRangeProperties(this);
2337                         };
2338                     };
2339                 }
2340
2341                 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2342                 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2343                 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2344                 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2345
2346                 /*--------------------------------------------------------------------------------------------------------*/
2347
2348                 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2349                 // whether the native implementation can be trusted
2350                 rangeProto.selectNodeContents = function(node) {
2351                     this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2352                 };
2353
2354                 /*--------------------------------------------------------------------------------------------------------*/
2355
2356                 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2357                 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2358
2359                 range.selectNodeContents(testTextNode);
2360                 range.setEnd(testTextNode, 3);
2361
2362                 var range2 = document.createRange();
2363                 range2.selectNodeContents(testTextNode);
2364                 range2.setEnd(testTextNode, 4);
2365                 range2.setStart(testTextNode, 2);
2366
2367                 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2368                         range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2369                     // This is the wrong way round, so correct for it
2370
2371                     rangeProto.compareBoundaryPoints = function(type, range) {
2372                         range = range.nativeRange || range;
2373                         if (type == range.START_TO_END) {
2374                             type = range.END_TO_START;
2375                         } else if (type == range.END_TO_START) {
2376                             type = range.START_TO_END;
2377                         }
2378                         return this.nativeRange.compareBoundaryPoints(type, range);
2379                     };
2380                 } else {
2381                     rangeProto.compareBoundaryPoints = function(type, range) {
2382                         return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2383                     };
2384                 }
2385
2386                 /*--------------------------------------------------------------------------------------------------------*/
2387
2388                 // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107.
2389
2390                 var el = document.createElement("div");
2391                 el.innerHTML = "123";
2392                 var textNode = el.firstChild;
2393                 var body = getBody(document);
2394                 body.appendChild(el);
2395
2396                 range.setStart(textNode, 1);
2397                 range.setEnd(textNode, 2);
2398                 range.deleteContents();
2399
2400                 if (textNode.data == "13") {
2401                     // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2402                     // extractContents()
2403                     rangeProto.deleteContents = function() {
2404                         this.nativeRange.deleteContents();
2405                         updateRangeProperties(this);
2406                     };
2407
2408                     rangeProto.extractContents = function() {
2409                         var frag = this.nativeRange.extractContents();
2410                         updateRangeProperties(this);
2411                         return frag;
2412                     };
2413                 } else {
2414                 }
2415
2416                 body.removeChild(el);
2417                 body = null;
2418
2419                 /*--------------------------------------------------------------------------------------------------------*/
2420
2421                 // Test for existence of createContextualFragment and delegate to it if it exists
2422                 if (util.isHostMethod(range, "createContextualFragment")) {
2423                     rangeProto.createContextualFragment = function(fragmentStr) {
2424                         return this.nativeRange.createContextualFragment(fragmentStr);
2425                     };
2426                 }
2427
2428                 /*--------------------------------------------------------------------------------------------------------*/
2429
2430                 // Clean up
2431                 getBody(document).removeChild(testTextNode);
2432
2433                 rangeProto.getName = function() {
2434                     return "WrappedRange";
2435                 };
2436
2437                 api.WrappedRange = WrappedRange;
2438
2439                 api.createNativeRange = function(doc) {
2440                     doc = getContentDocument(doc, module, "createNativeRange");
2441                     return doc.createRange();
2442                 };
2443             })();
2444         }
2445         
2446         if (api.features.implementsTextRange) {
2447             /*
2448             This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2449             method. For example, in the following (where pipes denote the selection boundaries):
2450
2451             <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2452
2453             var range = document.selection.createRange();
2454             alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2455
2456             This method returns the common ancestor node of the following:
2457             - the parentElement() of the textRange
2458             - the parentElement() of the textRange after calling collapse(true)
2459             - the parentElement() of the textRange after calling collapse(false)
2460             */
2461             var getTextRangeContainerElement = function(textRange) {
2462                 var parentEl = textRange.parentElement();
2463                 var range = textRange.duplicate();
2464                 range.collapse(true);
2465                 var startEl = range.parentElement();
2466                 range = textRange.duplicate();
2467                 range.collapse(false);
2468                 var endEl = range.parentElement();
2469                 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2470
2471                 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2472             };
2473
2474             var textRangeIsCollapsed = function(textRange) {
2475                 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2476             };
2477
2478             // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2479             // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2480             // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2481             // bugs, handling for inputs and images, plus optimizations.
2482             var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2483                 var workingRange = textRange.duplicate();
2484                 workingRange.collapse(isStart);
2485                 var containerElement = workingRange.parentElement();
2486
2487                 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2488                 // check for that
2489                 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2490                     containerElement = wholeRangeContainerElement;
2491                 }
2492
2493
2494                 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2495                 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2496                 if (!containerElement.canHaveHTML) {
2497                     var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2498                     return {
2499                         boundaryPosition: pos,
2500                         nodeInfo: {
2501                             nodeIndex: pos.offset,
2502                             containerElement: pos.node
2503                         }
2504                     };
2505                 }
2506
2507                 var workingNode = dom.getDocument(containerElement).createElement("span");
2508
2509                 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2510                 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2511                 if (workingNode.parentNode) {
2512                     workingNode.parentNode.removeChild(workingNode);
2513                 }
2514
2515                 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2516                 var previousNode, nextNode, boundaryPosition, boundaryNode;
2517                 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2518                 var childNodeCount = containerElement.childNodes.length;
2519                 var end = childNodeCount;
2520
2521                 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2522                 // after the range boundary.
2523                 var nodeIndex = end;
2524
2525                 while (true) {
2526                     if (nodeIndex == childNodeCount) {
2527                         containerElement.appendChild(workingNode);
2528                     } else {
2529                         containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2530                     }
2531                     workingRange.moveToElementText(workingNode);
2532                     comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2533                     if (comparison == 0 || start == end) {
2534                         break;
2535                     } else if (comparison == -1) {
2536                         if (end == start + 1) {
2537                             // We know the endth child node is after the range boundary, so we must be done.
2538                             break;
2539                         } else {
2540                             start = nodeIndex;
2541                         }
2542                     } else {
2543                         end = (end == start + 1) ? start : nodeIndex;
2544                     }
2545                     nodeIndex = Math.floor((start + end) / 2);
2546                     containerElement.removeChild(workingNode);
2547                 }
2548
2549
2550                 // We've now reached or gone past the boundary of the text range we're interested in
2551                 // so have identified the node we want
2552                 boundaryNode = workingNode.nextSibling;
2553
2554                 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2555                     // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2556                     // the node containing the text range's boundary, so we move the end of the working range to the
2557                     // boundary point and measure the length of its text to get the boundary's offset within the node.
2558                     workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2559
2560                     var offset;
2561
2562                     if (/[\r\n]/.test(boundaryNode.data)) {
2563                         /*
2564                         For the particular case of a boundary within a text node containing rendered line breaks (within a
2565                         <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2566                         IE. The facts:
2567                         
2568                         - Each line break is represented as \r in the text node's data/nodeValue properties
2569                         - Each line break is represented as \r\n in the TextRange's 'text' property
2570                         - The 'text' property of the TextRange does not contain trailing line breaks
2571                         
2572                         To get round the problem presented by the final fact above, we can use the fact that TextRange's
2573                         moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2574                         necessarily the same as the number of characters it was instructed to move. The simplest approach is
2575                         to use this to store the characters moved when moving both the start and end of the range to the
2576                         start of the document body and subtracting the start offset from the end offset (the
2577                         "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2578                         the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2579                         the end of the document) has the same problem.
2580                         
2581                         Another approach that works is to use moveStart() to move the start boundary of the range up to the
2582                         end boundary one character at a time and incrementing a counter with the value returned by the
2583                         moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2584                         expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2585                         by the location of the range within the document).
2586                         
2587                         The approach used below is a hybrid of the two methods above. It uses the fact that a string
2588                         containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2589                         be longer than the text of the TextRange, so the start of the range is moved that length initially
2590                         and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2591                         property. This has good performance in most situations compared to the previous two methods.
2592                         */
2593                         var tempRange = workingRange.duplicate();
2594                         var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2595
2596                         offset = tempRange.moveStart("character", rangeLength);
2597                         while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2598                             offset++;
2599                             tempRange.moveStart("character", 1);
2600                         }
2601                     } else {
2602                         offset = workingRange.text.length;
2603                     }
2604                     boundaryPosition = new DomPosition(boundaryNode, offset);
2605                 } else {
2606
2607                     // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2608                     // a position within that, and likewise for a start boundary preceding a character data node
2609                     previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2610                     nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2611                     if (nextNode && isCharacterDataNode(nextNode)) {
2612                         boundaryPosition = new DomPosition(nextNode, 0);
2613                     } else if (previousNode && isCharacterDataNode(previousNode)) {
2614                         boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2615                     } else {
2616                         boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2617                     }
2618                 }
2619
2620                 // Clean up
2621                 workingNode.parentNode.removeChild(workingNode);
2622
2623                 return {
2624                     boundaryPosition: boundaryPosition,
2625                     nodeInfo: {
2626                         nodeIndex: nodeIndex,
2627                         containerElement: containerElement
2628                     }
2629                 };
2630             };
2631
2632             // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2633             // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2634             // (http://code.google.com/p/ierange/)
2635             var createBoundaryTextRange = function(boundaryPosition, isStart) {
2636                 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2637                 var doc = dom.getDocument(boundaryPosition.node);
2638                 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2639                 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2640
2641                 if (nodeIsDataNode) {
2642                     boundaryNode = boundaryPosition.node;
2643                     boundaryParent = boundaryNode.parentNode;
2644                 } else {
2645                     childNodes = boundaryPosition.node.childNodes;
2646                     boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2647                     boundaryParent = boundaryPosition.node;
2648                 }
2649
2650                 // Position the range immediately before the node containing the boundary
2651                 workingNode = doc.createElement("span");
2652
2653                 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2654                 // the element rather than immediately before or after it
2655                 workingNode.innerHTML = "&#feff;";
2656
2657                 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2658                 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2659                 if (boundaryNode) {
2660                     boundaryParent.insertBefore(workingNode, boundaryNode);
2661                 } else {
2662                     boundaryParent.appendChild(workingNode);
2663                 }
2664
2665                 workingRange.moveToElementText(workingNode);
2666                 workingRange.collapse(!isStart);
2667
2668                 // Clean up
2669                 boundaryParent.removeChild(workingNode);
2670
2671                 // Move the working range to the text offset, if required
2672                 if (nodeIsDataNode) {
2673                     workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2674                 }
2675
2676                 return workingRange;
2677             };
2678
2679             /*------------------------------------------------------------------------------------------------------------*/
2680
2681             // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2682             // prototype
2683
2684             WrappedTextRange = function(textRange) {
2685                 this.textRange = textRange;
2686                 this.refresh();
2687             };
2688
2689             WrappedTextRange.prototype = new DomRange(document);
2690
2691             WrappedTextRange.prototype.refresh = function() {
2692                 var start, end, startBoundary;
2693
2694                 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2695                 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2696
2697                 if (textRangeIsCollapsed(this.textRange)) {
2698                     end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2699                         true).boundaryPosition;
2700                 } else {
2701                     startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2702                     start = startBoundary.boundaryPosition;
2703
2704                     // An optimization used here is that if the start and end boundaries have the same parent element, the
2705                     // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2706                     // the start boundary
2707                     end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2708                         startBoundary.nodeInfo).boundaryPosition;
2709                 }
2710
2711                 this.setStart(start.node, start.offset);
2712                 this.setEnd(end.node, end.offset);
2713             };
2714
2715             WrappedTextRange.prototype.getName = function() {
2716                 return "WrappedTextRange";
2717             };
2718
2719             DomRange.copyComparisonConstants(WrappedTextRange);
2720
2721             var rangeToTextRange = function(range) {
2722                 if (range.collapsed) {
2723                     return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2724                 } else {
2725                     var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2726                     var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2727                     var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2728                     textRange.setEndPoint("StartToStart", startRange);
2729                     textRange.setEndPoint("EndToEnd", endRange);
2730                     return textRange;
2731                 }
2732             };
2733
2734             WrappedTextRange.rangeToTextRange = rangeToTextRange;
2735
2736             WrappedTextRange.prototype.toTextRange = function() {
2737                 return rangeToTextRange(this);
2738             };
2739
2740             api.WrappedTextRange = WrappedTextRange;
2741
2742             // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2743             // implementation to use by default.
2744             if (!api.features.implementsDomRange || api.config.preferTextRange) {
2745                 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2746                 var globalObj = (function() { return this; })();
2747                 if (typeof globalObj.Range == "undefined") {
2748                     globalObj.Range = WrappedTextRange;
2749                 }
2750
2751                 api.createNativeRange = function(doc) {
2752                     doc = getContentDocument(doc, module, "createNativeRange");
2753                     return getBody(doc).createTextRange();
2754                 };
2755
2756                 api.WrappedRange = WrappedTextRange;
2757             }
2758         }
2759
2760         api.createRange = function(doc) {
2761             doc = getContentDocument(doc, module, "createRange");
2762             return new api.WrappedRange(api.createNativeRange(doc));
2763         };
2764
2765         api.createRangyRange = function(doc) {
2766             doc = getContentDocument(doc, module, "createRangyRange");
2767             return new DomRange(doc);
2768         };
2769
2770         api.createIframeRange = function(iframeEl) {
2771             module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
2772             return api.createRange(iframeEl);
2773         };
2774
2775         api.createIframeRangyRange = function(iframeEl) {
2776             module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
2777             return api.createRangyRange(iframeEl);
2778         };
2779
2780         api.addShimListener(function(win) {
2781             var doc = win.document;
2782             if (typeof doc.createRange == "undefined") {
2783                 doc.createRange = function() {
2784                     return api.createRange(doc);
2785                 };
2786             }
2787             doc = win = null;
2788         });
2789     });
2790
2791     /*----------------------------------------------------------------------------------------------------------------*/
2792
2793     // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2794     // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
2795     api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2796         api.config.checkSelectionRanges = true;
2797
2798         var BOOLEAN = "boolean";
2799         var NUMBER = "number";
2800         var dom = api.dom;
2801         var util = api.util;
2802         var isHostMethod = util.isHostMethod;
2803         var DomRange = api.DomRange;
2804         var WrappedRange = api.WrappedRange;
2805         var DOMException = api.DOMException;
2806         var DomPosition = dom.DomPosition;
2807         var getNativeSelection;
2808         var selectionIsCollapsed;
2809         var features = api.features;
2810         var CONTROL = "Control";
2811         var getDocument = dom.getDocument;
2812         var getBody = dom.getBody;
2813         var rangesEqual = DomRange.rangesEqual;
2814
2815
2816         // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
2817         // Boolean (true for backwards).
2818         function isDirectionBackward(dir) {
2819             return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2820         }
2821
2822         function getWindow(win, methodName) {
2823             if (!win) {
2824                 return window;
2825             } else if (dom.isWindow(win)) {
2826                 return win;
2827             } else if (win instanceof WrappedSelection) {
2828                 return win.win;
2829             } else {
2830                 var doc = dom.getContentDocument(win, module, methodName);
2831                 return dom.getWindow(doc);
2832             }
2833         }
2834
2835         function getWinSelection(winParam) {
2836             return getWindow(winParam, "getWinSelection").getSelection();
2837         }
2838
2839         function getDocSelection(winParam) {
2840             return getWindow(winParam, "getDocSelection").document.selection;
2841         }
2842         
2843         function winSelectionIsBackward(sel) {
2844             var backward = false;
2845             if (sel.anchorNode) {
2846                 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2847             }
2848             return backward;
2849         }
2850
2851         // Test for the Range/TextRange and Selection features required
2852         // Test for ability to retrieve selection
2853         var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2854             implementsDocSelection = util.isHostObject(document, "selection");
2855
2856         features.implementsWinGetSelection = implementsWinGetSelection;
2857         features.implementsDocSelection = implementsDocSelection;
2858
2859         var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2860
2861         if (useDocumentSelection) {
2862             getNativeSelection = getDocSelection;
2863             api.isSelectionValid = function(winParam) {
2864                 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2865
2866                 // Check whether the selection TextRange is actually contained within the correct document
2867                 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2868             };
2869         } else if (implementsWinGetSelection) {
2870             getNativeSelection = getWinSelection;
2871             api.isSelectionValid = function() {
2872                 return true;
2873             };
2874         } else {
2875             module.fail("Neither document.selection or window.getSelection() detected.");
2876         }
2877
2878         api.getNativeSelection = getNativeSelection;
2879
2880         var testSelection = getNativeSelection();
2881         var testRange = api.createNativeRange(document);
2882         var body = getBody(document);
2883
2884         // Obtaining a range from a selection
2885         var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2886             ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2887
2888         features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2889
2890         // Test for existence of native selection extend() method
2891         var selectionHasExtend = isHostMethod(testSelection, "extend");
2892         features.selectionHasExtend = selectionHasExtend;
2893         
2894         // Test if rangeCount exists
2895         var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2896         features.selectionHasRangeCount = selectionHasRangeCount;
2897
2898         var selectionSupportsMultipleRanges = false;
2899         var collapsedNonEditableSelectionsSupported = true;
2900
2901         var addRangeBackwardToNative = selectionHasExtend ?
2902             function(nativeSelection, range) {
2903                 var doc = DomRange.getRangeDocument(range);
2904                 var endRange = api.createRange(doc);
2905                 endRange.collapseToPoint(range.endContainer, range.endOffset);
2906                 nativeSelection.addRange(getNativeRange(endRange));
2907                 nativeSelection.extend(range.startContainer, range.startOffset);
2908             } : null;
2909
2910         if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2911                 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2912
2913             (function() {
2914                 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2915                 // performed on the current document's selection. See issue 109.
2916
2917                 // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
2918                 // because initialization usually happens when the document loads, but could be a problem for a script that
2919                 // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
2920                 // selection.
2921                 var sel = window.getSelection();
2922                 if (sel) {
2923                     // Store the current selection
2924                     var originalSelectionRangeCount = sel.rangeCount;
2925                     var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2926                     var originalSelectionRanges = [];
2927                     var originalSelectionBackward = winSelectionIsBackward(sel); 
2928                     for (var i = 0; i < originalSelectionRangeCount; ++i) {
2929                         originalSelectionRanges[i] = sel.getRangeAt(i);
2930                     }
2931                     
2932                     // Create some test elements
2933                     var body = getBody(document);
2934                     var testEl = body.appendChild( document.createElement("div") );
2935                     testEl.contentEditable = "false";
2936                     var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2937
2938                     // Test whether the native selection will allow a collapsed selection within a non-editable element
2939                     var r1 = document.createRange();
2940
2941                     r1.setStart(textNode, 1);
2942                     r1.collapse(true);
2943                     sel.addRange(r1);
2944                     collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2945                     sel.removeAllRanges();
2946
2947                     // Test whether the native selection is capable of supporting multiple ranges.
2948                     if (!selectionHasMultipleRanges) {
2949                         // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2950                         // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2951                         // nothing we can do about this while retaining the feature test so we have to resort to a browser
2952                         // sniff. I'm not happy about it. See
2953                         // https://code.google.com/p/chromium/issues/detail?id=399791
2954                         var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2955                         if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2956                             selectionSupportsMultipleRanges = false;
2957                         } else {
2958                             var r2 = r1.cloneRange();
2959                             r1.setStart(textNode, 0);
2960                             r2.setEnd(textNode, 3);
2961                             r2.setStart(textNode, 2);
2962                             sel.addRange(r1);
2963                             sel.addRange(r2);
2964                             selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2965                         }
2966                     }
2967
2968                     // Clean up
2969                     body.removeChild(testEl);
2970                     sel.removeAllRanges();
2971
2972                     for (i = 0; i < originalSelectionRangeCount; ++i) {
2973                         if (i == 0 && originalSelectionBackward) {
2974                             if (addRangeBackwardToNative) {
2975                                 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2976                             } else {
2977                                 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2978                                 sel.addRange(originalSelectionRanges[i]);
2979                             }
2980                         } else {
2981                             sel.addRange(originalSelectionRanges[i]);
2982                         }
2983                     }
2984                 }
2985             })();
2986         }
2987
2988         features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2989         features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2990
2991         // ControlRanges
2992         var implementsControlRange = false, testControlRange;
2993
2994         if (body && isHostMethod(body, "createControlRange")) {
2995             testControlRange = body.createControlRange();
2996             if (util.areHostProperties(testControlRange, ["item", "add"])) {
2997                 implementsControlRange = true;
2998             }
2999         }
3000         features.implementsControlRange = implementsControlRange;
3001
3002         // Selection collapsedness
3003         if (selectionHasAnchorAndFocus) {
3004             selectionIsCollapsed = function(sel) {
3005                 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3006             };
3007         } else {
3008             selectionIsCollapsed = function(sel) {
3009                 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3010             };
3011         }
3012
3013         function updateAnchorAndFocusFromRange(sel, range, backward) {
3014             var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3015             sel.anchorNode = range[anchorPrefix + "Container"];
3016             sel.anchorOffset = range[anchorPrefix + "Offset"];
3017             sel.focusNode = range[focusPrefix + "Container"];
3018             sel.focusOffset = range[focusPrefix + "Offset"];
3019         }
3020
3021         function updateAnchorAndFocusFromNativeSelection(sel) {
3022             var nativeSel = sel.nativeSelection;
3023             sel.anchorNode = nativeSel.anchorNode;
3024             sel.anchorOffset = nativeSel.anchorOffset;
3025             sel.focusNode = nativeSel.focusNode;
3026             sel.focusOffset = nativeSel.focusOffset;
3027         }
3028
3029         function updateEmptySelection(sel) {
3030             sel.anchorNode = sel.focusNode = null;
3031             sel.anchorOffset = sel.focusOffset = 0;
3032             sel.rangeCount = 0;
3033             sel.isCollapsed = true;
3034             sel._ranges.length = 0;
3035         }
3036
3037         function getNativeRange(range) {
3038             var nativeRange;
3039             if (range instanceof DomRange) {
3040                 nativeRange = api.createNativeRange(range.getDocument());
3041                 nativeRange.setEnd(range.endContainer, range.endOffset);
3042                 nativeRange.setStart(range.startContainer, range.startOffset);
3043             } else if (range instanceof WrappedRange) {
3044                 nativeRange = range.nativeRange;
3045             } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3046                 nativeRange = range;
3047             }
3048             return nativeRange;
3049         }
3050
3051         function rangeContainsSingleElement(rangeNodes) {
3052             if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3053                 return false;
3054             }
3055             for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3056                 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3057                     return false;
3058                 }
3059             }
3060             return true;
3061         }
3062
3063         function getSingleElementFromRange(range) {
3064             var nodes = range.getNodes();
3065             if (!rangeContainsSingleElement(nodes)) {
3066                 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3067             }
3068             return nodes[0];
3069         }
3070
3071         // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3072         function isTextRange(range) {
3073             return !!range && typeof range.text != "undefined";
3074         }
3075
3076         function updateFromTextRange(sel, range) {
3077             // Create a Range from the selected TextRange
3078             var wrappedRange = new WrappedRange(range);
3079             sel._ranges = [wrappedRange];
3080
3081             updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3082             sel.rangeCount = 1;
3083             sel.isCollapsed = wrappedRange.collapsed;
3084         }
3085
3086         function updateControlSelection(sel) {
3087             // Update the wrapped selection based on what's now in the native selection
3088             sel._ranges.length = 0;
3089             if (sel.docSelection.type == "None") {
3090                 updateEmptySelection(sel);
3091             } else {
3092                 var controlRange = sel.docSelection.createRange();
3093                 if (isTextRange(controlRange)) {
3094                     // This case (where the selection type is "Control" and calling createRange() on the selection returns
3095                     // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3096                     // ControlRange have been removed from the ControlRange and removed from the document.
3097                     updateFromTextRange(sel, controlRange);
3098                 } else {
3099                     sel.rangeCount = controlRange.length;
3100                     var range, doc = getDocument(controlRange.item(0));
3101                     for (var i = 0; i < sel.rangeCount; ++i) {
3102                         range = api.createRange(doc);
3103                         range.selectNode(controlRange.item(i));
3104                         sel._ranges.push(range);
3105                     }
3106                     sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3107                     updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3108                 }
3109             }
3110         }
3111
3112         function addRangeToControlSelection(sel, range) {
3113             var controlRange = sel.docSelection.createRange();
3114             var rangeElement = getSingleElementFromRange(range);
3115
3116             // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3117             // contained by the supplied range
3118             var doc = getDocument(controlRange.item(0));
3119             var newControlRange = getBody(doc).createControlRange();
3120             for (var i = 0, len = controlRange.length; i < len; ++i) {
3121                 newControlRange.add(controlRange.item(i));
3122             }
3123             try {
3124                 newControlRange.add(rangeElement);
3125             } catch (ex) {
3126                 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3127             }
3128             newControlRange.select();
3129
3130             // Update the wrapped selection based on what's now in the native selection
3131             updateControlSelection(sel);
3132         }
3133
3134         var getSelectionRangeAt;
3135
3136         if (isHostMethod(testSelection, "getRangeAt")) {
3137             // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3138             // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3139             // lesson to us all, especially me.
3140             getSelectionRangeAt = function(sel, index) {
3141                 try {
3142                     return sel.getRangeAt(index);
3143                 } catch (ex) {
3144                     return null;
3145                 }
3146             };
3147         } else if (selectionHasAnchorAndFocus) {
3148             getSelectionRangeAt = function(sel) {
3149                 var doc = getDocument(sel.anchorNode);
3150                 var range = api.createRange(doc);
3151                 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3152
3153                 // Handle the case when the selection was selected backwards (from the end to the start in the
3154                 // document)
3155                 if (range.collapsed !== this.isCollapsed) {
3156                     range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3157                 }
3158
3159                 return range;
3160             };
3161         }
3162
3163         function WrappedSelection(selection, docSelection, win) {
3164             this.nativeSelection = selection;
3165             this.docSelection = docSelection;
3166             this._ranges = [];
3167             this.win = win;
3168             this.refresh();
3169         }
3170
3171         WrappedSelection.prototype = api.selectionPrototype;
3172
3173         function deleteProperties(sel) {
3174             sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3175             sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3176             sel.detached = true;
3177         }
3178
3179         var cachedRangySelections = [];
3180
3181         function actOnCachedSelection(win, action) {
3182             var i = cachedRangySelections.length, cached, sel;
3183             while (i--) {
3184                 cached = cachedRangySelections[i];
3185                 sel = cached.selection;
3186                 if (action == "deleteAll") {
3187                     deleteProperties(sel);
3188                 } else if (cached.win == win) {
3189                     if (action == "delete") {
3190                         cachedRangySelections.splice(i, 1);
3191                         return true;
3192                     } else {
3193                         return sel;
3194                     }
3195                 }
3196             }
3197             if (action == "deleteAll") {
3198                 cachedRangySelections.length = 0;
3199             }
3200             return null;
3201         }
3202
3203         var getSelection = function(win) {
3204             // Check if the parameter is a Rangy Selection object
3205             if (win && win instanceof WrappedSelection) {
3206                 win.refresh();
3207                 return win;
3208             }
3209
3210             win = getWindow(win, "getNativeSelection");
3211
3212             var sel = actOnCachedSelection(win);
3213             var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3214             if (sel) {
3215                 sel.nativeSelection = nativeSel;
3216                 sel.docSelection = docSel;
3217                 sel.refresh();
3218             } else {
3219                 sel = new WrappedSelection(nativeSel, docSel, win);
3220                 cachedRangySelections.push( { win: win, selection: sel } );
3221             }
3222             return sel;
3223         };
3224
3225         api.getSelection = getSelection;
3226
3227         api.getIframeSelection = function(iframeEl) {
3228             module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
3229             return api.getSelection(dom.getIframeWindow(iframeEl));
3230         };
3231
3232         var selProto = WrappedSelection.prototype;
3233
3234         function createControlSelection(sel, ranges) {
3235             // Ensure that the selection becomes of type "Control"
3236             var doc = getDocument(ranges[0].startContainer);
3237             var controlRange = getBody(doc).createControlRange();
3238             for (var i = 0, el, len = ranges.length; i < len; ++i) {
3239                 el = getSingleElementFromRange(ranges[i]);
3240                 try {
3241                     controlRange.add(el);
3242                 } catch (ex) {
3243                     throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3244                 }
3245             }
3246             controlRange.select();
3247
3248             // Update the wrapped selection based on what's now in the native selection
3249             updateControlSelection(sel);
3250         }
3251
3252         // Selecting a range
3253         if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3254             selProto.removeAllRanges = function() {
3255                 this.nativeSelection.removeAllRanges();
3256                 updateEmptySelection(this);
3257             };
3258
3259             var addRangeBackward = function(sel, range) {
3260                 addRangeBackwardToNative(sel.nativeSelection, range);
3261                 sel.refresh();
3262             };
3263
3264             if (selectionHasRangeCount) {
3265                 selProto.addRange = function(range, direction) {
3266                     if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3267                         addRangeToControlSelection(this, range);
3268                     } else {
3269                         if (isDirectionBackward(direction) && selectionHasExtend) {
3270                             addRangeBackward(this, range);
3271                         } else {
3272                             var previousRangeCount;
3273                             if (selectionSupportsMultipleRanges) {
3274                                 previousRangeCount = this.rangeCount;
3275                             } else {
3276                                 this.removeAllRanges();
3277                                 previousRangeCount = 0;
3278                             }
3279                             // Clone the native range so that changing the selected range does not affect the selection.
3280                             // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3281                             // issue 80.
3282                             this.nativeSelection.addRange(getNativeRange(range).cloneRange());
3283
3284                             // Check whether adding the range was successful
3285                             this.rangeCount = this.nativeSelection.rangeCount;
3286
3287                             if (this.rangeCount == previousRangeCount + 1) {
3288                                 // The range was added successfully
3289
3290                                 // Check whether the range that we added to the selection is reflected in the last range extracted from
3291                                 // the selection
3292                                 if (api.config.checkSelectionRanges) {
3293                                     var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3294                                     if (nativeRange && !rangesEqual(nativeRange, range)) {
3295                                         // Happens in WebKit with, for example, a selection placed at the start of a text node
3296                                         range = new WrappedRange(nativeRange);
3297                                     }
3298                                 }
3299                                 this._ranges[this.rangeCount - 1] = range;
3300                                 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3301                                 this.isCollapsed = selectionIsCollapsed(this);
3302                             } else {
3303                                 // The range was not added successfully. The simplest thing is to refresh
3304                                 this.refresh();
3305                             }
3306                         }
3307                     }
3308                 };
3309             } else {
3310                 selProto.addRange = function(range, direction) {
3311                     if (isDirectionBackward(direction) && selectionHasExtend) {
3312                         addRangeBackward(this, range);
3313                     } else {
3314                         this.nativeSelection.addRange(getNativeRange(range));
3315                         this.refresh();
3316                     }
3317                 };
3318             }
3319
3320             selProto.setRanges = function(ranges) {
3321                 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3322                     createControlSelection(this, ranges);
3323                 } else {
3324                     this.removeAllRanges();
3325                     for (var i = 0, len = ranges.length; i < len; ++i) {
3326                         this.addRange(ranges[i]);
3327                     }
3328                 }
3329             };
3330         } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3331                    implementsControlRange && useDocumentSelection) {
3332
3333             selProto.removeAllRanges = function() {
3334                 // Added try/catch as fix for issue #21
3335                 try {
3336                     this.docSelection.empty();
3337
3338                     // Check for empty() not working (issue #24)
3339                     if (this.docSelection.type != "None") {
3340                         // Work around failure to empty a control selection by instead selecting a TextRange and then
3341                         // calling empty()
3342                         var doc;
3343                         if (this.anchorNode) {
3344                             doc = getDocument(this.anchorNode);
3345                         } else if (this.docSelection.type == CONTROL) {
3346                             var controlRange = this.docSelection.createRange();
3347                             if (controlRange.length) {
3348                                 doc = getDocument( controlRange.item(0) );
3349                             }
3350                         }
3351                         if (doc) {
3352                             var textRange = getBody(doc).createTextRange();
3353                             textRange.select();
3354                             this.docSelection.empty();
3355                         }
3356                     }
3357                 } catch(ex) {}
3358                 updateEmptySelection(this);
3359             };
3360
3361             selProto.addRange = function(range) {
3362                 if (this.docSelection.type == CONTROL) {
3363                     addRangeToControlSelection(this, range);
3364                 } else {
3365                     api.WrappedTextRange.rangeToTextRange(range).select();
3366                     this._ranges[0] = range;
3367                     this.rangeCount = 1;
3368                     this.isCollapsed = this._ranges[0].collapsed;
3369                     updateAnchorAndFocusFromRange(this, range, false);
3370                 }
3371             };
3372
3373             selProto.setRanges = function(ranges) {
3374                 this.removeAllRanges();
3375                 var rangeCount = ranges.length;
3376                 if (rangeCount > 1) {
3377                     createControlSelection(this, ranges);
3378                 } else if (rangeCount) {
3379                     this.addRange(ranges[0]);
3380                 }
3381             };
3382         } else {
3383             module.fail("No means of selecting a Range or TextRange was found");
3384             return false;
3385         }
3386
3387         selProto.getRangeAt = function(index) {
3388             if (index < 0 || index >= this.rangeCount) {
3389                 throw new DOMException("INDEX_SIZE_ERR");
3390             } else {
3391                 // Clone the range to preserve selection-range independence. See issue 80.
3392                 return this._ranges[index].cloneRange();
3393             }
3394         };
3395
3396         var refreshSelection;
3397
3398         if (useDocumentSelection) {
3399             refreshSelection = function(sel) {
3400                 var range;
3401                 if (api.isSelectionValid(sel.win)) {
3402                     range = sel.docSelection.createRange();
3403                 } else {
3404                     range = getBody(sel.win.document).createTextRange();
3405                     range.collapse(true);
3406                 }
3407
3408                 if (sel.docSelection.type == CONTROL) {
3409                     updateControlSelection(sel);
3410                 } else if (isTextRange(range)) {
3411                     updateFromTextRange(sel, range);
3412                 } else {
3413                     updateEmptySelection(sel);
3414                 }
3415             };
3416         } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3417             refreshSelection = function(sel) {
3418                 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3419                     updateControlSelection(sel);
3420                 } else {
3421                     sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3422                     if (sel.rangeCount) {
3423                         for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3424                             sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3425                         }
3426                         updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3427                         sel.isCollapsed = selectionIsCollapsed(sel);
3428                     } else {
3429                         updateEmptySelection(sel);
3430                     }
3431                 }
3432             };
3433         } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3434             refreshSelection = function(sel) {
3435                 var range, nativeSel = sel.nativeSelection;
3436                 if (nativeSel.anchorNode) {
3437                     range = getSelectionRangeAt(nativeSel, 0);
3438                     sel._ranges = [range];
3439                     sel.rangeCount = 1;
3440                     updateAnchorAndFocusFromNativeSelection(sel);
3441                     sel.isCollapsed = selectionIsCollapsed(sel);
3442                 } else {
3443                     updateEmptySelection(sel);
3444                 }
3445             };
3446         } else {
3447             module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3448             return false;
3449         }
3450
3451         selProto.refresh = function(checkForChanges) {
3452             var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3453             var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3454
3455             refreshSelection(this);
3456             if (checkForChanges) {
3457                 // Check the range count first
3458                 var i = oldRanges.length;
3459                 if (i != this._ranges.length) {
3460                     return true;
3461                 }
3462
3463                 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3464                 // ranges after this
3465                 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3466                     return true;
3467                 }
3468
3469                 // Finally, compare each range in turn
3470                 while (i--) {
3471                     if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3472                         return true;
3473                     }
3474                 }
3475                 return false;
3476             }
3477         };
3478
3479         // Removal of a single range
3480         var removeRangeManually = function(sel, range) {
3481             var ranges = sel.getAllRanges();
3482             sel.removeAllRanges();
3483             for (var i = 0, len = ranges.length; i < len; ++i) {
3484                 if (!rangesEqual(range, ranges[i])) {
3485                     sel.addRange(ranges[i]);
3486                 }
3487             }
3488             if (!sel.rangeCount) {
3489                 updateEmptySelection(sel);
3490             }
3491         };
3492
3493         if (implementsControlRange && implementsDocSelection) {
3494             selProto.removeRange = function(range) {
3495                 if (this.docSelection.type == CONTROL) {
3496                     var controlRange = this.docSelection.createRange();
3497                     var rangeElement = getSingleElementFromRange(range);
3498
3499                     // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3500                     // element contained by the supplied range
3501                     var doc = getDocument(controlRange.item(0));
3502                     var newControlRange = getBody(doc).createControlRange();
3503                     var el, removed = false;
3504                     for (var i = 0, len = controlRange.length; i < len; ++i) {
3505                         el = controlRange.item(i);
3506                         if (el !== rangeElement || removed) {
3507                             newControlRange.add(controlRange.item(i));
3508                         } else {
3509                             removed = true;
3510                         }
3511                     }
3512                     newControlRange.select();
3513
3514                     // Update the wrapped selection based on what's now in the native selection
3515                     updateControlSelection(this);
3516                 } else {
3517                     removeRangeManually(this, range);
3518                 }
3519             };
3520         } else {
3521             selProto.removeRange = function(range) {
3522                 removeRangeManually(this, range);
3523             };
3524         }
3525
3526         // Detecting if a selection is backward
3527         var selectionIsBackward;
3528         if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3529             selectionIsBackward = winSelectionIsBackward;
3530
3531             selProto.isBackward = function() {
3532                 return selectionIsBackward(this);
3533             };
3534         } else {
3535             selectionIsBackward = selProto.isBackward = function() {
3536                 return false;
3537             };
3538         }
3539
3540         // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3541         selProto.isBackwards = selProto.isBackward;
3542
3543         // Selection stringifier
3544         // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3545         // The current spec does not yet define this method.
3546         selProto.toString = function() {
3547             var rangeTexts = [];
3548             for (var i = 0, len = this.rangeCount; i < len; ++i) {
3549                 rangeTexts[i] = "" + this._ranges[i];
3550             }
3551             return rangeTexts.join("");
3552         };
3553
3554         function assertNodeInSameDocument(sel, node) {
3555             if (sel.win.document != getDocument(node)) {
3556                 throw new DOMException("WRONG_DOCUMENT_ERR");
3557             }
3558         }
3559
3560         // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3561         selProto.collapse = function(node, offset) {
3562             assertNodeInSameDocument(this, node);
3563             var range = api.createRange(node);
3564             range.collapseToPoint(node, offset);
3565             this.setSingleRange(range);
3566             this.isCollapsed = true;
3567         };
3568
3569         selProto.collapseToStart = function() {
3570             if (this.rangeCount) {
3571                 var range = this._ranges[0];
3572                 this.collapse(range.startContainer, range.startOffset);
3573             } else {
3574                 throw new DOMException("INVALID_STATE_ERR");
3575             }
3576         };
3577
3578         selProto.collapseToEnd = function() {
3579             if (this.rangeCount) {
3580                 var range = this._ranges[this.rangeCount - 1];
3581                 this.collapse(range.endContainer, range.endOffset);
3582             } else {
3583                 throw new DOMException("INVALID_STATE_ERR");
3584             }
3585         };
3586
3587         // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
3588         // never used by Rangy.
3589         selProto.selectAllChildren = function(node) {
3590             assertNodeInSameDocument(this, node);
3591             var range = api.createRange(node);
3592             range.selectNodeContents(node);
3593             this.setSingleRange(range);
3594         };
3595
3596         selProto.deleteFromDocument = function() {
3597             // Sepcial behaviour required for IE's control selections
3598             if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3599                 var controlRange = this.docSelection.createRange();
3600                 var element;
3601                 while (controlRange.length) {
3602                     element = controlRange.item(0);
3603                     controlRange.remove(element);
3604                     element.parentNode.removeChild(element);
3605                 }
3606                 this.refresh();
3607             } else if (this.rangeCount) {
3608                 var ranges = this.getAllRanges();
3609                 if (ranges.length) {
3610                     this.removeAllRanges();
3611                     for (var i = 0, len = ranges.length; i < len; ++i) {
3612                         ranges[i].deleteContents();
3613                     }
3614                     // The spec says nothing about what the selection should contain after calling deleteContents on each
3615                     // range. Firefox moves the selection to where the final selected range was, so we emulate that
3616                     this.addRange(ranges[len - 1]);
3617                 }
3618             }
3619         };
3620
3621         // The following are non-standard extensions
3622         selProto.eachRange = function(func, returnValue) {
3623             for (var i = 0, len = this._ranges.length; i < len; ++i) {
3624                 if ( func( this.getRangeAt(i) ) ) {
3625                     return returnValue;
3626                 }
3627             }
3628         };
3629
3630         selProto.getAllRanges = function() {
3631             var ranges = [];
3632             this.eachRange(function(range) {
3633                 ranges.push(range);
3634             });
3635             return ranges;
3636         };
3637
3638         selProto.setSingleRange = function(range, direction) {
3639             this.removeAllRanges();
3640             this.addRange(range, direction);
3641         };
3642
3643         selProto.callMethodOnEachRange = function(methodName, params) {
3644             var results = [];
3645             this.eachRange( function(range) {
3646                 results.push( range[methodName].apply(range, params) );
3647             } );
3648             return results;
3649         };
3650         
3651         function createStartOrEndSetter(isStart) {
3652             return function(node, offset) {
3653                 var range;
3654                 if (this.rangeCount) {
3655                     range = this.getRangeAt(0);
3656                     range["set" + (isStart ? "Start" : "End")](node, offset);
3657                 } else {
3658                     range = api.createRange(this.win.document);
3659                     range.setStartAndEnd(node, offset);
3660                 }
3661                 this.setSingleRange(range, this.isBackward());
3662             };
3663         }
3664
3665         selProto.setStart = createStartOrEndSetter(true);
3666         selProto.setEnd = createStartOrEndSetter(false);
3667         
3668         // Add select() method to Range prototype. Any existing selection will be removed.
3669         api.rangePrototype.select = function(direction) {
3670             getSelection( this.getDocument() ).setSingleRange(this, direction);
3671         };
3672
3673         selProto.changeEachRange = function(func) {
3674             var ranges = [];
3675             var backward = this.isBackward();
3676
3677             this.eachRange(function(range) {
3678                 func(range);
3679                 ranges.push(range);
3680             });
3681
3682             this.removeAllRanges();
3683             if (backward && ranges.length == 1) {
3684                 this.addRange(ranges[0], "backward");
3685             } else {
3686                 this.setRanges(ranges);
3687             }
3688         };
3689
3690         selProto.containsNode = function(node, allowPartial) {
3691             return this.eachRange( function(range) {
3692                 return range.containsNode(node, allowPartial);
3693             }, true ) || false;
3694         };
3695
3696         selProto.getBookmark = function(containerNode) {
3697             return {
3698                 backward: this.isBackward(),
3699                 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3700             };
3701         };
3702
3703         selProto.moveToBookmark = function(bookmark) {
3704             var selRanges = [];
3705             for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3706                 range = api.createRange(this.win);
3707                 range.moveToBookmark(rangeBookmark);
3708                 selRanges.push(range);
3709             }
3710             if (bookmark.backward) {
3711                 this.setSingleRange(selRanges[0], "backward");
3712             } else {
3713                 this.setRanges(selRanges);
3714             }
3715         };
3716
3717         selProto.toHtml = function() {
3718             var rangeHtmls = [];
3719             this.eachRange(function(range) {
3720                 rangeHtmls.push( DomRange.toHtml(range) );
3721             });
3722             return rangeHtmls.join("");
3723         };
3724
3725         if (features.implementsTextRange) {
3726             selProto.getNativeTextRange = function() {
3727                 var sel, textRange;
3728                 if ( (sel = this.docSelection) ) {
3729                     var range = sel.createRange();
3730                     if (isTextRange(range)) {
3731                         return range;
3732                     } else {
3733                         throw module.createError("getNativeTextRange: selection is a control selection"); 
3734                     }
3735                 } else if (this.rangeCount > 0) {
3736                     return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3737                 } else {
3738                     throw module.createError("getNativeTextRange: selection contains no range");
3739                 }
3740             };
3741         }
3742
3743         function inspect(sel) {
3744             var rangeInspects = [];
3745             var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3746             var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3747             var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3748
3749             if (typeof sel.rangeCount != "undefined") {
3750                 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3751                     rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3752                 }
3753             }
3754             return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3755                     ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3756         }
3757
3758         selProto.getName = function() {
3759             return "WrappedSelection";
3760         };
3761
3762         selProto.inspect = function() {
3763             return inspect(this);
3764         };
3765
3766         selProto.detach = function() {
3767             actOnCachedSelection(this.win, "delete");
3768             deleteProperties(this);
3769         };
3770
3771         WrappedSelection.detachAll = function() {
3772             actOnCachedSelection(null, "deleteAll");
3773         };
3774
3775         WrappedSelection.inspect = inspect;
3776         WrappedSelection.isDirectionBackward = isDirectionBackward;
3777
3778         api.Selection = WrappedSelection;
3779
3780         api.selectionPrototype = selProto;
3781
3782         api.addShimListener(function(win) {
3783             if (typeof win.getSelection == "undefined") {
3784                 win.getSelection = function() {
3785                     return getSelection(win);
3786                 };
3787             }
3788             win = null;
3789         });
3790     });
3791     
3792
3793     /*----------------------------------------------------------------------------------------------------------------*/
3794
3795     return api;
3796 }, this);;/**
3797  * Selection save and restore module for Rangy.
3798  * Saves and restores user selections using marker invisible elements in the DOM.
3799  *
3800  * Part of Rangy, a cross-browser JavaScript range and selection library
3801  * http://code.google.com/p/rangy/
3802  *
3803  * Depends on Rangy core.
3804  *
3805  * Copyright 2014, Tim Down
3806  * Licensed under the MIT license.
3807  * Version: 1.3alpha.20140804
3808  * Build date: 4 August 2014
3809  */
3810 (function(factory, global) {
3811     if (typeof define == "function" && define.amd) {
3812         // AMD. Register as an anonymous module with a dependency on Rangy.
3813         define(["rangy"], factory);
3814         /*
3815          } else if (typeof exports == "object") {
3816          // Node/CommonJS style for Browserify
3817          module.exports = factory;
3818          */
3819     } else {
3820         // No AMD or CommonJS support so we use the rangy global variable
3821         factory(global.rangy);
3822     }
3823 })(function(rangy) {
3824     rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
3825         var dom = api.dom;
3826
3827         var markerTextChar = "\ufeff";
3828
3829         function gEBI(id, doc) {
3830             return (doc || document).getElementById(id);
3831         }
3832
3833         function insertRangeBoundaryMarker(range, atStart) {
3834             var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
3835             var markerEl;
3836             var doc = dom.getDocument(range.startContainer);
3837
3838             // Clone the Range and collapse to the appropriate boundary point
3839             var boundaryRange = range.cloneRange();
3840             boundaryRange.collapse(atStart);
3841
3842             // Create the marker element containing a single invisible character using DOM methods and insert it
3843             markerEl = doc.createElement("span");
3844             markerEl.id = markerId;
3845             markerEl.style.lineHeight = "0";
3846             markerEl.style.display = "none";
3847             markerEl.className = "rangySelectionBoundary";
3848             markerEl.appendChild(doc.createTextNode(markerTextChar));
3849
3850             boundaryRange.insertNode(markerEl);
3851             return markerEl;
3852         }
3853
3854         function setRangeBoundary(doc, range, markerId, atStart) {
3855             var markerEl = gEBI(markerId, doc);
3856             if (markerEl) {
3857                 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
3858                 markerEl.parentNode.removeChild(markerEl);
3859             } else {
3860                 module.warn("Marker element has been removed. Cannot restore selection.");
3861             }
3862         }
3863
3864         function compareRanges(r1, r2) {
3865             return r2.compareBoundaryPoints(r1.START_TO_START, r1);
3866         }
3867
3868         function saveRange(range, backward) {
3869             var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
3870
3871             if (range.collapsed) {
3872                 endEl = insertRangeBoundaryMarker(range, false);
3873                 return {
3874                     document: doc,
3875                     markerId: endEl.id,
3876                     collapsed: true
3877                 };
3878             } else {
3879                 endEl = insertRangeBoundaryMarker(range, false);
3880                 startEl = insertRangeBoundaryMarker(range, true);
3881
3882                 return {
3883                     document: doc,
3884                     startMarkerId: startEl.id,
3885                     endMarkerId: endEl.id,
3886                     collapsed: false,
3887                     backward: backward,
3888                     toString: function() {
3889                         return "original text: '" + text + "', new text: '" + range.toString() + "'";
3890                     }
3891                 };
3892             }
3893         }
3894
3895         function restoreRange(rangeInfo, normalize) {
3896             var doc = rangeInfo.document;
3897             if (typeof normalize == "undefined") {
3898                 normalize = true;
3899             }
3900             var range = api.createRange(doc);
3901             if (rangeInfo.collapsed) {
3902                 var markerEl = gEBI(rangeInfo.markerId, doc);
3903                 if (markerEl) {
3904                     markerEl.style.display = "inline";
3905                     var previousNode = markerEl.previousSibling;
3906
3907                     // Workaround for issue 17
3908                     if (previousNode && previousNode.nodeType == 3) {
3909                         markerEl.parentNode.removeChild(markerEl);
3910                         range.collapseToPoint(previousNode, previousNode.length);
3911                     } else {
3912                         range.collapseBefore(markerEl);
3913                         markerEl.parentNode.removeChild(markerEl);
3914                     }
3915                 } else {
3916                     module.warn("Marker element has been removed. Cannot restore selection.");
3917                 }
3918             } else {
3919                 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
3920                 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
3921             }
3922
3923             if (normalize) {
3924                 range.normalizeBoundaries();
3925             }
3926
3927             return range;
3928         }
3929
3930         function saveRanges(ranges, backward) {
3931             var rangeInfos = [], range, doc;
3932
3933             // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
3934             ranges = ranges.slice(0);
3935             ranges.sort(compareRanges);
3936
3937             for (var i = 0, len = ranges.length; i < len; ++i) {
3938                 rangeInfos[i] = saveRange(ranges[i], backward);
3939             }
3940
3941             // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
3942             // between its markers
3943             for (i = len - 1; i >= 0; --i) {
3944                 range = ranges[i];
3945                 doc = api.DomRange.getRangeDocument(range);
3946                 if (range.collapsed) {
3947                     range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
3948                 } else {
3949                     range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
3950                     range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
3951                 }
3952             }
3953
3954             return rangeInfos;
3955         }
3956
3957         function saveSelection(win) {
3958             if (!api.isSelectionValid(win)) {
3959                 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
3960                 return null;
3961             }
3962             var sel = api.getSelection(win);
3963             var ranges = sel.getAllRanges();
3964             var backward = (ranges.length == 1 && sel.isBackward());
3965
3966             var rangeInfos = saveRanges(ranges, backward);
3967
3968             // Ensure current selection is unaffected
3969             if (backward) {
3970                 sel.setSingleRange(ranges[0], "backward");
3971             } else {
3972                 sel.setRanges(ranges);
3973             }
3974
3975             return {
3976                 win: win,
3977                 rangeInfos: rangeInfos,
3978                 restored: false
3979             };
3980         }
3981
3982         function restoreRanges(rangeInfos) {
3983             var ranges = [];
3984
3985             // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
3986             // normalization affecting previously restored ranges.
3987             var rangeCount = rangeInfos.length;
3988
3989             for (var i = rangeCount - 1; i >= 0; i--) {
3990                 ranges[i] = restoreRange(rangeInfos[i], true);
3991             }
3992
3993             return ranges;
3994         }
3995
3996         function restoreSelection(savedSelection, preserveDirection) {
3997             if (!savedSelection.restored) {
3998                 var rangeInfos = savedSelection.rangeInfos;
3999                 var sel = api.getSelection(savedSelection.win);
4000                 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
4001
4002                 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4003                     sel.removeAllRanges();
4004                     sel.addRange(ranges[0], true);
4005                 } else {
4006                     sel.setRanges(ranges);
4007                 }
4008
4009                 savedSelection.restored = true;
4010             }
4011         }
4012
4013         function removeMarkerElement(doc, markerId) {
4014             var markerEl = gEBI(markerId, doc);
4015             if (markerEl) {
4016                 markerEl.parentNode.removeChild(markerEl);
4017             }
4018         }
4019
4020         function removeMarkers(savedSelection) {
4021             var rangeInfos = savedSelection.rangeInfos;
4022             for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
4023                 rangeInfo = rangeInfos[i];
4024                 if (rangeInfo.collapsed) {
4025                     removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
4026                 } else {
4027                     removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4028                     removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4029                 }
4030             }
4031         }
4032
4033         api.util.extend(api, {
4034             saveRange: saveRange,
4035             restoreRange: restoreRange,
4036             saveRanges: saveRanges,
4037             restoreRanges: restoreRanges,
4038             saveSelection: saveSelection,
4039             restoreSelection: restoreSelection,
4040             removeMarkerElement: removeMarkerElement,
4041             removeMarkers: removeMarkers
4042         });
4043     });
4044     
4045 }, this);;/*
4046         Base.js, version 1.1a
4047         Copyright 2006-2010, Dean Edwards
4048         License: http://www.opensource.org/licenses/mit-license.php
4049 */
4050
4051 var Base = function() {
4052         // dummy
4053 };
4054
4055 Base.extend = function(_instance, _static) { // subclass
4056         var extend = Base.prototype.extend;
4057         
4058         // build the prototype
4059         Base._prototyping = true;
4060         var proto = new this;
4061         extend.call(proto, _instance);
4062   proto.base = function() {
4063     // call this method from any other method to invoke that method's ancestor
4064   };
4065         delete Base._prototyping;
4066         
4067         // create the wrapper for the constructor function
4068         //var constructor = proto.constructor.valueOf(); //-dean
4069         var constructor = proto.constructor;
4070         var klass = proto.constructor = function() {
4071                 if (!Base._prototyping) {
4072                         if (this._constructing || this.constructor == klass) { // instantiation
4073                                 this._constructing = true;
4074                                 constructor.apply(this, arguments);
4075                                 delete this._constructing;
4076                         } else if (arguments[0] != null) { // casting
4077                                 return (arguments[0].extend || extend).call(arguments[0], proto);
4078                         }
4079                 }
4080         };
4081         
4082         // build the class interface
4083         klass.ancestor = this;
4084         klass.extend = this.extend;
4085         klass.forEach = this.forEach;
4086         klass.implement = this.implement;
4087         klass.prototype = proto;
4088         klass.toString = this.toString;
4089         klass.valueOf = function(type) {
4090                 //return (type == "object") ? klass : constructor; //-dean
4091                 return (type == "object") ? klass : constructor.valueOf();
4092         };
4093         extend.call(klass, _static);
4094         // class initialisation
4095         if (typeof klass.init == "function") klass.init();
4096         return klass;
4097 };
4098
4099 Base.prototype = {      
4100         extend: function(source, value) {
4101                 if (arguments.length > 1) { // extending with a name/value pair
4102                         var ancestor = this[source];
4103                         if (ancestor && (typeof value == "function") && // overriding a method?
4104                                 // the valueOf() comparison is to avoid circular references
4105                                 (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
4106                                 /\bbase\b/.test(value)) {
4107                                 // get the underlying method
4108                                 var method = value.valueOf();
4109                                 // override
4110                                 value = function() {
4111                                         var previous = this.base || Base.prototype.base;
4112                                         this.base = ancestor;
4113                                         var returnValue = method.apply(this, arguments);
4114                                         this.base = previous;
4115                                         return returnValue;
4116                                 };
4117                                 // point to the underlying method
4118                                 value.valueOf = function(type) {
4119                                         return (type == "object") ? value : method;
4120                                 };
4121                                 value.toString = Base.toString;
4122                         }
4123                         this[source] = value;
4124                 } else if (source) { // extending with an object literal
4125                         var extend = Base.prototype.extend;
4126                         // if this object has a customised extend method then use it
4127                         if (!Base._prototyping && typeof this != "function") {
4128                                 extend = this.extend || extend;
4129                         }
4130                         var proto = {toSource: null};
4131                         // do the "toString" and other methods manually
4132                         var hidden = ["constructor", "toString", "valueOf"];
4133                         // if we are prototyping then include the constructor
4134                         var i = Base._prototyping ? 0 : 1;
4135                         while (key = hidden[i++]) {
4136                                 if (source[key] != proto[key]) {
4137                                         extend.call(this, key, source[key]);
4138
4139                                 }
4140                         }
4141                         // copy each of the source object's properties to this object
4142                         for (var key in source) {
4143                                 if (!proto[key]) extend.call(this, key, source[key]);
4144                         }
4145                 }
4146                 return this;
4147         }
4148 };
4149
4150 // initialise
4151 Base = Base.extend({
4152         constructor: function() {
4153                 this.extend(arguments[0]);
4154         }
4155 }, {
4156         ancestor: Object,
4157         version: "1.1",
4158         
4159         forEach: function(object, block, context) {
4160                 for (var key in object) {
4161                         if (this.prototype[key] === undefined) {
4162                                 block.call(context, object[key], key, object);
4163                         }
4164                 }
4165         },
4166                 
4167         implement: function() {
4168                 for (var i = 0; i < arguments.length; i++) {
4169                         if (typeof arguments[i] == "function") {
4170                                 // if it's a function, call it
4171                                 arguments[i](this.prototype);
4172                         } else {
4173                                 // add the interface using the extend method
4174                                 this.prototype.extend(arguments[i]);
4175                         }
4176                 }
4177                 return this;
4178         },
4179         
4180         toString: function() {
4181                 return String(this.valueOf());
4182         }
4183 });;/**
4184  * Detect browser support for specific features
4185  */
4186 wysihtml5.browser = (function() {
4187   var userAgent   = navigator.userAgent,
4188       testElement = document.createElement("div"),
4189       // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
4190       isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1,
4191       isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1,
4192       isChrome    = userAgent.indexOf("Chrome/")      !== -1,
4193       isOpera     = userAgent.indexOf("Opera/")       !== -1;
4194
4195   function iosVersion(userAgent) {
4196     return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
4197   }
4198
4199   function androidVersion(userAgent) {
4200     return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
4201   }
4202
4203   function isIE(version, equation) {
4204     var rv = -1,
4205         re;
4206
4207     if (navigator.appName == 'Microsoft Internet Explorer') {
4208       re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
4209     } else if (navigator.appName == 'Netscape') {
4210       re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
4211     }
4212
4213     if (re && re.exec(navigator.userAgent) != null) {
4214       rv = parseFloat(RegExp.$1);
4215     }
4216
4217     if (rv === -1) { return false; }
4218     if (!version) { return true; }
4219     if (!equation) { return version === rv; }
4220     if (equation === "<") { return version < rv; }
4221     if (equation === ">") { return version > rv; }
4222     if (equation === "<=") { return version <= rv; }
4223     if (equation === ">=") { return version >= rv; }
4224   }
4225
4226   return {
4227     // Static variable needed, publicly accessible, to be able override it in unit tests
4228     USER_AGENT: userAgent,
4229
4230     /**
4231      * Exclude browsers that are not capable of displaying and handling
4232      * contentEditable as desired:
4233      *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
4234      *    - IE < 8 create invalid markup and crash randomly from time to time
4235      *
4236      * @return {Boolean}
4237      */
4238     supported: function() {
4239       var userAgent                   = this.USER_AGENT.toLowerCase(),
4240           // Essential for making html elements editable
4241           hasContentEditableSupport   = "contentEditable" in testElement,
4242           // Following methods are needed in order to interact with the contentEditable area
4243           hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
4244           // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
4245           hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
4246           // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
4247           isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
4248       return hasContentEditableSupport
4249         && hasEditingApiSupport
4250         && hasQuerySelectorSupport
4251         && !isIncompatibleMobileBrowser;
4252     },
4253
4254     isTouchDevice: function() {
4255       return this.supportsEvent("touchmove");
4256     },
4257
4258     isIos: function() {
4259       return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
4260     },
4261
4262     isAndroid: function() {
4263       return this.USER_AGENT.indexOf("Android") !== -1;
4264     },
4265
4266     /**
4267      * Whether the browser supports sandboxed iframes
4268      * Currently only IE 6+ offers such feature <iframe security="restricted">
4269      *
4270      * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
4271      * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
4272      *
4273      * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
4274      */
4275     supportsSandboxedIframes: function() {
4276       return isIE();
4277     },
4278
4279     /**
4280      * IE6+7 throw a mixed content warning when the src of an iframe
4281      * is empty/unset or about:blank
4282      * window.querySelector is implemented as of IE8
4283      */
4284     throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
4285       return !("querySelector" in document);
4286     },
4287
4288     /**
4289      * Whether the caret is correctly displayed in contentEditable elements
4290      * Firefox sometimes shows a huge caret in the beginning after focusing
4291      */
4292     displaysCaretInEmptyContentEditableCorrectly: function() {
4293       return isIE();
4294     },
4295
4296     /**
4297      * Opera and IE are the only browsers who offer the css value
4298      * in the original unit, thx to the currentStyle object
4299      * All other browsers provide the computed style in px via window.getComputedStyle
4300      */
4301     hasCurrentStyleProperty: function() {
4302       return "currentStyle" in testElement;
4303     },
4304
4305     /**
4306      * Firefox on OSX navigates through history when hitting CMD + Arrow right/left
4307      */
4308     hasHistoryIssue: function() {
4309       return isGecko && navigator.platform.substr(0, 3) === "Mac";
4310     },
4311
4312     /**
4313      * Whether the browser inserts a <br> when pressing enter in a contentEditable element
4314      */
4315     insertsLineBreaksOnReturn: function() {
4316       return isGecko;
4317     },
4318
4319     supportsPlaceholderAttributeOn: function(element) {
4320       return "placeholder" in element;
4321     },
4322
4323     supportsEvent: function(eventName) {
4324       return "on" + eventName in testElement || (function() {
4325         testElement.setAttribute("on" + eventName, "return;");
4326         return typeof(testElement["on" + eventName]) === "function";
4327       })();
4328     },
4329
4330     /**
4331      * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
4332      */
4333     supportsEventsInIframeCorrectly: function() {
4334       return !isOpera;
4335     },
4336
4337     /**
4338      * Everything below IE9 doesn't know how to treat HTML5 tags
4339      *
4340      * @param {Object} context The document object on which to check HTML5 support
4341      *
4342      * @example
4343      *    wysihtml5.browser.supportsHTML5Tags(document);
4344      */
4345     supportsHTML5Tags: function(context) {
4346       var element = context.createElement("div"),
4347           html5   = "<article>foo</article>";
4348       element.innerHTML = html5;
4349       return element.innerHTML.toLowerCase() === html5;
4350     },
4351
4352     /**
4353      * Checks whether a document supports a certain queryCommand
4354      * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
4355      * in oder to report correct results
4356      *
4357      * @param {Object} doc Document object on which to check for a query command
4358      * @param {String} command The query command to check for
4359      * @return {Boolean}
4360      *
4361      * @example
4362      *    wysihtml5.browser.supportsCommand(document, "bold");
4363      */
4364     supportsCommand: (function() {
4365       // Following commands are supported but contain bugs in some browsers
4366       var buggyCommands = {
4367         // formatBlock fails with some tags (eg. <blockquote>)
4368         "formatBlock":          isIE(10, "<="),
4369          // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
4370          // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
4371          // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
4372         "insertUnorderedList":  isIE(),
4373         "insertOrderedList":    isIE()
4374       };
4375
4376       // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
4377       var supported = {
4378         "insertHTML": isGecko
4379       };
4380
4381       return function(doc, command) {
4382         var isBuggy = buggyCommands[command];
4383         if (!isBuggy) {
4384           // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
4385           try {
4386             return doc.queryCommandSupported(command);
4387           } catch(e1) {}
4388
4389           try {
4390             return doc.queryCommandEnabled(command);
4391           } catch(e2) {
4392             return !!supported[command];
4393           }
4394         }
4395         return false;
4396       };
4397     })(),
4398
4399     /**
4400      * IE: URLs starting with:
4401      *    www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
4402      *    nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
4403      * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
4404      * space bar when the caret is directly after such an url.
4405      * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
4406      * (related blog post on msdn
4407      * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
4408      */
4409     doesAutoLinkingInContentEditable: function() {
4410       return isIE();
4411     },
4412
4413     /**
4414      * As stated above, IE auto links urls typed into contentEditable elements
4415      * Since IE9 it's possible to prevent this behavior
4416      */
4417     canDisableAutoLinking: function() {
4418       return this.supportsCommand(document, "AutoUrlDetect");
4419     },
4420
4421     /**
4422      * IE leaves an empty paragraph in the contentEditable element after clearing it
4423      * Chrome/Safari sometimes an empty <div>
4424      */
4425     clearsContentEditableCorrectly: function() {
4426       return isGecko || isOpera || isWebKit;
4427     },
4428
4429     /**
4430      * IE gives wrong results for getAttribute
4431      */
4432     supportsGetAttributeCorrectly: function() {
4433       var td = document.createElement("td");
4434       return td.getAttribute("rowspan") != "1";
4435     },
4436
4437     /**
4438      * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
4439      * Chrome and Safari both don't support this
4440      */
4441     canSelectImagesInContentEditable: function() {
4442       return isGecko || isIE() || isOpera;
4443     },
4444
4445     /**
4446      * All browsers except Safari and Chrome automatically scroll the range/caret position into view
4447      */
4448     autoScrollsToCaret: function() {
4449       return !isWebKit;
4450     },
4451
4452     /**
4453      * Check whether the browser automatically closes tags that don't need to be opened
4454      */
4455     autoClosesUnclosedTags: function() {
4456       var clonedTestElement = testElement.cloneNode(false),
4457           returnValue,
4458           innerHTML;
4459
4460       clonedTestElement.innerHTML = "<p><div></div>";
4461       innerHTML                   = clonedTestElement.innerHTML.toLowerCase();
4462       returnValue                 = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
4463
4464       // Cache result by overwriting current function
4465       this.autoClosesUnclosedTags = function() { return returnValue; };
4466
4467       return returnValue;
4468     },
4469
4470     /**
4471      * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
4472      */
4473     supportsNativeGetElementsByClassName: function() {
4474       return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
4475     },
4476
4477     /**
4478      * As of now (19.04.2011) only supported by Firefox 4 and Chrome
4479      * See https://developer.mozilla.org/en/DOM/Selection/modify
4480      */
4481     supportsSelectionModify: function() {
4482       return "getSelection" in window && "modify" in window.getSelection();
4483     },
4484
4485     /**
4486      * Opera needs a white space after a <br> in order to position the caret correctly
4487      */
4488     needsSpaceAfterLineBreak: function() {
4489       return isOpera;
4490     },
4491
4492     /**
4493      * Whether the browser supports the speech api on the given element
4494      * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
4495      *
4496      * @example
4497      *    var input = document.createElement("input");
4498      *    if (wysihtml5.browser.supportsSpeechApiOn(input)) {
4499      *      // ...
4500      *    }
4501      */
4502     supportsSpeechApiOn: function(input) {
4503       var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
4504       return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
4505     },
4506
4507     /**
4508      * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
4509      * See https://connect.microsoft.com/ie/feedback/details/650112
4510      * or try the POC http://tifftiff.de/ie9_crash/
4511      */
4512     crashesWhenDefineProperty: function(property) {
4513       return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
4514     },
4515
4516     /**
4517      * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
4518      */
4519     doesAsyncFocus: function() {
4520       return isIE();
4521     },
4522
4523     /**
4524      * 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
4525      */
4526     hasProblemsSettingCaretAfterImg: function() {
4527       return isIE();
4528     },
4529
4530     hasUndoInContextMenu: function() {
4531       return isGecko || isChrome || isOpera;
4532     },
4533
4534     /**
4535      * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode)
4536      * is used (regardless if rangy or native)
4537      * This especially happens when the caret is positioned right after a <br> because then
4538      * insertNode() will insert the node right before the <br>
4539      */
4540     hasInsertNodeIssue: function() {
4541       return isOpera;
4542     },
4543
4544     /**
4545      * IE 8+9 don't fire the focus event of the <body> when the iframe gets focused (even though the caret gets set into the <body>)
4546      */
4547     hasIframeFocusIssue: function() {
4548       return isIE();
4549     },
4550
4551     /**
4552      * Chrome + Safari create invalid nested markup after paste
4553      *
4554      *  <p>
4555      *    foo
4556      *    <p>bar</p> <!-- BOO! -->
4557      *  </p>
4558      */
4559     createsNestedInvalidMarkupAfterPaste: function() {
4560       return isWebKit;
4561     },
4562
4563     supportsMutationEvents: function() {
4564         return ("MutationEvent" in window);
4565     },
4566
4567     /**
4568       IE (at least up to 11) does not support clipboardData on event.
4569       It is on window but cannot return text/html
4570       Should actually check for clipboardData on paste event, but cannot in firefox
4571     */
4572     supportsModenPaste: function () {
4573       return !("clipboardData" in window);
4574     }
4575   };
4576 })();
4577 ;wysihtml5.lang.array = function(arr) {
4578   return {
4579     /**
4580      * Check whether a given object exists in an array
4581      *
4582      * @example
4583      *    wysihtml5.lang.array([1, 2]).contains(1);
4584      *    // => true
4585      *
4586      * Can be used to match array with array. If intersection is found true is returned
4587      */
4588     contains: function(needle) {
4589       if (Array.isArray(needle)) {
4590         for (var i = needle.length; i--;) {
4591           if (wysihtml5.lang.array(arr).indexOf(needle[i]) !== -1) {
4592             return true;
4593           }
4594         }
4595         return false;
4596       } else {
4597         return wysihtml5.lang.array(arr).indexOf(needle) !== -1;
4598       }
4599     },
4600
4601     /**
4602      * Check whether a given object exists in an array and return index
4603      * If no elelemt found returns -1
4604      *
4605      * @example
4606      *    wysihtml5.lang.array([1, 2]).indexOf(2);
4607      *    // => 1
4608      */
4609     indexOf: function(needle) {
4610         if (arr.indexOf) {
4611           return arr.indexOf(needle);
4612         } else {
4613           for (var i=0, length=arr.length; i<length; i++) {
4614             if (arr[i] === needle) { return i; }
4615           }
4616           return -1;
4617         }
4618     },
4619
4620     /**
4621      * Substract one array from another
4622      *
4623      * @example
4624      *    wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
4625      *    // => [1, 2]
4626      */
4627     without: function(arrayToSubstract) {
4628       arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
4629       var newArr  = [],
4630           i       = 0,
4631           length  = arr.length;
4632       for (; i<length; i++) {
4633         if (!arrayToSubstract.contains(arr[i])) {
4634           newArr.push(arr[i]);
4635         }
4636       }
4637       return newArr;
4638     },
4639
4640     /**
4641      * Return a clean native array
4642      *
4643      * Following will convert a Live NodeList to a proper Array
4644      * @example
4645      *    var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
4646      */
4647     get: function() {
4648       var i        = 0,
4649           length   = arr.length,
4650           newArray = [];
4651       for (; i<length; i++) {
4652         newArray.push(arr[i]);
4653       }
4654       return newArray;
4655     },
4656
4657     /**
4658      * Creates a new array with the results of calling a provided function on every element in this array.
4659      * optionally this can be provided as second argument
4660      *
4661      * @example
4662      *    var childNodes = wysihtml5.lang.array([1,2,3,4]).map(function (value, index, array) {
4663             return value * 2;
4664      *    });
4665      *    // => [2,4,6,8]
4666      */
4667     map: function(callback, thisArg) {
4668       if (Array.prototype.map) {
4669         return arr.map(callback, thisArg);
4670       } else {
4671         var len = arr.length >>> 0,
4672             A = new Array(len),
4673             i = 0;
4674         for (; i < len; i++) {
4675            A[i] = callback.call(thisArg, arr[i], i, arr);
4676         }
4677         return A;
4678       }
4679     },
4680
4681     /* ReturnS new array without duplicate entries
4682      *
4683      * @example
4684      *    var uniq = wysihtml5.lang.array([1,2,3,2,1,4]).unique();
4685      *    // => [1,2,3,4]
4686      */
4687     unique: function() {
4688       var vals = [],
4689           max = arr.length,
4690           idx = 0;
4691
4692       while (idx < max) {
4693         if (!wysihtml5.lang.array(vals).contains(arr[idx])) {
4694           vals.push(arr[idx]);
4695         }
4696         idx++;
4697       }
4698       return vals;
4699     }
4700
4701   };
4702 };
4703 ;wysihtml5.lang.Dispatcher = Base.extend(
4704   /** @scope wysihtml5.lang.Dialog.prototype */ {
4705   on: function(eventName, handler) {
4706     this.events = this.events || {};
4707     this.events[eventName] = this.events[eventName] || [];
4708     this.events[eventName].push(handler);
4709     return this;
4710   },
4711
4712   off: function(eventName, handler) {
4713     this.events = this.events || {};
4714     var i = 0,
4715         handlers,
4716         newHandlers;
4717     if (eventName) {
4718       handlers    = this.events[eventName] || [],
4719       newHandlers = [];
4720       for (; i<handlers.length; i++) {
4721         if (handlers[i] !== handler && handler) {
4722           newHandlers.push(handlers[i]);
4723         }
4724       }
4725       this.events[eventName] = newHandlers;
4726     } else {
4727       // Clean up all events
4728       this.events = {};
4729     }
4730     return this;
4731   },
4732
4733   fire: function(eventName, payload) {
4734     this.events = this.events || {};
4735     var handlers = this.events[eventName] || [],
4736         i        = 0;
4737     for (; i<handlers.length; i++) {
4738       handlers[i].call(this, payload);
4739     }
4740     return this;
4741   },
4742
4743   // deprecated, use .on()
4744   observe: function() {
4745     return this.on.apply(this, arguments);
4746   },
4747
4748   // deprecated, use .off()
4749   stopObserving: function() {
4750     return this.off.apply(this, arguments);
4751   }
4752 });
4753 ;wysihtml5.lang.object = function(obj) {
4754   return {
4755     /**
4756      * @example
4757      *    wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
4758      *    // => { foo: 1, bar: 2, baz: 3 }
4759      */
4760     merge: function(otherObj) {
4761       for (var i in otherObj) {
4762         obj[i] = otherObj[i];
4763       }
4764       return this;
4765     },
4766
4767     get: function() {
4768       return obj;
4769     },
4770
4771     /**
4772      * @example
4773      *    wysihtml5.lang.object({ foo: 1 }).clone();
4774      *    // => { foo: 1 }
4775      *
4776      *    v0.4.14 adds options for deep clone : wysihtml5.lang.object({ foo: 1 }).clone(true);
4777      */
4778     clone: function(deep) {
4779       var newObj = {},
4780           i;
4781
4782       if (obj === null || !wysihtml5.lang.object(obj).isPlainObject()) {
4783         return obj;
4784       }
4785
4786       for (i in obj) {
4787         if(obj.hasOwnProperty(i)) {
4788           if (deep) {
4789             newObj[i] = wysihtml5.lang.object(obj[i]).clone(deep);
4790           } else {
4791             newObj[i] = obj[i];
4792           }
4793         }
4794       }
4795       return newObj;
4796     },
4797
4798     /**
4799      * @example
4800      *    wysihtml5.lang.object([]).isArray();
4801      *    // => true
4802      */
4803     isArray: function() {
4804       return Object.prototype.toString.call(obj) === "[object Array]";
4805     },
4806
4807     /**
4808      * @example
4809      *    wysihtml5.lang.object(function() {}).isFunction();
4810      *    // => true
4811      */
4812     isFunction: function() {
4813       return Object.prototype.toString.call(obj) === '[object Function]';
4814     },
4815
4816     isPlainObject: function () {
4817       return Object.prototype.toString.call(obj) === '[object Object]';
4818     }
4819   };
4820 };
4821 ;(function() {
4822   var WHITE_SPACE_START = /^\s+/,
4823       WHITE_SPACE_END   = /\s+$/,
4824       ENTITY_REG_EXP    = /[&<>\t"]/g,
4825       ENTITY_MAP = {
4826         '&': '&amp;',
4827         '<': '&lt;',
4828         '>': '&gt;',
4829         '"': "&quot;",
4830         '\t':"&nbsp; "
4831       };
4832   wysihtml5.lang.string = function(str) {
4833     str = String(str);
4834     return {
4835       /**
4836        * @example
4837        *    wysihtml5.lang.string("   foo   ").trim();
4838        *    // => "foo"
4839        */
4840       trim: function() {
4841         return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
4842       },
4843
4844       /**
4845        * @example
4846        *    wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
4847        *    // => "Hello Christopher"
4848        */
4849       interpolate: function(vars) {
4850         for (var i in vars) {
4851           str = this.replace("#{" + i + "}").by(vars[i]);
4852         }
4853         return str;
4854       },
4855
4856       /**
4857        * @example
4858        *    wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
4859        *    // => "Hello Hans"
4860        */
4861       replace: function(search) {
4862         return {
4863           by: function(replace) {
4864             return str.split(search).join(replace);
4865           }
4866         };
4867       },
4868
4869       /**
4870        * @example
4871        *    wysihtml5.lang.string("hello<br>").escapeHTML();
4872        *    // => "hello&lt;br&gt;"
4873        */
4874       escapeHTML: function(linebreaks, convertSpaces) {
4875         var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
4876         if (linebreaks) {
4877           html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
4878         }
4879         if (convertSpaces) {
4880           html = html.replace(/  /gi, "&nbsp; ");
4881         }
4882         return html;
4883       }
4884     };
4885   };
4886 })();
4887 ;/**
4888  * Find urls in descendant text nodes of an element and auto-links them
4889  * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
4890  *
4891  * @param {Element} element Container element in which to search for urls
4892  *
4893  * @example
4894  *    <div id="text-container">Please click here: www.google.com</div>
4895  *    <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
4896  */
4897 (function(wysihtml5) {
4898   var /**
4899        * Don't auto-link urls that are contained in the following elements:
4900        */
4901       IGNORE_URLS_IN        = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
4902       /**
4903        * revision 1:
4904        *    /(\S+\.{1}[^\s\,\.\!]+)/g
4905        *
4906        * revision 2:
4907        *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
4908        *
4909        * put this in the beginning if you don't wan't to match within a word
4910        *    (^|[\>\(\{\[\s\>])
4911        */
4912       URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
4913       TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
4914       MAX_DISPLAY_LENGTH    = 100,
4915       BRACKETS              = { ")": "(", "]": "[", "}": "{" };
4916
4917   function autoLink(element, ignoreInClasses) {
4918     if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
4919       return element;
4920     }
4921
4922     if (element === element.ownerDocument.documentElement) {
4923       element = element.ownerDocument.body;
4924     }
4925
4926     return _parseNode(element, ignoreInClasses);
4927   }
4928
4929   /**
4930    * This is basically a rebuild of
4931    * the rails auto_link_urls text helper
4932    */
4933   function _convertUrlsToLinks(str) {
4934     return str.replace(URL_REG_EXP, function(match, url) {
4935       var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
4936           opening     = BRACKETS[punctuation];
4937       url = url.replace(TRAILING_CHAR_REG_EXP, "");
4938
4939       if (url.split(opening).length > url.split(punctuation).length) {
4940         url = url + punctuation;
4941         punctuation = "";
4942       }
4943       var realUrl    = url,
4944           displayUrl = url;
4945       if (url.length > MAX_DISPLAY_LENGTH) {
4946         displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
4947       }
4948       // Add http prefix if necessary
4949       if (realUrl.substr(0, 4) === "www.") {
4950         realUrl = "http://" + realUrl;
4951       }
4952
4953       return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
4954     });
4955   }
4956
4957   /**
4958    * Creates or (if already cached) returns a temp element
4959    * for the given document object
4960    */
4961   function _getTempElement(context) {
4962     var tempElement = context._wysihtml5_tempElement;
4963     if (!tempElement) {
4964       tempElement = context._wysihtml5_tempElement = context.createElement("div");
4965     }
4966     return tempElement;
4967   }
4968
4969   /**
4970    * Replaces the original text nodes with the newly auto-linked dom tree
4971    */
4972   function _wrapMatchesInNode(textNode) {
4973     var parentNode  = textNode.parentNode,
4974         nodeValue   = wysihtml5.lang.string(textNode.data).escapeHTML(),
4975         tempElement = _getTempElement(parentNode.ownerDocument);
4976
4977     // We need to insert an empty/temporary <span /> to fix IE quirks
4978     // Elsewise IE would strip white space in the beginning
4979     tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue);
4980     tempElement.removeChild(tempElement.firstChild);
4981
4982     while (tempElement.firstChild) {
4983       // inserts tempElement.firstChild before textNode
4984       parentNode.insertBefore(tempElement.firstChild, textNode);
4985     }
4986     parentNode.removeChild(textNode);
4987   }
4988
4989   function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
4990     var nodeName;
4991     while (node.parentNode) {
4992       node = node.parentNode;
4993       nodeName = node.nodeName;
4994       if (node.className && wysihtml5.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
4995         return true;
4996       }
4997       if (IGNORE_URLS_IN.contains(nodeName)) {
4998         return true;
4999       } else if (nodeName === "body") {
5000         return false;
5001       }
5002     }
5003     return false;
5004   }
5005
5006   function _parseNode(element, ignoreInClasses) {
5007     if (IGNORE_URLS_IN.contains(element.nodeName)) {
5008       return;
5009     }
5010
5011     if (element.className && wysihtml5.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
5012       return;
5013     }
5014
5015     if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
5016       _wrapMatchesInNode(element);
5017       return;
5018     }
5019
5020     var childNodes        = wysihtml5.lang.array(element.childNodes).get(),
5021         childNodesLength  = childNodes.length,
5022         i                 = 0;
5023
5024     for (; i<childNodesLength; i++) {
5025       _parseNode(childNodes[i], ignoreInClasses);
5026     }
5027
5028     return element;
5029   }
5030
5031   wysihtml5.dom.autoLink = autoLink;
5032
5033   // Reveal url reg exp to the outside
5034   wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
5035 })(wysihtml5);
5036 ;(function(wysihtml5) {
5037   var api = wysihtml5.dom;
5038
5039   api.addClass = function(element, className) {
5040     var classList = element.classList;
5041     if (classList) {
5042       return classList.add(className);
5043     }
5044     if (api.hasClass(element, className)) {
5045       return;
5046     }
5047     element.className += " " + className;
5048   };
5049
5050   api.removeClass = function(element, className) {
5051     var classList = element.classList;
5052     if (classList) {
5053       return classList.remove(className);
5054     }
5055
5056     element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
5057   };
5058
5059   api.hasClass = function(element, className) {
5060     var classList = element.classList;
5061     if (classList) {
5062       return classList.contains(className);
5063     }
5064
5065     var elementClassName = element.className;
5066     return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
5067   };
5068 })(wysihtml5);
5069 ;wysihtml5.dom.contains = (function() {
5070   var documentElement = document.documentElement;
5071   if (documentElement.contains) {
5072     return function(container, element) {
5073       if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5074         element = element.parentNode;
5075       }
5076       return container !== element && container.contains(element);
5077     };
5078   } else if (documentElement.compareDocumentPosition) {
5079     return function(container, element) {
5080       // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
5081       return !!(container.compareDocumentPosition(element) & 16);
5082     };
5083   }
5084 })();
5085 ;/**
5086  * Converts an HTML fragment/element into a unordered/ordered list
5087  *
5088  * @param {Element} element The element which should be turned into a list
5089  * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
5090  * @return {Element} The created list
5091  *
5092  * @example
5093  *    <!-- Assume the following dom: -->
5094  *    <span id="pseudo-list">
5095  *      eminem<br>
5096  *      dr. dre
5097  *      <div>50 Cent</div>
5098  *    </span>
5099  *
5100  *    <script>
5101  *      wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
5102  *    </script>
5103  *
5104  *    <!-- Will result in: -->
5105  *    <ul>
5106  *      <li>eminem</li>
5107  *      <li>dr. dre</li>
5108  *      <li>50 Cent</li>
5109  *    </ul>
5110  */
5111 wysihtml5.dom.convertToList = (function() {
5112   function _createListItem(doc, list) {
5113     var listItem = doc.createElement("li");
5114     list.appendChild(listItem);
5115     return listItem;
5116   }
5117
5118   function _createList(doc, type) {
5119     return doc.createElement(type);
5120   }
5121
5122   function convertToList(element, listType, uneditableClass) {
5123     if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
5124       // Already a list
5125       return element;
5126     }
5127
5128     var doc               = element.ownerDocument,
5129         list              = _createList(doc, listType),
5130         lineBreaks        = element.querySelectorAll("br"),
5131         lineBreaksLength  = lineBreaks.length,
5132         childNodes,
5133         childNodesLength,
5134         childNode,
5135         lineBreak,
5136         parentNode,
5137         isBlockElement,
5138         isLineBreak,
5139         currentListItem,
5140         i;
5141
5142     // First find <br> at the end of inline elements and move them behind them
5143     for (i=0; i<lineBreaksLength; i++) {
5144       lineBreak = lineBreaks[i];
5145       while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
5146         if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
5147           parentNode.removeChild(lineBreak);
5148           break;
5149         }
5150         wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
5151       }
5152     }
5153
5154     childNodes        = wysihtml5.lang.array(element.childNodes).get();
5155     childNodesLength  = childNodes.length;
5156
5157     for (i=0; i<childNodesLength; i++) {
5158       currentListItem   = currentListItem || _createListItem(doc, list);
5159       childNode         = childNodes[i];
5160       isBlockElement    = wysihtml5.dom.getStyle("display").from(childNode) === "block";
5161       isLineBreak       = childNode.nodeName === "BR";
5162
5163       // consider uneditable as an inline element
5164       if (isBlockElement && (!uneditableClass || !wysihtml5.dom.hasClass(childNode, uneditableClass))) {
5165         // Append blockElement to current <li> if empty, otherwise create a new one
5166         currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
5167         currentListItem.appendChild(childNode);
5168         currentListItem = null;
5169         continue;
5170       }
5171
5172       if (isLineBreak) {
5173         // Only create a new list item in the next iteration when the current one has already content
5174         currentListItem = currentListItem.firstChild ? null : currentListItem;
5175         continue;
5176       }
5177
5178       currentListItem.appendChild(childNode);
5179     }
5180
5181     if (childNodes.length === 0) {
5182       _createListItem(doc, list);
5183     }
5184
5185     element.parentNode.replaceChild(list, element);
5186     return list;
5187   }
5188
5189   return convertToList;
5190 })();
5191 ;/**
5192  * Copy a set of attributes from one element to another
5193  *
5194  * @param {Array} attributesToCopy List of attributes which should be copied
5195  * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
5196  *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
5197  *    with the element where to copy the attributes to (see example)
5198  *
5199  * @example
5200  *    var textarea    = document.querySelector("textarea"),
5201  *        div         = document.querySelector("div[contenteditable=true]"),
5202  *        anotherDiv  = document.querySelector("div.preview");
5203  *    wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
5204  *
5205  */
5206 wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5207   return {
5208     from: function(elementToCopyFrom) {
5209       return {
5210         to: function(elementToCopyTo) {
5211           var attribute,
5212               i         = 0,
5213               length    = attributesToCopy.length;
5214           for (; i<length; i++) {
5215             attribute = attributesToCopy[i];
5216             if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
5217               elementToCopyTo[attribute] = elementToCopyFrom[attribute];
5218             }
5219           }
5220           return { andTo: arguments.callee };
5221         }
5222       };
5223     }
5224   };
5225 };
5226 ;/**
5227  * Copy a set of styles from one element to another
5228  * Please note that this only works properly across browsers when the element from which to copy the styles
5229  * is in the dom
5230  *
5231  * Interesting article on how to copy styles
5232  *
5233  * @param {Array} stylesToCopy List of styles which should be copied
5234  * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
5235  *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked
5236  *    with the element where to copy the styles to (see example)
5237  *
5238  * @example
5239  *    var textarea    = document.querySelector("textarea"),
5240  *        div         = document.querySelector("div[contenteditable=true]"),
5241  *        anotherDiv  = document.querySelector("div.preview");
5242  *    wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
5243  *
5244  */
5245 (function(dom) {
5246
5247   /**
5248    * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
5249    * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
5250    * its computed css width will be 198px
5251    *
5252    * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
5253    */
5254   var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
5255
5256   var shouldIgnoreBoxSizingBorderBox = function(element) {
5257     if (hasBoxSizingBorderBox(element)) {
5258        return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
5259     }
5260     return false;
5261   };
5262
5263   var hasBoxSizingBorderBox = function(element) {
5264     var i       = 0,
5265         length  = BOX_SIZING_PROPERTIES.length;
5266     for (; i<length; i++) {
5267       if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
5268         return BOX_SIZING_PROPERTIES[i];
5269       }
5270     }
5271   };
5272
5273   dom.copyStyles = function(stylesToCopy) {
5274     return {
5275       from: function(element) {
5276         if (shouldIgnoreBoxSizingBorderBox(element)) {
5277           stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
5278         }
5279
5280         var cssText = "",
5281             length  = stylesToCopy.length,
5282             i       = 0,
5283             property;
5284         for (; i<length; i++) {
5285           property = stylesToCopy[i];
5286           cssText += property + ":" + dom.getStyle(property).from(element) + ";";
5287         }
5288
5289         return {
5290           to: function(element) {
5291             dom.setStyles(cssText).on(element);
5292             return { andTo: arguments.callee };
5293           }
5294         };
5295       }
5296     };
5297   };
5298 })(wysihtml5.dom);
5299 ;/**
5300  * Event Delegation
5301  *
5302  * @example
5303  *    wysihtml5.dom.delegate(document.body, "a", "click", function() {
5304  *      // foo
5305  *    });
5306  */
5307 (function(wysihtml5) {
5308
5309   wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
5310     return wysihtml5.dom.observe(container, eventName, function(event) {
5311       var target    = event.target,
5312           match     = wysihtml5.lang.array(container.querySelectorAll(selector));
5313
5314       while (target && target !== container) {
5315         if (match.contains(target)) {
5316           handler.call(target, event);
5317           break;
5318         }
5319         target = target.parentNode;
5320       }
5321     });
5322   };
5323
5324 })(wysihtml5);
5325 ;// TODO: Refactor dom tree traversing here
5326 (function(wysihtml5) {
5327   wysihtml5.dom.domNode = function(node) {
5328     var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
5329
5330     var _isBlankText = function(node) {
5331       return node.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/g).test(node.data);
5332     };
5333
5334     return {
5335
5336       // var node = wysihtml5.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
5337       prev: function(options) {
5338         var prevNode = node.previousSibling,
5339             types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
5340         
5341         if (!prevNode) {
5342           return null;
5343         }
5344
5345         if (
5346           (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
5347           (options && options.ignoreBlankTexts && _isBlankText(prevNode)) // Blank text nodes bypassed if set
5348         ) {
5349           return wysihtml5.dom.domNode(prevNode).prev(options);
5350         }
5351         
5352         return prevNode;
5353       },
5354
5355       // var node = wysihtml5.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true});
5356       next: function(options) {
5357         var nextNode = node.nextSibling,
5358             types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
5359         
5360         if (!nextNode) {
5361           return null;
5362         }
5363
5364         if (
5365           (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
5366           (options && options.ignoreBlankTexts && _isBlankText(nextNode)) // blank text nodes bypassed if set
5367         ) {
5368           return wysihtml5.dom.domNode(nextNode).next(options);
5369         }
5370         
5371         return nextNode;
5372       }
5373
5374
5375
5376     };
5377   };
5378 })(wysihtml5);;/**
5379  * Returns the given html wrapped in a div element
5380  *
5381  * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
5382  * when inserted via innerHTML
5383  *
5384  * @param {String} html The html which should be wrapped in a dom element
5385  * @param {Obejct} [context] Document object of the context the html belongs to
5386  *
5387  * @example
5388  *    wysihtml5.dom.getAsDom("<article>foo</article>");
5389  */
5390 wysihtml5.dom.getAsDom = (function() {
5391
5392   var _innerHTMLShiv = function(html, context) {
5393     var tempElement = context.createElement("div");
5394     tempElement.style.display = "none";
5395     context.body.appendChild(tempElement);
5396     // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
5397     try { tempElement.innerHTML = html; } catch(e) {}
5398     context.body.removeChild(tempElement);
5399     return tempElement;
5400   };
5401
5402   /**
5403    * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
5404    */
5405   var _ensureHTML5Compatibility = function(context) {
5406     if (context._wysihtml5_supportsHTML5Tags) {
5407       return;
5408     }
5409     for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
5410       context.createElement(HTML5_ELEMENTS[i]);
5411     }
5412     context._wysihtml5_supportsHTML5Tags = true;
5413   };
5414
5415
5416   /**
5417    * List of html5 tags
5418    * taken from http://simon.html5.org/html5-elements
5419    */
5420   var HTML5_ELEMENTS = [
5421     "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
5422     "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
5423     "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
5424   ];
5425
5426   return function(html, context) {
5427     context = context || document;
5428     var tempElement;
5429     if (typeof(html) === "object" && html.nodeType) {
5430       tempElement = context.createElement("div");
5431       tempElement.appendChild(html);
5432     } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
5433       tempElement = context.createElement("div");
5434       tempElement.innerHTML = html;
5435     } else {
5436       _ensureHTML5Compatibility(context);
5437       tempElement = _innerHTMLShiv(html, context);
5438     }
5439     return tempElement;
5440   };
5441 })();
5442 ;/**
5443  * Walks the dom tree from the given node up until it finds a match
5444  * Designed for optimal performance.
5445  *
5446  * @param {Element} node The from which to check the parent nodes
5447  * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
5448  * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
5449  * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
5450  * @example
5451  *    var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
5452  *    // ... or ...
5453  *    var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
5454  *    // ... or ...
5455  *    var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
5456  */
5457 wysihtml5.dom.getParentElement = (function() {
5458
5459   function _isSameNodeName(nodeName, desiredNodeNames) {
5460     if (!desiredNodeNames || !desiredNodeNames.length) {
5461       return true;
5462     }
5463
5464     if (typeof(desiredNodeNames) === "string") {
5465       return nodeName === desiredNodeNames;
5466     } else {
5467       return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
5468     }
5469   }
5470
5471   function _isElement(node) {
5472     return node.nodeType === wysihtml5.ELEMENT_NODE;
5473   }
5474
5475   function _hasClassName(element, className, classRegExp) {
5476     var classNames = (element.className || "").match(classRegExp) || [];
5477     if (!className) {
5478       return !!classNames.length;
5479     }
5480     return classNames[classNames.length - 1] === className;
5481   }
5482
5483   function _hasStyle(element, cssStyle, styleRegExp) {
5484     var styles = (element.getAttribute('style') || "").match(styleRegExp) || [];
5485     if (!cssStyle) {
5486       return !!styles.length;
5487     }
5488     return styles[styles.length - 1] === cssStyle;
5489   }
5490
5491   return function(node, matchingSet, levels, container) {
5492     var findByStyle = (matchingSet.cssStyle || matchingSet.styleRegExp),
5493         findByClass = (matchingSet.className || matchingSet.classRegExp);
5494
5495     levels = levels || 50; // Go max 50 nodes upwards from current node
5496
5497     while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
5498       if (_isElement(node) && _isSameNodeName(node.nodeName, matchingSet.nodeName) &&
5499           (!findByStyle || _hasStyle(node, matchingSet.cssStyle, matchingSet.styleRegExp)) &&
5500           (!findByClass || _hasClassName(node, matchingSet.className, matchingSet.classRegExp))
5501       ) {
5502         return node;
5503       }
5504       node = node.parentNode;
5505     }
5506     return null;
5507   };
5508 })();
5509 ;/**
5510  * Get element's style for a specific css property
5511  *
5512  * @param {Element} element The element on which to retrieve the style
5513  * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
5514  *
5515  * @example
5516  *    wysihtml5.dom.getStyle("display").from(document.body);
5517  *    // => "block"
5518  */
5519 wysihtml5.dom.getStyle = (function() {
5520   var stylePropertyMapping = {
5521         "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
5522       },
5523       REG_EXP_CAMELIZE = /\-[a-z]/g;
5524
5525   function camelize(str) {
5526     return str.replace(REG_EXP_CAMELIZE, function(match) {
5527       return match.charAt(1).toUpperCase();
5528     });
5529   }
5530
5531   return function(property) {
5532     return {
5533       from: function(element) {
5534         if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5535           return;
5536         }
5537
5538         var doc               = element.ownerDocument,
5539             camelizedProperty = stylePropertyMapping[property] || camelize(property),
5540             style             = element.style,
5541             currentStyle      = element.currentStyle,
5542             styleValue        = style[camelizedProperty];
5543         if (styleValue) {
5544           return styleValue;
5545         }
5546
5547         // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
5548         // window.getComputedStyle, since it returns css property values in their original unit:
5549         // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
5550         // gives you the original "50%".
5551         // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
5552         if (currentStyle) {
5553           try {
5554             return currentStyle[camelizedProperty];
5555           } catch(e) {
5556             //ie will occasionally fail for unknown reasons. swallowing exception
5557           }
5558         }
5559
5560         var win                 = doc.defaultView || doc.parentWindow,
5561             needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
5562             originalOverflow,
5563             returnValue;
5564
5565         if (win.getComputedStyle) {
5566           // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
5567           // therfore we remove and restore the scrollbar and calculate the value in between
5568           if (needsOverflowReset) {
5569             originalOverflow = style.overflow;
5570             style.overflow = "hidden";
5571           }
5572           returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
5573           if (needsOverflowReset) {
5574             style.overflow = originalOverflow || "";
5575           }
5576           return returnValue;
5577         }
5578       }
5579     };
5580   };
5581 })();
5582 ;wysihtml5.dom.getTextNodes = function(node, ingoreEmpty){
5583   var all = [];
5584   for (node=node.firstChild;node;node=node.nextSibling){
5585     if (node.nodeType == 3) {
5586       if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
5587         all.push(node);
5588       }
5589     } else {
5590       all = all.concat(wysihtml5.dom.getTextNodes(node, ingoreEmpty));
5591     }
5592   }
5593   return all;
5594 };;/**
5595  * High performant way to check whether an element with a specific tag name is in the given document
5596  * Optimized for being heavily executed
5597  * Unleashes the power of live node lists
5598  *
5599  * @param {Object} doc The document object of the context where to check
5600  * @param {String} tagName Upper cased tag name
5601  * @example
5602  *    wysihtml5.dom.hasElementWithTagName(document, "IMG");
5603  */
5604 wysihtml5.dom.hasElementWithTagName = (function() {
5605   var LIVE_CACHE          = {},
5606       DOCUMENT_IDENTIFIER = 1;
5607
5608   function _getDocumentIdentifier(doc) {
5609     return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5610   }
5611
5612   return function(doc, tagName) {
5613     var key         = _getDocumentIdentifier(doc) + ":" + tagName,
5614         cacheEntry  = LIVE_CACHE[key];
5615     if (!cacheEntry) {
5616       cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
5617     }
5618
5619     return cacheEntry.length > 0;
5620   };
5621 })();
5622 ;/**
5623  * High performant way to check whether an element with a specific class name is in the given document
5624  * Optimized for being heavily executed
5625  * Unleashes the power of live node lists
5626  *
5627  * @param {Object} doc The document object of the context where to check
5628  * @param {String} tagName Upper cased tag name
5629  * @example
5630  *    wysihtml5.dom.hasElementWithClassName(document, "foobar");
5631  */
5632 (function(wysihtml5) {
5633   var LIVE_CACHE          = {},
5634       DOCUMENT_IDENTIFIER = 1;
5635
5636   function _getDocumentIdentifier(doc) {
5637     return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5638   }
5639
5640   wysihtml5.dom.hasElementWithClassName = function(doc, className) {
5641     // getElementsByClassName is not supported by IE<9
5642     // but is sometimes mocked via library code (which then doesn't return live node lists)
5643     if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
5644       return !!doc.querySelector("." + className);
5645     }
5646
5647     var key         = _getDocumentIdentifier(doc) + ":" + className,
5648         cacheEntry  = LIVE_CACHE[key];
5649     if (!cacheEntry) {
5650       cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
5651     }
5652
5653     return cacheEntry.length > 0;
5654   };
5655 })(wysihtml5);
5656 ;wysihtml5.dom.insert = function(elementToInsert) {
5657   return {
5658     after: function(element) {
5659       element.parentNode.insertBefore(elementToInsert, element.nextSibling);
5660     },
5661
5662     before: function(element) {
5663       element.parentNode.insertBefore(elementToInsert, element);
5664     },
5665
5666     into: function(element) {
5667       element.appendChild(elementToInsert);
5668     }
5669   };
5670 };
5671 ;wysihtml5.dom.insertCSS = function(rules) {
5672   rules = rules.join("\n");
5673
5674   return {
5675     into: function(doc) {
5676       var styleElement = doc.createElement("style");
5677       styleElement.type = "text/css";
5678
5679       if (styleElement.styleSheet) {
5680         styleElement.styleSheet.cssText = rules;
5681       } else {
5682         styleElement.appendChild(doc.createTextNode(rules));
5683       }
5684
5685       var link = doc.querySelector("head link");
5686       if (link) {
5687         link.parentNode.insertBefore(styleElement, link);
5688         return;
5689       } else {
5690         var head = doc.querySelector("head");
5691         if (head) {
5692           head.appendChild(styleElement);
5693         }
5694       }
5695     }
5696   };
5697 };
5698 ;// TODO: Refactor dom tree traversing here
5699 (function(wysihtml5) {
5700   wysihtml5.dom.lineBreaks = function(node) {
5701
5702     function _isLineBreak(n) {
5703       return n.nodeName === "BR";
5704     }
5705
5706     /**
5707      * Checks whether the elment causes a visual line break
5708      * (<br> or block elements)
5709      */
5710     function _isLineBreakOrBlockElement(element) {
5711       if (_isLineBreak(element)) {
5712         return true;
5713       }
5714
5715       if (wysihtml5.dom.getStyle("display").from(element) === "block") {
5716         return true;
5717       }
5718
5719       return false;
5720     }
5721
5722     return {
5723
5724       /* wysihtml5.dom.lineBreaks(element).add();
5725        *
5726        * Adds line breaks before and after the given node if the previous and next siblings
5727        * aren't already causing a visual line break (block element or <br>)
5728        */
5729       add: function(options) {
5730         var doc             = node.ownerDocument,
5731           nextSibling     = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}),
5732           previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true});
5733
5734         if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
5735           wysihtml5.dom.insert(doc.createElement("br")).after(node);
5736         }
5737         if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
5738           wysihtml5.dom.insert(doc.createElement("br")).before(node);
5739         }
5740       },
5741
5742       /* wysihtml5.dom.lineBreaks(element).remove();
5743        *
5744        * Removes line breaks before and after the given node
5745        */
5746       remove: function(options) {
5747         var nextSibling     = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}),
5748             previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true});
5749
5750         if (nextSibling && _isLineBreak(nextSibling)) {
5751           nextSibling.parentNode.removeChild(nextSibling);
5752         }
5753         if (previousSibling && _isLineBreak(previousSibling)) {
5754           previousSibling.parentNode.removeChild(previousSibling);
5755         }
5756       }
5757     };
5758   };
5759 })(wysihtml5);;/**
5760  * Method to set dom events
5761  *
5762  * @example
5763  *    wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
5764  */
5765 wysihtml5.dom.observe = function(element, eventNames, handler) {
5766   eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
5767
5768   var handlerWrapper,
5769       eventName,
5770       i       = 0,
5771       length  = eventNames.length;
5772
5773   for (; i<length; i++) {
5774     eventName = eventNames[i];
5775     if (element.addEventListener) {
5776       element.addEventListener(eventName, handler, false);
5777     } else {
5778       handlerWrapper = function(event) {
5779         if (!("target" in event)) {
5780           event.target = event.srcElement;
5781         }
5782         event.preventDefault = event.preventDefault || function() {
5783           this.returnValue = false;
5784         };
5785         event.stopPropagation = event.stopPropagation || function() {
5786           this.cancelBubble = true;
5787         };
5788         handler.call(element, event);
5789       };
5790       element.attachEvent("on" + eventName, handlerWrapper);
5791     }
5792   }
5793
5794   return {
5795     stop: function() {
5796       var eventName,
5797           i       = 0,
5798           length  = eventNames.length;
5799       for (; i<length; i++) {
5800         eventName = eventNames[i];
5801         if (element.removeEventListener) {
5802           element.removeEventListener(eventName, handler, false);
5803         } else {
5804           element.detachEvent("on" + eventName, handlerWrapper);
5805         }
5806       }
5807     }
5808   };
5809 };
5810 ;/**
5811  * HTML Sanitizer
5812  * Rewrites the HTML based on given rules
5813  *
5814  * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
5815  * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
5816  *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
5817  *    desired substitution.
5818  * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
5819  *
5820  * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
5821  *
5822  * @example
5823  *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
5824  *    wysihtml5.dom.parse(userHTML, {
5825  *      tags {
5826  *        p:      "div",      // Rename p tags to div tags
5827  *        font:   "span"      // Rename font tags to span tags
5828  *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
5829  *        script: undefined   // Remove script elements
5830  *      }
5831  *    });
5832  *    // => <div><div><span>foo bar</span></div></div>
5833  *
5834  *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
5835  *    wysihtml5.dom.parse(userHTML);
5836  *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
5837  *
5838  *    var userHTML = '<div>foobar<br>foobar</div>';
5839  *    wysihtml5.dom.parse(userHTML, {
5840  *      tags: {
5841  *        div: undefined,
5842  *        br:  true
5843  *      }
5844  *    });
5845  *    // => ''
5846  *
5847  *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
5848  *    wysihtml5.dom.parse(userHTML, {
5849  *      classes: {
5850  *        red:    1,
5851  *        green:  1
5852  *      },
5853  *      tags: {
5854  *        div: {
5855  *          rename_tag:     "p"
5856  *        }
5857  *      }
5858  *    });
5859  *    // => '<p class="red">foo</p><p>bar</p>'
5860  */
5861
5862 wysihtml5.dom.parse = function(elementOrHtml_current, config_current) {
5863   /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
5864    * Refactor whole code as this method while workind is kind of awkward too */
5865
5866   /**
5867    * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
5868    * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
5869    * node isn't closed
5870    *
5871    * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
5872    */
5873   var NODE_TYPE_MAPPING = {
5874         "1": _handleElement,
5875         "3": _handleText,
5876         "8": _handleComment
5877       },
5878       // Rename unknown tags to this
5879       DEFAULT_NODE_NAME   = "span",
5880       WHITE_SPACE_REG_EXP = /\s+/,
5881       defaultRules        = { tags: {}, classes: {} },
5882       currentRules        = {};
5883
5884   /**
5885    * Iterates over all childs of the element, recreates them, appends them into a document fragment
5886    * which later replaces the entire body content
5887    */
5888    function parse(elementOrHtml, config) {
5889     wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
5890
5891     var context       = config.context || elementOrHtml.ownerDocument || document,
5892         fragment      = context.createDocumentFragment(),
5893         isString      = typeof(elementOrHtml) === "string",
5894         clearInternals = false,
5895         element,
5896         newNode,
5897         firstChild;
5898
5899     if (config.clearInternals === true) {
5900       clearInternals = true;
5901     }
5902
5903     if (isString) {
5904       element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5905     } else {
5906       element = elementOrHtml;
5907     }
5908
5909     if (currentRules.selectors) {
5910       _applySelectorRules(element, currentRules.selectors);
5911     }
5912
5913     while (element.firstChild) {
5914       firstChild = element.firstChild;
5915       newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
5916       if (newNode) {
5917         fragment.appendChild(newNode);
5918       }
5919       if (firstChild !== newNode) {
5920         element.removeChild(firstChild);
5921       }
5922     }
5923
5924     if (config.unjoinNbsps) {
5925       // replace joined non-breakable spaces with unjoined
5926       var txtnodes = wysihtml5.dom.getTextNodes(fragment);
5927       for (var n = txtnodes.length; n--;) {
5928         txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
5929       }
5930     }
5931
5932     // Clear element contents
5933     element.innerHTML = "";
5934
5935     // Insert new DOM tree
5936     element.appendChild(fragment);
5937
5938     return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
5939   }
5940
5941   function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
5942     var oldNodeType     = oldNode.nodeType,
5943         oldChilds       = oldNode.childNodes,
5944         oldChildsLength = oldChilds.length,
5945         method          = NODE_TYPE_MAPPING[oldNodeType],
5946         i               = 0,
5947         fragment,
5948         newNode,
5949         newChild;
5950
5951     // Passes directly elemets with uneditable class
5952     if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) {
5953         return oldNode;
5954     }
5955
5956     newNode = method && method(oldNode, clearInternals);
5957
5958     // Remove or unwrap node in case of return value null or false
5959     if (!newNode) {
5960         if (newNode === false) {
5961             // false defines that tag should be removed but contents should remain (unwrap)
5962             fragment = oldNode.ownerDocument.createDocumentFragment();
5963
5964             for (i = oldChildsLength; i--;) {
5965               if (oldChilds[i]) {
5966                 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
5967                 if (newChild) {
5968                   if (oldChilds[i] === newChild) {
5969                     i--;
5970                   }
5971                   fragment.insertBefore(newChild, fragment.firstChild);
5972                 }
5973               }
5974             }
5975
5976             if (wysihtml5.dom.getStyle("display").from(oldNode) === "block") {
5977               fragment.appendChild(oldNode.ownerDocument.createElement("br"));
5978             }
5979
5980             // TODO: try to minimize surplus spaces
5981             if (wysihtml5.lang.array([
5982                 "div", "pre", "p",
5983                 "table", "td", "th",
5984                 "ul", "ol", "li",
5985                 "dd", "dl",
5986                 "footer", "header", "section",
5987                 "h1", "h2", "h3", "h4", "h5", "h6"
5988             ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
5989                 // add space at first when unwraping non-textflow elements
5990                 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
5991                   fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
5992                 }
5993             }
5994
5995             if (fragment.normalize) {
5996               fragment.normalize();
5997             }
5998             return fragment;
5999         } else {
6000           // Remove
6001           return null;
6002         }
6003     }
6004
6005     // Converts all childnodes
6006     for (i=0; i<oldChildsLength; i++) {
6007       if (oldChilds[i]) {
6008         newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
6009         if (newChild) {
6010           if (oldChilds[i] === newChild) {
6011             i--;
6012           }
6013           newNode.appendChild(newChild);
6014         }
6015       }
6016     }
6017
6018     // Cleanup senseless <span> elements
6019     if (cleanUp &&
6020         newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
6021         (!newNode.childNodes.length ||
6022          ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml5-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
6023          !newNode.attributes.length)
6024         ) {
6025       fragment = newNode.ownerDocument.createDocumentFragment();
6026       while (newNode.firstChild) {
6027         fragment.appendChild(newNode.firstChild);
6028       }
6029       if (fragment.normalize) {
6030         fragment.normalize();
6031       }
6032       return fragment;
6033     }
6034
6035     if (newNode.normalize) {
6036       newNode.normalize();
6037     }
6038     return newNode;
6039   }
6040
6041   function _applySelectorRules (element, selectorRules) {
6042     var sel, method, els;
6043
6044     for (sel in selectorRules) {
6045       if (selectorRules.hasOwnProperty(sel)) {
6046         if (wysihtml5.lang.object(selectorRules[sel]).isFunction()) {
6047           method = selectorRules[sel];
6048         } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
6049           method = elementHandlingMethods[selectorRules[sel]];
6050         }
6051         els = element.querySelectorAll(sel);
6052         for (var i = els.length; i--;) {
6053           method(els[i]);
6054         }
6055       }
6056     }
6057   }
6058
6059   function _handleElement(oldNode, clearInternals) {
6060     var rule,
6061         newNode,
6062         tagRules    = currentRules.tags,
6063         nodeName    = oldNode.nodeName.toLowerCase(),
6064         scopeName   = oldNode.scopeName,
6065         renameTag;
6066
6067     /**
6068      * We already parsed that element
6069      * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
6070      */
6071     if (oldNode._wysihtml5) {
6072       return null;
6073     }
6074     oldNode._wysihtml5 = 1;
6075
6076     if (oldNode.className === "wysihtml5-temp") {
6077       return null;
6078     }
6079
6080     /**
6081      * IE is the only browser who doesn't include the namespace in the
6082      * nodeName, that's why we have to prepend it by ourselves
6083      * scopeName is a proprietary IE feature
6084      * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
6085      */
6086     if (scopeName && scopeName != "HTML") {
6087       nodeName = scopeName + ":" + nodeName;
6088     }
6089     /**
6090      * Repair node
6091      * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
6092      * 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
6093      */
6094     if ("outerHTML" in oldNode) {
6095       if (!wysihtml5.browser.autoClosesUnclosedTags() &&
6096           oldNode.nodeName === "P" &&
6097           oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
6098         nodeName = "div";
6099       }
6100     }
6101
6102     if (nodeName in tagRules) {
6103       rule = tagRules[nodeName];
6104       if (!rule || rule.remove) {
6105         return null;
6106       } else if (rule.unwrap) {
6107         return false;
6108       }
6109       rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
6110     } else if (oldNode.firstChild) {
6111       rule = { rename_tag: DEFAULT_NODE_NAME };
6112     } else {
6113       // Remove empty unknown elements
6114       return null;
6115     }
6116
6117     // tests if type condition is met or node should be removed/unwrapped/renamed
6118     if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
6119       if (rule.remove_action) {
6120         if (rule.remove_action === "unwrap") {
6121           return false;
6122         } else if (rule.remove_action === "rename") {
6123           renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
6124         } else {
6125           return null;
6126         }
6127       } else {
6128         return null;
6129       }
6130     }
6131
6132     newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
6133     _handleAttributes(oldNode, newNode, rule, clearInternals);
6134     _handleStyles(oldNode, newNode, rule);
6135
6136     oldNode = null;
6137
6138     if (newNode.normalize) { newNode.normalize(); }
6139     return newNode;
6140   }
6141
6142   function _testTypes(oldNode, rules, types, clearInternals) {
6143     var definition, type;
6144
6145     // do not interfere with placeholder span or pasting caret position is not maintained
6146     if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml5-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
6147       return true;
6148     }
6149
6150     for (type in types) {
6151       if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
6152         definition = rules.type_definitions[type];
6153         if (_testType(oldNode, definition)) {
6154           return true;
6155         }
6156       }
6157     }
6158     return false;
6159   }
6160
6161   function array_contains(a, obj) {
6162       var i = a.length;
6163       while (i--) {
6164          if (a[i] === obj) {
6165              return true;
6166          }
6167       }
6168       return false;
6169   }
6170
6171   function _testType(oldNode, definition) {
6172
6173     var nodeClasses = oldNode.getAttribute("class"),
6174         nodeStyles =  oldNode.getAttribute("style"),
6175         classesLength, s, s_corrected, a, attr, currentClass, styleProp;
6176
6177     // test for methods
6178     if (definition.methods) {
6179       for (var m in definition.methods) {
6180         if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
6181
6182           if (typeCeckMethods[m](oldNode)) {
6183             return true;
6184           }
6185         }
6186       }
6187     }
6188
6189     // test for classes, if one found return true
6190     if (nodeClasses && definition.classes) {
6191       nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
6192       classesLength = nodeClasses.length;
6193       for (var i = 0; i < classesLength; i++) {
6194         if (definition.classes[nodeClasses[i]]) {
6195           return true;
6196         }
6197       }
6198     }
6199
6200     // test for styles, if one found return true
6201     if (nodeStyles && definition.styles) {
6202
6203       nodeStyles = nodeStyles.split(';');
6204       for (s in definition.styles) {
6205         if (definition.styles.hasOwnProperty(s)) {
6206           for (var sp = nodeStyles.length; sp--;) {
6207             styleProp = nodeStyles[sp].split(':');
6208
6209             if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
6210               if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml5.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
6211                 return true;
6212               }
6213             }
6214           }
6215         }
6216       }
6217     }
6218
6219     // test for attributes in general against regex match
6220     if (definition.attrs) {
6221         for (a in definition.attrs) {
6222             if (definition.attrs.hasOwnProperty(a)) {
6223                 attr = wysihtml5.dom.getAttribute(oldNode, a);
6224                 if (typeof(attr) === "string") {
6225                     if (attr.search(definition.attrs[a]) > -1) {
6226                         return true;
6227                     }
6228                 }
6229             }
6230         }
6231     }
6232     return false;
6233   }
6234
6235   function _handleStyles(oldNode, newNode, rule) {
6236     var s, v;
6237     if(rule && rule.keep_styles) {
6238       for (s in rule.keep_styles) {
6239         if (rule.keep_styles.hasOwnProperty(s)) {
6240           v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
6241           // value can be regex and if so should match or style skipped
6242           if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
6243             continue;
6244           }
6245           if (s === "float") {
6246             // IE compability
6247             newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
6248            } else if (oldNode.style[s]) {
6249              newNode.style[s] = v;
6250            }
6251         }
6252       }
6253     }
6254   };
6255
6256   function _getAttributesBeginningWith(beginning, attributes) {
6257     var returnAttributes = [];
6258     for (var attr in attributes) {
6259       if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
6260         returnAttributes.push(attr);
6261       }
6262     }
6263     return returnAttributes;
6264   }
6265
6266   function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
6267     var method = attributeCheckMethods[methodName],
6268         newAttributeValue;
6269
6270     if (method) {
6271       if (attributeValue || (attributeName === "alt" && nodeName == "IMG")) {
6272         newAttributeValue = method(attributeValue);
6273         if (typeof(newAttributeValue) === "string") {
6274           return newAttributeValue;
6275         }
6276       }
6277     }
6278
6279     return false;
6280   }
6281
6282   function _checkAttributes(oldNode, local_attributes) {
6283     var globalAttributes  = wysihtml5.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
6284         checkAttributes   = wysihtml5.lang.object(globalAttributes).merge( wysihtml5.lang.object(local_attributes || {}).clone()).get(),
6285         attributes        = {},
6286         oldAttributes     = wysihtml5.dom.getAttributes(oldNode),
6287         attributeName, newValue, matchingAttributes;
6288
6289     for (attributeName in checkAttributes) {
6290       if ((/\*$/).test(attributeName)) {
6291
6292         matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
6293         for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
6294
6295           newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
6296           if (newValue !== false) {
6297             attributes[matchingAttributes[i]] = newValue;
6298           }
6299         }
6300       } else {
6301         newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
6302         if (newValue !== false) {
6303           attributes[attributeName] = newValue;
6304         }
6305       }
6306     }
6307
6308     return attributes;
6309   }
6310
6311   // TODO: refactor. Too long to read
6312   function _handleAttributes(oldNode, newNode, rule, clearInternals) {
6313     var attributes          = {},                         // fresh new set of attributes to set on newNode
6314         setClass            = rule.set_class,             // classes to set
6315         addClass            = rule.add_class,             // add classes based on existing attributes
6316         addStyle            = rule.add_style,             // add styles based on existing attributes
6317         setAttributes       = rule.set_attributes,        // attributes to set on the current node
6318         allowedClasses      = currentRules.classes,
6319         i                   = 0,
6320         classes             = [],
6321         styles              = [],
6322         newClasses          = [],
6323         oldClasses          = [],
6324         classesLength,
6325         newClassesLength,
6326         currentClass,
6327         newClass,
6328         attributeName,
6329         method;
6330
6331     if (setAttributes) {
6332       attributes = wysihtml5.lang.object(setAttributes).clone();
6333     }
6334
6335     // check/convert values of attributes
6336     attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode,  rule.check_attributes)).get();
6337
6338     if (setClass) {
6339       classes.push(setClass);
6340     }
6341
6342     if (addClass) {
6343       for (attributeName in addClass) {
6344         method = addClassMethods[addClass[attributeName]];
6345         if (!method) {
6346           continue;
6347         }
6348         newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6349         if (typeof(newClass) === "string") {
6350           classes.push(newClass);
6351         }
6352       }
6353     }
6354
6355     if (addStyle) {
6356       for (attributeName in addStyle) {
6357         method = addStyleMethods[addStyle[attributeName]];
6358         if (!method) {
6359           continue;
6360         }
6361
6362         newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6363         if (typeof(newStyle) === "string") {
6364           styles.push(newStyle);
6365         }
6366       }
6367     }
6368
6369
6370     if (typeof(allowedClasses) === "string" && allowedClasses === "any" && oldNode.getAttribute("class")) {
6371       if (currentRules.classes_blacklist) {
6372         oldClasses = oldNode.getAttribute("class");
6373         if (oldClasses) {
6374           classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6375         }
6376
6377         classesLength = classes.length;
6378         for (; i<classesLength; i++) {
6379           currentClass = classes[i];
6380           if (!currentRules.classes_blacklist[currentClass]) {
6381             newClasses.push(currentClass);
6382           }
6383         }
6384
6385         if (newClasses.length) {
6386           attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6387         }
6388
6389       } else {
6390         attributes["class"] = oldNode.getAttribute("class");
6391       }
6392     } else {
6393       // make sure that wysihtml5 temp class doesn't get stripped out
6394       if (!clearInternals) {
6395         allowedClasses["_wysihtml5-temp-placeholder"] = 1;
6396         allowedClasses["_rangySelectionBoundary"] = 1;
6397         allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
6398       }
6399
6400       // add old classes last
6401       oldClasses = oldNode.getAttribute("class");
6402       if (oldClasses) {
6403         classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6404       }
6405       classesLength = classes.length;
6406       for (; i<classesLength; i++) {
6407         currentClass = classes[i];
6408         if (allowedClasses[currentClass]) {
6409           newClasses.push(currentClass);
6410         }
6411       }
6412
6413       if (newClasses.length) {
6414         attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6415       }
6416     }
6417
6418     // remove table selection class if present
6419     if (attributes["class"] && clearInternals) {
6420       attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", "");
6421       if ((/^\s*$/g).test(attributes["class"])) {
6422         delete attributes["class"];
6423       }
6424     }
6425
6426     if (styles.length) {
6427       attributes["style"] = wysihtml5.lang.array(styles).unique().join(" ");
6428     }
6429
6430     // set attributes on newNode
6431     for (attributeName in attributes) {
6432       // Setting attributes can cause a js error in IE under certain circumstances
6433       // eg. on a <img> under https when it's new attribute value is non-https
6434       // TODO: Investigate this further and check for smarter handling
6435       try {
6436         newNode.setAttribute(attributeName, attributes[attributeName]);
6437       } catch(e) {}
6438     }
6439
6440     // IE8 sometimes loses the width/height attributes when those are set before the "src"
6441     // so we make sure to set them again
6442     if (attributes.src) {
6443       if (typeof(attributes.width) !== "undefined") {
6444         newNode.setAttribute("width", attributes.width);
6445       }
6446       if (typeof(attributes.height) !== "undefined") {
6447         newNode.setAttribute("height", attributes.height);
6448       }
6449     }
6450   }
6451
6452   var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
6453   function _handleText(oldNode) {
6454     var nextSibling = oldNode.nextSibling;
6455     if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) {
6456       // Concatenate text nodes
6457       nextSibling.data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(INVISIBLE_SPACE_REG_EXP, "");
6458     } else {
6459       // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
6460       var data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, "");
6461       return oldNode.ownerDocument.createTextNode(data);
6462     }
6463   }
6464
6465   function _handleComment(oldNode) {
6466     if (currentRules.comments) {
6467       return oldNode.ownerDocument.createComment(oldNode.nodeValue);
6468     }
6469   }
6470
6471   // ------------ attribute checks ------------ \\
6472   var attributeCheckMethods = {
6473     url: (function() {
6474       var REG_EXP = /^https?:\/\//i;
6475       return function(attributeValue) {
6476         if (!attributeValue || !attributeValue.match(REG_EXP)) {
6477           return null;
6478         }
6479         return attributeValue.replace(REG_EXP, function(match) {
6480           return match.toLowerCase();
6481         });
6482       };
6483     })(),
6484
6485     src: (function() {
6486       var REG_EXP = /^(\/|https?:\/\/)/i;
6487       return function(attributeValue) {
6488         if (!attributeValue || !attributeValue.match(REG_EXP)) {
6489           return null;
6490         }
6491         return attributeValue.replace(REG_EXP, function(match) {
6492           return match.toLowerCase();
6493         });
6494       };
6495     })(),
6496
6497     href: (function() {
6498       var REG_EXP = /^(#|\/|https?:\/\/|mailto:)/i;
6499       return function(attributeValue) {
6500         if (!attributeValue || !attributeValue.match(REG_EXP)) {
6501           return null;
6502         }
6503         return attributeValue.replace(REG_EXP, function(match) {
6504           return match.toLowerCase();
6505         });
6506       };
6507     })(),
6508
6509     alt: (function() {
6510       var REG_EXP = /[^ a-z0-9_\-]/gi;
6511       return function(attributeValue) {
6512         if (!attributeValue) {
6513           return "";
6514         }
6515         return attributeValue.replace(REG_EXP, "");
6516       };
6517     })(),
6518
6519     numbers: (function() {
6520       var REG_EXP = /\D/g;
6521       return function(attributeValue) {
6522         attributeValue = (attributeValue || "").replace(REG_EXP, "");
6523         return attributeValue || null;
6524       };
6525     })(),
6526
6527     any: (function() {
6528       return function(attributeValue) {
6529         return attributeValue;
6530       };
6531     })()
6532   };
6533
6534   // ------------ style converter (converts an html attribute to a style) ------------ \\
6535   var addStyleMethods = {
6536     align_text: (function() {
6537       var mapping = {
6538         left:     "text-align: left;",
6539         right:    "text-align: right;",
6540         center:   "text-align: center;"
6541       };
6542       return function(attributeValue) {
6543         return mapping[String(attributeValue).toLowerCase()];
6544       };
6545     })(),
6546   };
6547
6548   // ------------ class converter (converts an html attribute to a class name) ------------ \\
6549   var addClassMethods = {
6550     align_img: (function() {
6551       var mapping = {
6552         left:   "wysiwyg-float-left",
6553         right:  "wysiwyg-float-right"
6554       };
6555       return function(attributeValue) {
6556         return mapping[String(attributeValue).toLowerCase()];
6557       };
6558     })(),
6559
6560     align_text: (function() {
6561       var mapping = {
6562         left:     "wysiwyg-text-align-left",
6563         right:    "wysiwyg-text-align-right",
6564         center:   "wysiwyg-text-align-center",
6565         justify:  "wysiwyg-text-align-justify"
6566       };
6567       return function(attributeValue) {
6568         return mapping[String(attributeValue).toLowerCase()];
6569       };
6570     })(),
6571
6572     clear_br: (function() {
6573       var mapping = {
6574         left:   "wysiwyg-clear-left",
6575         right:  "wysiwyg-clear-right",
6576         both:   "wysiwyg-clear-both",
6577         all:    "wysiwyg-clear-both"
6578       };
6579       return function(attributeValue) {
6580         return mapping[String(attributeValue).toLowerCase()];
6581       };
6582     })(),
6583
6584     size_font: (function() {
6585       var mapping = {
6586         "1": "wysiwyg-font-size-xx-small",
6587         "2": "wysiwyg-font-size-small",
6588         "3": "wysiwyg-font-size-medium",
6589         "4": "wysiwyg-font-size-large",
6590         "5": "wysiwyg-font-size-x-large",
6591         "6": "wysiwyg-font-size-xx-large",
6592         "7": "wysiwyg-font-size-xx-large",
6593         "-": "wysiwyg-font-size-smaller",
6594         "+": "wysiwyg-font-size-larger"
6595       };
6596       return function(attributeValue) {
6597         return mapping[String(attributeValue).charAt(0)];
6598       };
6599     })()
6600   };
6601
6602   // checks if element is possibly visible
6603   var typeCeckMethods = {
6604     has_visible_contet: (function() {
6605       var txt,
6606           isVisible = false,
6607           visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
6608                              'style', 'table', 'iframe', 'object', 'embed', 'audio',
6609                              'svg', 'input', 'button', 'select','textarea', 'canvas'];
6610
6611       return function(el) {
6612
6613         // has visible innertext. so is visible
6614         txt = (el.innerText || el.textContent).replace(/\s/g, '');
6615         if (txt && txt.length > 0) {
6616           return true;
6617         }
6618
6619         // matches list of visible dimensioned elements
6620         for (var i = visibleElements.length; i--;) {
6621           if (el.querySelector(visibleElements[i])) {
6622             return true;
6623           }
6624         }
6625
6626         // try to measure dimesions in last resort. (can find only of elements in dom)
6627         if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
6628           return true;
6629         }
6630
6631         return false;
6632       };
6633     })()
6634   };
6635
6636   var elementHandlingMethods = {
6637     unwrap: function (element) {
6638       wysihtml5.dom.unwrap(element);
6639     },
6640
6641     remove: function (element) {
6642       element.parentNode.removeChild(element);
6643     }
6644   };
6645
6646   return parse(elementOrHtml_current, config_current);
6647 };
6648 ;/**
6649  * Checks for empty text node childs and removes them
6650  *
6651  * @param {Element} node The element in which to cleanup
6652  * @example
6653  *    wysihtml5.dom.removeEmptyTextNodes(element);
6654  */
6655 wysihtml5.dom.removeEmptyTextNodes = function(node) {
6656   var childNode,
6657       childNodes        = wysihtml5.lang.array(node.childNodes).get(),
6658       childNodesLength  = childNodes.length,
6659       i                 = 0;
6660   for (; i<childNodesLength; i++) {
6661     childNode = childNodes[i];
6662     if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
6663       childNode.parentNode.removeChild(childNode);
6664     }
6665   }
6666 };
6667 ;/**
6668  * Renames an element (eg. a <div> to a <p>) and keeps its childs
6669  *
6670  * @param {Element} element The list element which should be renamed
6671  * @param {Element} newNodeName The desired tag name
6672  *
6673  * @example
6674  *    <!-- Assume the following dom: -->
6675  *    <ul id="list">
6676  *      <li>eminem</li>
6677  *      <li>dr. dre</li>
6678  *      <li>50 Cent</li>
6679  *    </ul>
6680  *
6681  *    <script>
6682  *      wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
6683  *    </script>
6684  *
6685  *    <!-- Will result in: -->
6686  *    <ol>
6687  *      <li>eminem</li>
6688  *      <li>dr. dre</li>
6689  *      <li>50 Cent</li>
6690  *    </ol>
6691  */
6692 wysihtml5.dom.renameElement = function(element, newNodeName) {
6693   var newElement = element.ownerDocument.createElement(newNodeName),
6694       firstChild;
6695   while (firstChild = element.firstChild) {
6696     newElement.appendChild(firstChild);
6697   }
6698   wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
6699   element.parentNode.replaceChild(newElement, element);
6700   return newElement;
6701 };
6702 ;/**
6703  * Takes an element, removes it and replaces it with it's childs
6704  *
6705  * @param {Object} node The node which to replace with it's child nodes
6706  * @example
6707  *    <div id="foo">
6708  *      <span>hello</span>
6709  *    </div>
6710  *    <script>
6711  *      // Remove #foo and replace with it's children
6712  *      wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
6713  *    </script>
6714  */
6715 wysihtml5.dom.replaceWithChildNodes = function(node) {
6716   if (!node.parentNode) {
6717     return;
6718   }
6719
6720   if (!node.firstChild) {
6721     node.parentNode.removeChild(node);
6722     return;
6723   }
6724
6725   var fragment = node.ownerDocument.createDocumentFragment();
6726   while (node.firstChild) {
6727     fragment.appendChild(node.firstChild);
6728   }
6729   node.parentNode.replaceChild(fragment, node);
6730   node = fragment = null;
6731 };
6732 ;/**
6733  * Unwraps an unordered/ordered list
6734  *
6735  * @param {Element} element The list element which should be unwrapped
6736  *
6737  * @example
6738  *    <!-- Assume the following dom: -->
6739  *    <ul id="list">
6740  *      <li>eminem</li>
6741  *      <li>dr. dre</li>
6742  *      <li>50 Cent</li>
6743  *    </ul>
6744  *
6745  *    <script>
6746  *      wysihtml5.dom.resolveList(document.getElementById("list"));
6747  *    </script>
6748  *
6749  *    <!-- Will result in: -->
6750  *    eminem<br>
6751  *    dr. dre<br>
6752  *    50 Cent<br>
6753  */
6754 (function(dom) {
6755   function _isBlockElement(node) {
6756     return dom.getStyle("display").from(node) === "block";
6757   }
6758
6759   function _isLineBreak(node) {
6760     return node.nodeName === "BR";
6761   }
6762
6763   function _appendLineBreak(element) {
6764     var lineBreak = element.ownerDocument.createElement("br");
6765     element.appendChild(lineBreak);
6766   }
6767
6768   function resolveList(list, useLineBreaks) {
6769     if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
6770       return;
6771     }
6772
6773     var doc             = list.ownerDocument,
6774         fragment        = doc.createDocumentFragment(),
6775         previousSibling = wysihtml5.dom.domNode(list).prev({ignoreBlankTexts: true}),
6776         firstChild,
6777         lastChild,
6778         isLastChild,
6779         shouldAppendLineBreak,
6780         paragraph,
6781         listItem;
6782
6783     if (useLineBreaks) {
6784       // Insert line break if list is after a non-block element
6785       if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
6786         _appendLineBreak(fragment);
6787       }
6788
6789       while (listItem = (list.firstElementChild || list.firstChild)) {
6790         lastChild = listItem.lastChild;
6791         while (firstChild = listItem.firstChild) {
6792           isLastChild           = firstChild === lastChild;
6793           // This needs to be done before appending it to the fragment, as it otherwise will lose style information
6794           shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
6795           fragment.appendChild(firstChild);
6796           if (shouldAppendLineBreak) {
6797             _appendLineBreak(fragment);
6798           }
6799         }
6800
6801         listItem.parentNode.removeChild(listItem);
6802       }
6803     } else {
6804       while (listItem = (list.firstElementChild || list.firstChild)) {
6805         if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
6806           while (firstChild = listItem.firstChild) {
6807             fragment.appendChild(firstChild);
6808           }
6809         } else {
6810           paragraph = doc.createElement("p");
6811           while (firstChild = listItem.firstChild) {
6812             paragraph.appendChild(firstChild);
6813           }
6814           fragment.appendChild(paragraph);
6815         }
6816         listItem.parentNode.removeChild(listItem);
6817       }
6818     }
6819
6820     list.parentNode.replaceChild(fragment, list);
6821   }
6822
6823   dom.resolveList = resolveList;
6824 })(wysihtml5.dom);
6825 ;/**
6826  * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
6827  *
6828  * Browser Compatibility:
6829  *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
6830  *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
6831  *
6832  * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
6833  *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
6834  *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
6835  *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
6836  *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
6837  *      can do anything as if the sandbox attribute wasn't set
6838  *
6839  * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
6840  * @param {Object} [config] Optional parameters
6841  *
6842  * @example
6843  *    new wysihtml5.dom.Sandbox(function(sandbox) {
6844  *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
6845  *    });
6846  */
6847 (function(wysihtml5) {
6848   var /**
6849        * Default configuration
6850        */
6851       doc                 = document,
6852       /**
6853        * Properties to unset/protect on the window object
6854        */
6855       windowProperties    = [
6856         "parent", "top", "opener", "frameElement", "frames",
6857         "localStorage", "globalStorage", "sessionStorage", "indexedDB"
6858       ],
6859       /**
6860        * Properties on the window object which are set to an empty function
6861        */
6862       windowProperties2   = [
6863         "open", "close", "openDialog", "showModalDialog",
6864         "alert", "confirm", "prompt",
6865         "openDatabase", "postMessage",
6866         "XMLHttpRequest", "XDomainRequest"
6867       ],
6868       /**
6869        * Properties to unset/protect on the document object
6870        */
6871       documentProperties  = [
6872         "referrer",
6873         "write", "open", "close"
6874       ];
6875
6876   wysihtml5.dom.Sandbox = Base.extend(
6877     /** @scope wysihtml5.dom.Sandbox.prototype */ {
6878
6879     constructor: function(readyCallback, config) {
6880       this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
6881       this.config   = wysihtml5.lang.object({}).merge(config).get();
6882       this.editableArea   = this._createIframe();
6883     },
6884
6885     insertInto: function(element) {
6886       if (typeof(element) === "string") {
6887         element = doc.getElementById(element);
6888       }
6889
6890       element.appendChild(this.editableArea);
6891     },
6892
6893     getIframe: function() {
6894       return this.editableArea;
6895     },
6896
6897     getWindow: function() {
6898       this._readyError();
6899     },
6900
6901     getDocument: function() {
6902       this._readyError();
6903     },
6904
6905     destroy: function() {
6906       var iframe = this.getIframe();
6907       iframe.parentNode.removeChild(iframe);
6908     },
6909
6910     _readyError: function() {
6911       throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
6912     },
6913
6914     /**
6915      * Creates the sandbox iframe
6916      *
6917      * Some important notes:
6918      *  - We can't use HTML5 sandbox for now:
6919      *    setting it causes that the iframe's dom can't be accessed from the outside
6920      *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
6921      *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
6922      *    In order to make this happen we need to set the "allow-scripts" flag.
6923      *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
6924      *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
6925      *  - IE needs to have the security="restricted" attribute set before the iframe is
6926      *    inserted into the dom tree
6927      *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
6928      *    though it supports it
6929      *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
6930      *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
6931      *    on the onreadystatechange event
6932      */
6933     _createIframe: function() {
6934       var that   = this,
6935           iframe = doc.createElement("iframe");
6936       iframe.className = "wysihtml5-sandbox";
6937       wysihtml5.dom.setAttributes({
6938         "security":           "restricted",
6939         "allowtransparency":  "true",
6940         "frameborder":        0,
6941         "width":              0,
6942         "height":             0,
6943         "marginwidth":        0,
6944         "marginheight":       0
6945       }).on(iframe);
6946
6947       // Setting the src like this prevents ssl warnings in IE6
6948       if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
6949         iframe.src = "javascript:'<html></html>'";
6950       }
6951
6952       iframe.onload = function() {
6953         iframe.onreadystatechange = iframe.onload = null;
6954         that._onLoadIframe(iframe);
6955       };
6956
6957       iframe.onreadystatechange = function() {
6958         if (/loaded|complete/.test(iframe.readyState)) {
6959           iframe.onreadystatechange = iframe.onload = null;
6960           that._onLoadIframe(iframe);
6961         }
6962       };
6963
6964       return iframe;
6965     },
6966
6967     /**
6968      * Callback for when the iframe has finished loading
6969      */
6970     _onLoadIframe: function(iframe) {
6971       // don't resume when the iframe got unloaded (eg. by removing it from the dom)
6972       if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
6973         return;
6974       }
6975
6976       var that           = this,
6977           iframeWindow   = iframe.contentWindow,
6978           iframeDocument = iframe.contentWindow.document,
6979           charset        = doc.characterSet || doc.charset || "utf-8",
6980           sandboxHtml    = this._getHtml({
6981             charset:      charset,
6982             stylesheets:  this.config.stylesheets
6983           });
6984
6985       // Create the basic dom tree including proper DOCTYPE and charset
6986       iframeDocument.open("text/html", "replace");
6987       iframeDocument.write(sandboxHtml);
6988       iframeDocument.close();
6989
6990       this.getWindow = function() { return iframe.contentWindow; };
6991       this.getDocument = function() { return iframe.contentWindow.document; };
6992
6993       // Catch js errors and pass them to the parent's onerror event
6994       // addEventListener("error") doesn't work properly in some browsers
6995       // TODO: apparently this doesn't work in IE9!
6996       iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
6997         throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
6998       };
6999
7000       if (!wysihtml5.browser.supportsSandboxedIframes()) {
7001         // Unset a bunch of sensitive variables
7002         // Please note: This isn't hack safe!
7003         // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
7004         // IE is secure though, which is the most important thing, since IE is the only browser, who
7005         // takes over scripts & styles into contentEditable elements when copied from external websites
7006         // or applications (Microsoft Word, ...)
7007         var i, length;
7008         for (i=0, length=windowProperties.length; i<length; i++) {
7009           this._unset(iframeWindow, windowProperties[i]);
7010         }
7011         for (i=0, length=windowProperties2.length; i<length; i++) {
7012           this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
7013         }
7014         for (i=0, length=documentProperties.length; i<length; i++) {
7015           this._unset(iframeDocument, documentProperties[i]);
7016         }
7017         // This doesn't work in Safari 5
7018         // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
7019         this._unset(iframeDocument, "cookie", "", true);
7020       }
7021
7022       this.loaded = true;
7023
7024       // Trigger the callback
7025       setTimeout(function() { that.callback(that); }, 0);
7026     },
7027
7028     _getHtml: function(templateVars) {
7029       var stylesheets = templateVars.stylesheets,
7030           html        = "",
7031           i           = 0,
7032           length;
7033       stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
7034       if (stylesheets) {
7035         length = stylesheets.length;
7036         for (; i<length; i++) {
7037           html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
7038         }
7039       }
7040       templateVars.stylesheets = html;
7041
7042       return wysihtml5.lang.string(
7043         '<!DOCTYPE html><html><head>'
7044         + '<meta charset="#{charset}">#{stylesheets}</head>'
7045         + '<body></body></html>'
7046       ).interpolate(templateVars);
7047     },
7048
7049     /**
7050      * Method to unset/override existing variables
7051      * @example
7052      *    // Make cookie unreadable and unwritable
7053      *    this._unset(document, "cookie", "", true);
7054      */
7055     _unset: function(object, property, value, setter) {
7056       try { object[property] = value; } catch(e) {}
7057
7058       try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
7059       if (setter) {
7060         try { object.__defineSetter__(property, function() {}); } catch(e) {}
7061       }
7062
7063       if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
7064         try {
7065           var config = {
7066             get: function() { return value; }
7067           };
7068           if (setter) {
7069             config.set = function() {};
7070           }
7071           Object.defineProperty(object, property, config);
7072         } catch(e) {}
7073       }
7074     }
7075   });
7076 })(wysihtml5);
7077 ;(function(wysihtml5) {
7078   var doc = document;
7079   wysihtml5.dom.ContentEditableArea = Base.extend({
7080       getContentEditable: function() {
7081         return this.element;
7082       },
7083
7084       getWindow: function() {
7085         return this.element.ownerDocument.defaultView;
7086       },
7087
7088       getDocument: function() {
7089         return this.element.ownerDocument;
7090       },
7091
7092       constructor: function(readyCallback, config, contentEditable) {
7093         this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
7094         this.config   = wysihtml5.lang.object({}).merge(config).get();
7095         if (contentEditable) {
7096             this.element = this._bindElement(contentEditable);
7097         } else {
7098             this.element = this._createElement();
7099         }
7100       },
7101
7102       // creates a new contenteditable and initiates it
7103       _createElement: function() {
7104         var element = doc.createElement("div");
7105         element.className = "wysihtml5-sandbox";
7106         this._loadElement(element);
7107         return element;
7108       },
7109
7110       // initiates an allready existent contenteditable
7111       _bindElement: function(contentEditable) {
7112         contentEditable.className = (contentEditable.className && contentEditable.className != '') ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox";
7113         this._loadElement(contentEditable, true);
7114         return contentEditable;
7115       },
7116
7117       _loadElement: function(element, contentExists) {
7118           var that = this;
7119         if (!contentExists) {
7120             var sandboxHtml = this._getHtml();
7121             element.innerHTML = sandboxHtml;
7122         }
7123
7124         this.getWindow = function() { return element.ownerDocument.defaultView; };
7125         this.getDocument = function() { return element.ownerDocument; };
7126
7127         // Catch js errors and pass them to the parent's onerror event
7128         // addEventListener("error") doesn't work properly in some browsers
7129         // TODO: apparently this doesn't work in IE9!
7130         // TODO: figure out and bind the errors logic for contenteditble mode
7131         /*iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
7132           throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
7133         }
7134         */
7135         this.loaded = true;
7136         // Trigger the callback
7137         setTimeout(function() { that.callback(that); }, 0);
7138       },
7139
7140       _getHtml: function(templateVars) {
7141         return '';
7142       }
7143
7144   });
7145 })(wysihtml5);
7146 ;(function() {
7147   var mapping = {
7148     "className": "class"
7149   };
7150   wysihtml5.dom.setAttributes = function(attributes) {
7151     return {
7152       on: function(element) {
7153         for (var i in attributes) {
7154           element.setAttribute(mapping[i] || i, attributes[i]);
7155         }
7156       }
7157     };
7158   };
7159 })();
7160 ;wysihtml5.dom.setStyles = function(styles) {
7161   return {
7162     on: function(element) {
7163       var style = element.style;
7164       if (typeof(styles) === "string") {
7165         style.cssText += ";" + styles;
7166         return;
7167       }
7168       for (var i in styles) {
7169         if (i === "float") {
7170           style.cssFloat = styles[i];
7171           style.styleFloat = styles[i];
7172         } else {
7173           style[i] = styles[i];
7174         }
7175       }
7176     }
7177   };
7178 };
7179 ;/**
7180  * Simulate HTML5 placeholder attribute
7181  *
7182  * Needed since
7183  *    - div[contentEditable] elements don't support it
7184  *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
7185  *
7186  * @param {Object} parent Instance of main wysihtml5.Editor class
7187  * @param {Element} view Instance of wysihtml5.views.* class
7188  * @param {String} placeholderText
7189  *
7190  * @example
7191  *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
7192  */
7193 (function(dom) {
7194   dom.simulatePlaceholder = function(editor, view, placeholderText) {
7195     var CLASS_NAME = "placeholder",
7196         unset = function() {
7197           var composerIsVisible   = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
7198           if (view.hasPlaceholderSet()) {
7199             view.clear();
7200             view.element.focus();
7201             if (composerIsVisible ) {
7202               setTimeout(function() {
7203                 var sel = view.selection.getSelection();
7204                 if (!sel.focusNode || !sel.anchorNode) {
7205                   view.selection.selectNode(view.element.firstChild || view.element);
7206                 }
7207               }, 0);
7208             }
7209           }
7210           view.placeholderSet = false;
7211           dom.removeClass(view.element, CLASS_NAME);
7212         },
7213         set = function() {
7214           if (view.isEmpty()) {
7215             view.placeholderSet = true;
7216             view.setValue(placeholderText);
7217             dom.addClass(view.element, CLASS_NAME);
7218           }
7219         };
7220
7221     editor
7222       .on("set_placeholder", set)
7223       .on("unset_placeholder", unset)
7224       .on("focus:composer", unset)
7225       .on("paste:composer", unset)
7226       .on("blur:composer", set);
7227
7228     set();
7229   };
7230 })(wysihtml5.dom);
7231 ;(function(dom) {
7232   var documentElement = document.documentElement;
7233   if ("textContent" in documentElement) {
7234     dom.setTextContent = function(element, text) {
7235       element.textContent = text;
7236     };
7237
7238     dom.getTextContent = function(element) {
7239       return element.textContent;
7240     };
7241   } else if ("innerText" in documentElement) {
7242     dom.setTextContent = function(element, text) {
7243       element.innerText = text;
7244     };
7245
7246     dom.getTextContent = function(element) {
7247       return element.innerText;
7248     };
7249   } else {
7250     dom.setTextContent = function(element, text) {
7251       element.nodeValue = text;
7252     };
7253
7254     dom.getTextContent = function(element) {
7255       return element.nodeValue;
7256     };
7257   }
7258 })(wysihtml5.dom);
7259
7260 ;/**
7261  * Get a set of attribute from one element
7262  *
7263  * IE gives wrong results for hasAttribute/getAttribute, for example:
7264  *    var td = document.createElement("td");
7265  *    td.getAttribute("rowspan"); // => "1" in IE
7266  *
7267  * Therefore we have to check the element's outerHTML for the attribute
7268 */
7269
7270 wysihtml5.dom.getAttribute = function(node, attributeName) {
7271   var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
7272   attributeName = attributeName.toLowerCase();
7273   var nodeName = node.nodeName;
7274   if (nodeName == "IMG" && attributeName == "src" && wysihtml5.dom.isLoadedImage(node) === true) {
7275     // Get 'src' attribute value via object property since this will always contain the
7276     // full absolute url (http://...)
7277     // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
7278     // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
7279     return node.src;
7280   } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
7281     // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
7282     var outerHTML      = node.outerHTML.toLowerCase(),
7283         // TODO: This might not work for attributes without value: <input disabled>
7284         hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
7285
7286     return hasAttribute ? node.getAttribute(attributeName) : null;
7287   } else{
7288     return node.getAttribute(attributeName);
7289   }
7290 };
7291 ;/**
7292  * Get all attributes of an element
7293  *
7294  * IE gives wrong results for hasAttribute/getAttribute, for example:
7295  *    var td = document.createElement("td");
7296  *    td.getAttribute("rowspan"); // => "1" in IE
7297  *
7298  * Therefore we have to check the element's outerHTML for the attribute
7299 */
7300
7301 wysihtml5.dom.getAttributes = function(node) {
7302   var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(),
7303       nodeName = node.nodeName,
7304       attributes = [],
7305       attr;
7306
7307   for (attr in node.attributes) {
7308     if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr)))  {
7309       if (node.attributes[attr].specified) {
7310         if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml5.dom.isLoadedImage(node) === true) {
7311           attributes['src'] = node.src;
7312         } else if (wysihtml5.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
7313           if (node.attributes[attr].value !== 1) {
7314             attributes[node.attributes[attr].name] = node.attributes[attr].value;
7315           }
7316         } else {
7317           attributes[node.attributes[attr].name] = node.attributes[attr].value;
7318         }
7319       }
7320     }
7321   }
7322   return attributes;
7323 };;/**
7324    * Check whether the given node is a proper loaded image
7325    * FIXME: Returns undefined when unknown (Chrome, Safari)
7326 */
7327
7328 wysihtml5.dom.isLoadedImage = function (node) {
7329   try {
7330     return node.complete && !node.mozMatchesSelector(":-moz-broken");
7331   } catch(e) {
7332     if (node.complete && node.readyState === "complete") {
7333       return true;
7334     }
7335   }
7336 };
7337 ;(function(wysihtml5) {
7338
7339     var api = wysihtml5.dom;
7340
7341     var MapCell = function(cell) {
7342       this.el = cell;
7343       this.isColspan= false;
7344       this.isRowspan= false;
7345       this.firstCol= true;
7346       this.lastCol= true;
7347       this.firstRow= true;
7348       this.lastRow= true;
7349       this.isReal= true;
7350       this.spanCollection= [];
7351       this.modified = false;
7352     };
7353
7354     var TableModifyerByCell = function (cell, table) {
7355         if (cell) {
7356             this.cell = cell;
7357             this.table = api.getParentElement(cell, { nodeName: ["TABLE"] });
7358         } else if (table) {
7359             this.table = table;
7360             this.cell = this.table.querySelectorAll('th, td')[0];
7361         }
7362     };
7363
7364     function queryInList(list, query) {
7365         var ret = [],
7366             q;
7367         for (var e = 0, len = list.length; e < len; e++) {
7368             q = list[e].querySelectorAll(query);
7369             if (q) {
7370                 for(var i = q.length; i--; ret.unshift(q[i]));
7371             }
7372         }
7373         return ret;
7374     }
7375
7376     function removeElement(el) {
7377         el.parentNode.removeChild(el);
7378     }
7379
7380     function insertAfter(referenceNode, newNode) {
7381         referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
7382     }
7383
7384     function nextNode(node, tag) {
7385         var element = node.nextSibling;
7386         while (element.nodeType !=1) {
7387             element = element.nextSibling;
7388             if (!tag || tag == element.tagName.toLowerCase()) {
7389                 return element;
7390             }
7391         }
7392         return null;
7393     }
7394
7395     TableModifyerByCell.prototype = {
7396
7397         addSpannedCellToMap: function(cell, map, r, c, cspan, rspan) {
7398             var spanCollect = [],
7399                 rmax = r + ((rspan) ? parseInt(rspan, 10) - 1 : 0),
7400                 cmax = c + ((cspan) ? parseInt(cspan, 10) - 1 : 0);
7401
7402             for (var rr = r; rr <= rmax; rr++) {
7403                 if (typeof map[rr] == "undefined") { map[rr] = []; }
7404                 for (var cc = c; cc <= cmax; cc++) {
7405                     map[rr][cc] = new MapCell(cell);
7406                     map[rr][cc].isColspan = (cspan && parseInt(cspan, 10) > 1);
7407                     map[rr][cc].isRowspan = (rspan && parseInt(rspan, 10) > 1);
7408                     map[rr][cc].firstCol = cc == c;
7409                     map[rr][cc].lastCol = cc == cmax;
7410                     map[rr][cc].firstRow = rr == r;
7411                     map[rr][cc].lastRow = rr == rmax;
7412                     map[rr][cc].isReal = cc == c && rr == r;
7413                     map[rr][cc].spanCollection = spanCollect;
7414
7415                     spanCollect.push(map[rr][cc]);
7416                 }
7417             }
7418         },
7419
7420         setCellAsModified: function(cell) {
7421             cell.modified = true;
7422             if (cell.spanCollection.length > 0) {
7423               for (var s = 0, smax = cell.spanCollection.length; s < smax; s++) {
7424                 cell.spanCollection[s].modified = true;
7425               }
7426             }
7427         },
7428
7429         setTableMap: function() {
7430             var map = [];
7431             var tableRows = this.getTableRows(),
7432                 ridx, row, cells, cidx, cell,
7433                 c,
7434                 cspan, rspan;
7435
7436             for (ridx = 0; ridx < tableRows.length; ridx++) {
7437                 row = tableRows[ridx];
7438                 cells = this.getRowCells(row);
7439                 c = 0;
7440                 if (typeof map[ridx] == "undefined") { map[ridx] = []; }
7441                 for (cidx = 0; cidx < cells.length; cidx++) {
7442                     cell = cells[cidx];
7443
7444                     // If cell allready set means it is set by col or rowspan,
7445                     // so increase cols index until free col is found
7446                     while (typeof map[ridx][c] != "undefined") { c++; }
7447
7448                     cspan = api.getAttribute(cell, 'colspan');
7449                     rspan = api.getAttribute(cell, 'rowspan');
7450
7451                     if (cspan || rspan) {
7452                         this.addSpannedCellToMap(cell, map, ridx, c, cspan, rspan);
7453                         c = c + ((cspan) ? parseInt(cspan, 10) : 1);
7454                     } else {
7455                         map[ridx][c] = new MapCell(cell);
7456                         c++;
7457                     }
7458                 }
7459             }
7460             this.map = map;
7461             return map;
7462         },
7463
7464         getRowCells: function(row) {
7465             var inlineTables = this.table.querySelectorAll('table'),
7466                 inlineCells = (inlineTables) ? queryInList(inlineTables, 'th, td') : [],
7467                 allCells = row.querySelectorAll('th, td'),
7468                 tableCells = (inlineCells.length > 0) ? wysihtml5.lang.array(allCells).without(inlineCells) : allCells;
7469
7470             return tableCells;
7471         },
7472
7473         getTableRows: function() {
7474           var inlineTables = this.table.querySelectorAll('table'),
7475               inlineRows = (inlineTables) ? queryInList(inlineTables, 'tr') : [],
7476               allRows = this.table.querySelectorAll('tr'),
7477               tableRows = (inlineRows.length > 0) ? wysihtml5.lang.array(allRows).without(inlineRows) : allRows;
7478
7479           return tableRows;
7480         },
7481
7482         getMapIndex: function(cell) {
7483           var r_length = this.map.length,
7484               c_length = (this.map && this.map[0]) ? this.map[0].length : 0;
7485
7486           for (var r_idx = 0;r_idx < r_length; r_idx++) {
7487               for (var c_idx = 0;c_idx < c_length; c_idx++) {
7488                   if (this.map[r_idx][c_idx].el === cell) {
7489                       return {'row': r_idx, 'col': c_idx};
7490                   }
7491               }
7492           }
7493           return false;
7494         },
7495
7496         getElementAtIndex: function(idx) {
7497             this.setTableMap();
7498             if (this.map[idx.row] && this.map[idx.row][idx.col] && this.map[idx.row][idx.col].el) {
7499                 return this.map[idx.row][idx.col].el;
7500             }
7501             return null;
7502         },
7503
7504         getMapElsTo: function(to_cell) {
7505             var els = [];
7506             this.setTableMap();
7507             this.idx_start = this.getMapIndex(this.cell);
7508             this.idx_end = this.getMapIndex(to_cell);
7509
7510             // switch indexes if start is bigger than end
7511             if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7512                 var temp_idx = this.idx_start;
7513                 this.idx_start = this.idx_end;
7514                 this.idx_end = temp_idx;
7515             }
7516             if (this.idx_start.col > this.idx_end.col) {
7517                 var temp_cidx = this.idx_start.col;
7518                 this.idx_start.col = this.idx_end.col;
7519                 this.idx_end.col = temp_cidx;
7520             }
7521
7522             if (this.idx_start != null && this.idx_end != null) {
7523                 for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7524                     for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7525                         els.push(this.map[row][col].el);
7526                     }
7527                 }
7528             }
7529             return els;
7530         },
7531
7532         orderSelectionEnds: function(secondcell) {
7533             this.setTableMap();
7534             this.idx_start = this.getMapIndex(this.cell);
7535             this.idx_end = this.getMapIndex(secondcell);
7536
7537             // switch indexes if start is bigger than end
7538             if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7539                 var temp_idx = this.idx_start;
7540                 this.idx_start = this.idx_end;
7541                 this.idx_end = temp_idx;
7542             }
7543             if (this.idx_start.col > this.idx_end.col) {
7544                 var temp_cidx = this.idx_start.col;
7545                 this.idx_start.col = this.idx_end.col;
7546                 this.idx_end.col = temp_cidx;
7547             }
7548
7549             return {
7550                 "start": this.map[this.idx_start.row][this.idx_start.col].el,
7551                 "end": this.map[this.idx_end.row][this.idx_end.col].el
7552             };
7553         },
7554
7555         createCells: function(tag, nr, attrs) {
7556             var doc = this.table.ownerDocument,
7557                 frag = doc.createDocumentFragment(),
7558                 cell;
7559             for (var i = 0; i < nr; i++) {
7560                 cell = doc.createElement(tag);
7561
7562                 if (attrs) {
7563                     for (var attr in attrs) {
7564                         if (attrs.hasOwnProperty(attr)) {
7565                             cell.setAttribute(attr, attrs[attr]);
7566                         }
7567                     }
7568                 }
7569
7570                 // add non breaking space
7571                 cell.appendChild(document.createTextNode("\u00a0"));
7572
7573                 frag.appendChild(cell);
7574             }
7575             return frag;
7576         },
7577
7578         // Returns next real cell (not part of spanned cell unless first) on row if selected index is not real. I no real cells -1 will be returned
7579         correctColIndexForUnreals: function(col, row) {
7580             var r = this.map[row],
7581                 corrIdx = -1;
7582             for (var i = 0, max = col; i < col; i++) {
7583                 if (r[i].isReal){
7584                     corrIdx++;
7585                 }
7586             }
7587             return corrIdx;
7588         },
7589
7590         getLastNewCellOnRow: function(row, rowLimit) {
7591             var cells = this.getRowCells(row),
7592                 cell, idx;
7593
7594             for (var cidx = 0, cmax = cells.length; cidx < cmax; cidx++) {
7595                 cell = cells[cidx];
7596                 idx = this.getMapIndex(cell);
7597                 if (idx === false || (typeof rowLimit != "undefined" && idx.row != rowLimit)) {
7598                     return cell;
7599                 }
7600             }
7601             return null;
7602         },
7603
7604         removeEmptyTable: function() {
7605             var cells = this.table.querySelectorAll('td, th');
7606             if (!cells || cells.length == 0) {
7607                 removeElement(this.table);
7608                 return true;
7609             } else {
7610                 return false;
7611             }
7612         },
7613
7614         // Splits merged cell on row to unique cells
7615         splitRowToCells: function(cell) {
7616             if (cell.isColspan) {
7617                 var colspan = parseInt(api.getAttribute(cell.el, 'colspan') || 1, 10),
7618                     cType = cell.el.tagName.toLowerCase();
7619                 if (colspan > 1) {
7620                     var newCells = this.createCells(cType, colspan -1);
7621                     insertAfter(cell.el, newCells);
7622                 }
7623                 cell.el.removeAttribute('colspan');
7624             }
7625         },
7626
7627         getRealRowEl: function(force, idx) {
7628             var r = null,
7629                 c = null;
7630
7631             idx = idx || this.idx;
7632
7633             for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) {
7634                 c = this.map[idx.row][cidx];
7635                 if (c.isReal) {
7636                     r = api.getParentElement(c.el, { nodeName: ["TR"] });
7637                     if (r) {
7638                         return r;
7639                     }
7640                 }
7641             }
7642
7643             if (r === null && force) {
7644                 r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null;
7645             }
7646
7647             return r;
7648         },
7649
7650         injectRowAt: function(row, col, colspan, cType, c) {
7651             var r = this.getRealRowEl(false, {'row': row, 'col': col}),
7652                 new_cells = this.createCells(cType, colspan);
7653
7654             if (r) {
7655                 var n_cidx = this.correctColIndexForUnreals(col, row);
7656                 if (n_cidx >= 0) {
7657                     insertAfter(this.getRowCells(r)[n_cidx], new_cells);
7658                 } else {
7659                     r.insertBefore(new_cells, r.firstChild);
7660                 }
7661             } else {
7662                 var rr = this.table.ownerDocument.createElement('tr');
7663                 rr.appendChild(new_cells);
7664                 insertAfter(api.getParentElement(c.el, { nodeName: ["TR"] }), rr);
7665             }
7666         },
7667
7668         canMerge: function(to) {
7669             this.to = to;
7670             this.setTableMap();
7671             this.idx_start = this.getMapIndex(this.cell);
7672             this.idx_end = this.getMapIndex(this.to);
7673
7674             // switch indexes if start is bigger than end
7675             if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7676                 var temp_idx = this.idx_start;
7677                 this.idx_start = this.idx_end;
7678                 this.idx_end = temp_idx;
7679             }
7680             if (this.idx_start.col > this.idx_end.col) {
7681                 var temp_cidx = this.idx_start.col;
7682                 this.idx_start.col = this.idx_end.col;
7683                 this.idx_end.col = temp_cidx;
7684             }
7685
7686             for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7687                 for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7688                     if (this.map[row][col].isColspan || this.map[row][col].isRowspan) {
7689                         return false;
7690                     }
7691                 }
7692             }
7693             return true;
7694         },
7695
7696         decreaseCellSpan: function(cell, span) {
7697             var nr = parseInt(api.getAttribute(cell.el, span), 10) - 1;
7698             if (nr >= 1) {
7699                 cell.el.setAttribute(span, nr);
7700             } else {
7701                 cell.el.removeAttribute(span);
7702                 if (span == 'colspan') {
7703                     cell.isColspan = false;
7704                 }
7705                 if (span == 'rowspan') {
7706                     cell.isRowspan = false;
7707                 }
7708                 cell.firstCol = true;
7709                 cell.lastCol = true;
7710                 cell.firstRow = true;
7711                 cell.lastRow = true;
7712                 cell.isReal = true;
7713             }
7714         },
7715
7716         removeSurplusLines: function() {
7717             var row, cell, ridx, rmax, cidx, cmax, allRowspan;
7718
7719             this.setTableMap();
7720             if (this.map) {
7721                 ridx = 0;
7722                 rmax = this.map.length;
7723                 for (;ridx < rmax; ridx++) {
7724                     row = this.map[ridx];
7725                     allRowspan = true;
7726                     cidx = 0;
7727                     cmax = row.length;
7728                     for (; cidx < cmax; cidx++) {
7729                         cell = row[cidx];
7730                         if (!(api.getAttribute(cell.el, "rowspan") && parseInt(api.getAttribute(cell.el, "rowspan"), 10) > 1 && cell.firstRow !== true)) {
7731                             allRowspan = false;
7732                             break;
7733                         }
7734                     }
7735                     if (allRowspan) {
7736                         cidx = 0;
7737                         for (; cidx < cmax; cidx++) {
7738                             this.decreaseCellSpan(row[cidx], 'rowspan');
7739                         }
7740                     }
7741                 }
7742
7743                 // remove rows without cells
7744                 var tableRows = this.getTableRows();
7745                 ridx = 0;
7746                 rmax = tableRows.length;
7747                 for (;ridx < rmax; ridx++) {
7748                     row = tableRows[ridx];
7749                     if (row.childNodes.length == 0 && (/^\s*$/.test(row.textContent || row.innerText))) {
7750                         removeElement(row);
7751                     }
7752                 }
7753             }
7754         },
7755
7756         fillMissingCells: function() {
7757             var r_max = 0,
7758                 c_max = 0,
7759                 prevcell = null;
7760
7761             this.setTableMap();
7762             if (this.map) {
7763
7764                 // find maximal dimensions of broken table
7765                 r_max = this.map.length;
7766                 for (var ridx = 0; ridx < r_max; ridx++) {
7767                     if (this.map[ridx].length > c_max) { c_max = this.map[ridx].length; }
7768                 }
7769
7770                 for (var row = 0; row < r_max; row++) {
7771                     for (var col = 0; col < c_max; col++) {
7772                         if (this.map[row] && !this.map[row][col]) {
7773                             if (col > 0) {
7774                                 this.map[row][col] = new MapCell(this.createCells('td', 1));
7775                                 prevcell = this.map[row][col-1];
7776                                 if (prevcell && prevcell.el && prevcell.el.parent) { // if parent does not exist element is removed from dom
7777                                     insertAfter(this.map[row][col-1].el, this.map[row][col].el);
7778                                 }
7779                             }
7780                         }
7781                     }
7782                 }
7783             }
7784         },
7785
7786         rectify: function() {
7787             if (!this.removeEmptyTable()) {
7788                 this.removeSurplusLines();
7789                 this.fillMissingCells();
7790                 return true;
7791             } else {
7792                 return false;
7793             }
7794         },
7795
7796         unmerge: function() {
7797             if (this.rectify()) {
7798                 this.setTableMap();
7799                 this.idx = this.getMapIndex(this.cell);
7800
7801                 if (this.idx) {
7802                     var thisCell = this.map[this.idx.row][this.idx.col],
7803                         colspan = (api.getAttribute(thisCell.el, "colspan")) ? parseInt(api.getAttribute(thisCell.el, "colspan"), 10) : 1,
7804                         cType = thisCell.el.tagName.toLowerCase();
7805
7806                     if (thisCell.isRowspan) {
7807                         var rowspan = parseInt(api.getAttribute(thisCell.el, "rowspan"), 10);
7808                         if (rowspan > 1) {
7809                             for (var nr = 1, maxr = rowspan - 1; nr <= maxr; nr++){
7810                                 this.injectRowAt(this.idx.row + nr, this.idx.col, colspan, cType, thisCell);
7811                             }
7812                         }
7813                         thisCell.el.removeAttribute('rowspan');
7814                     }
7815                     this.splitRowToCells(thisCell);
7816                 }
7817             }
7818         },
7819
7820         // merges cells from start cell (defined in creating obj) to "to" cell
7821         merge: function(to) {
7822             if (this.rectify()) {
7823                 if (this.canMerge(to)) {
7824                     var rowspan = this.idx_end.row - this.idx_start.row + 1,
7825                         colspan = this.idx_end.col - this.idx_start.col + 1;
7826
7827                     for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7828                         for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7829
7830                             if (row == this.idx_start.row && col == this.idx_start.col) {
7831                                 if (rowspan > 1) {
7832                                     this.map[row][col].el.setAttribute('rowspan', rowspan);
7833                                 }
7834                                 if (colspan > 1) {
7835                                     this.map[row][col].el.setAttribute('colspan', colspan);
7836                                 }
7837                             } else {
7838                                 // transfer content
7839                                 if (!(/^\s*<br\/?>\s*$/.test(this.map[row][col].el.innerHTML.toLowerCase()))) {
7840                                     this.map[this.idx_start.row][this.idx_start.col].el.innerHTML += ' ' + this.map[row][col].el.innerHTML;
7841                                 }
7842                                 removeElement(this.map[row][col].el);
7843                             }
7844                         }
7845                     }
7846                     this.rectify();
7847                 } else {
7848                     if (window.console) {
7849                         console.log('Do not know how to merge allready merged cells.');
7850                     }
7851                 }
7852             }
7853         },
7854
7855         // Decreases rowspan of a cell if it is done on first cell of rowspan row (real cell)
7856         // Cell is moved to next row (if it is real)
7857         collapseCellToNextRow: function(cell) {
7858             var cellIdx = this.getMapIndex(cell.el),
7859                 newRowIdx = cellIdx.row + 1,
7860                 newIdx = {'row': newRowIdx, 'col': cellIdx.col};
7861
7862             if (newRowIdx < this.map.length) {
7863
7864                 var row = this.getRealRowEl(false, newIdx);
7865                 if (row !== null) {
7866                     var n_cidx = this.correctColIndexForUnreals(newIdx.col, newIdx.row);
7867                     if (n_cidx >= 0) {
7868                         insertAfter(this.getRowCells(row)[n_cidx], cell.el);
7869                     } else {
7870                         var lastCell = this.getLastNewCellOnRow(row, newRowIdx);
7871                         if (lastCell !== null) {
7872                             insertAfter(lastCell, cell.el);
7873                         } else {
7874                             row.insertBefore(cell.el, row.firstChild);
7875                         }
7876                     }
7877                     if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7878                         cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7879                     } else {
7880                         cell.el.removeAttribute('rowspan');
7881                     }
7882                 }
7883             }
7884         },
7885
7886         // Removes a cell when removing a row
7887         // If is rowspan cell then decreases the rowspan
7888         // and moves cell to next row if needed (is first cell of rowspan)
7889         removeRowCell: function(cell) {
7890             if (cell.isReal) {
7891                if (cell.isRowspan) {
7892                    this.collapseCellToNextRow(cell);
7893                } else {
7894                    removeElement(cell.el);
7895                }
7896             } else {
7897                 if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7898                     cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7899                 } else {
7900                     cell.el.removeAttribute('rowspan');
7901                 }
7902             }
7903         },
7904
7905         getRowElementsByCell: function() {
7906             var cells = [];
7907             this.setTableMap();
7908             this.idx = this.getMapIndex(this.cell);
7909             if (this.idx !== false) {
7910                 var modRow = this.map[this.idx.row];
7911                 for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) {
7912                     if (modRow[cidx].isReal) {
7913                         cells.push(modRow[cidx].el);
7914                     }
7915                 }
7916             }
7917             return cells;
7918         },
7919
7920         getColumnElementsByCell: function() {
7921             var cells = [];
7922             this.setTableMap();
7923             this.idx = this.getMapIndex(this.cell);
7924             if (this.idx !== false) {
7925                 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) {
7926                     if (this.map[ridx][this.idx.col] && this.map[ridx][this.idx.col].isReal) {
7927                         cells.push(this.map[ridx][this.idx.col].el);
7928                     }
7929                 }
7930             }
7931             return cells;
7932         },
7933
7934         // Removes the row of selected cell
7935         removeRow: function() {
7936             var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] });
7937             if (oldRow) {
7938                 this.setTableMap();
7939                 this.idx = this.getMapIndex(this.cell);
7940                 if (this.idx !== false) {
7941                     var modRow = this.map[this.idx.row];
7942                     for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) {
7943                         if (!modRow[cidx].modified) {
7944                             this.setCellAsModified(modRow[cidx]);
7945                             this.removeRowCell(modRow[cidx]);
7946                         }
7947                     }
7948                 }
7949                 removeElement(oldRow);
7950             }
7951         },
7952
7953         removeColCell: function(cell) {
7954             if (cell.isColspan) {
7955                 if (parseInt(api.getAttribute(cell.el, 'colspan'), 10) > 2) {
7956                     cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) - 1);
7957                 } else {
7958                     cell.el.removeAttribute('colspan');
7959                 }
7960             } else if (cell.isReal) {
7961                 removeElement(cell.el);
7962             }
7963         },
7964
7965         removeColumn: function() {
7966             this.setTableMap();
7967             this.idx = this.getMapIndex(this.cell);
7968             if (this.idx !== false) {
7969                 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) {
7970                     if (!this.map[ridx][this.idx.col].modified) {
7971                         this.setCellAsModified(this.map[ridx][this.idx.col]);
7972                         this.removeColCell(this.map[ridx][this.idx.col]);
7973                     }
7974                 }
7975             }
7976         },
7977
7978         // removes row or column by selected cell element
7979         remove: function(what) {
7980             if (this.rectify()) {
7981                 switch (what) {
7982                     case 'row':
7983                         this.removeRow();
7984                     break;
7985                     case 'column':
7986                         this.removeColumn();
7987                     break;
7988                 }
7989                 this.rectify();
7990             }
7991         },
7992
7993         addRow: function(where) {
7994             var doc = this.table.ownerDocument;
7995
7996             this.setTableMap();
7997             this.idx = this.getMapIndex(this.cell);
7998             if (where == "below" && api.getAttribute(this.cell, 'rowspan')) {
7999                 this.idx.row = this.idx.row + parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1;
8000             }
8001
8002             if (this.idx !== false) {
8003                 var modRow = this.map[this.idx.row],
8004                     newRow = doc.createElement('tr');
8005
8006                 for (var ridx = 0, rmax = modRow.length; ridx < rmax; ridx++) {
8007                     if (!modRow[ridx].modified) {
8008                         this.setCellAsModified(modRow[ridx]);
8009                         this.addRowCell(modRow[ridx], newRow, where);
8010                     }
8011                 }
8012
8013                 switch (where) {
8014                     case 'below':
8015                         insertAfter(this.getRealRowEl(true), newRow);
8016                     break;
8017                     case 'above':
8018                         var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] });
8019                         if (cr) {
8020                             cr.parentNode.insertBefore(newRow, cr);
8021                         }
8022                     break;
8023                 }
8024             }
8025         },
8026
8027         addRowCell: function(cell, row, where) {
8028             var colSpanAttr = (cell.isColspan) ? {"colspan" : api.getAttribute(cell.el, 'colspan')} : null;
8029             if (cell.isReal) {
8030                 if (where != 'above' && cell.isRowspan) {
8031                     cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el,'rowspan'), 10) + 1);
8032                 } else {
8033                     row.appendChild(this.createCells('td', 1, colSpanAttr));
8034                 }
8035             } else {
8036                 if (where != 'above' && cell.isRowspan && cell.lastRow) {
8037                     row.appendChild(this.createCells('td', 1, colSpanAttr));
8038                 } else if (c.isRowspan) {
8039                     cell.el.attr('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) + 1);
8040                 }
8041             }
8042         },
8043
8044         add: function(where) {
8045             if (this.rectify()) {
8046                 if (where == 'below' || where == 'above') {
8047                     this.addRow(where);
8048                 }
8049                 if (where == 'before' || where == 'after') {
8050                     this.addColumn(where);
8051                 }
8052             }
8053         },
8054
8055         addColCell: function (cell, ridx, where) {
8056             var doAdd,
8057                 cType = cell.el.tagName.toLowerCase();
8058
8059             // defines add cell vs expand cell conditions
8060             // true means add
8061             switch (where) {
8062                 case "before":
8063                     doAdd = (!cell.isColspan || cell.firstCol);
8064                 break;
8065                 case "after":
8066                     doAdd = (!cell.isColspan || cell.lastCol || (cell.isColspan && c.el == this.cell));
8067                 break;
8068             }
8069
8070             if (doAdd){
8071                 // adds a cell before or after current cell element
8072                 switch (where) {
8073                     case "before":
8074                         cell.el.parentNode.insertBefore(this.createCells(cType, 1), cell.el);
8075                     break;
8076                     case "after":
8077                         insertAfter(cell.el, this.createCells(cType, 1));
8078                     break;
8079                 }
8080
8081                 // handles if cell has rowspan
8082                 if (cell.isRowspan) {
8083                     this.handleCellAddWithRowspan(cell, ridx+1, where);
8084                 }
8085
8086             } else {
8087                 // expands cell
8088                 cell.el.setAttribute('colspan',  parseInt(api.getAttribute(cell.el, 'colspan'), 10) + 1);
8089             }
8090         },
8091
8092         addColumn: function(where) {
8093             var row, modCell;
8094
8095             this.setTableMap();
8096             this.idx = this.getMapIndex(this.cell);
8097             if (where == "after" && api.getAttribute(this.cell, 'colspan')) {
8098               this.idx.col = this.idx.col + parseInt(api.getAttribute(this.cell, 'colspan'), 10) - 1;
8099             }
8100
8101             if (this.idx !== false) {
8102                 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++ ) {
8103                     row = this.map[ridx];
8104                     if (row[this.idx.col]) {
8105                         modCell = row[this.idx.col];
8106                         if (!modCell.modified) {
8107                             this.setCellAsModified(modCell);
8108                             this.addColCell(modCell, ridx , where);
8109                         }
8110                     }
8111                 }
8112             }
8113         },
8114
8115         handleCellAddWithRowspan: function (cell, ridx, where) {
8116             var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1,
8117                 crow = api.getParentElement(cell.el, { nodeName: ["TR"] }),
8118                 cType = cell.el.tagName.toLowerCase(),
8119                 cidx, temp_r_cells,
8120                 doc = this.table.ownerDocument,
8121                 nrow;
8122
8123             for (var i = 0; i < addRowsNr; i++) {
8124                 cidx = this.correctColIndexForUnreals(this.idx.col, (ridx + i));
8125                 crow = nextNode(crow, 'tr');
8126                 if (crow) {
8127                     if (cidx > 0) {
8128                         switch (where) {
8129                             case "before":
8130                                 temp_r_cells = this.getRowCells(crow);
8131                                 if (cidx > 0 && this.map[ridx + i][this.idx.col].el != temp_r_cells[cidx] && cidx == temp_r_cells.length - 1) {
8132                                      insertAfter(temp_r_cells[cidx], this.createCells(cType, 1));
8133                                 } else {
8134                                     temp_r_cells[cidx].parentNode.insertBefore(this.createCells(cType, 1), temp_r_cells[cidx]);
8135                                 }
8136
8137                             break;
8138                             case "after":
8139                                 insertAfter(this.getRowCells(crow)[cidx], this.createCells(cType, 1));
8140                             break;
8141                         }
8142                     } else {
8143                         crow.insertBefore(this.createCells(cType, 1), crow.firstChild);
8144                     }
8145                 } else {
8146                     nrow = doc.createElement('tr');
8147                     nrow.appendChild(this.createCells(cType, 1));
8148                     this.table.appendChild(nrow);
8149                 }
8150             }
8151         }
8152     };
8153
8154     api.table = {
8155         getCellsBetween: function(cell1, cell2) {
8156             var c1 = new TableModifyerByCell(cell1);
8157             return c1.getMapElsTo(cell2);
8158         },
8159
8160         addCells: function(cell, where) {
8161             var c = new TableModifyerByCell(cell);
8162             c.add(where);
8163         },
8164
8165         removeCells: function(cell, what) {
8166             var c = new TableModifyerByCell(cell);
8167             c.remove(what);
8168         },
8169
8170         mergeCellsBetween: function(cell1, cell2) {
8171             var c1 = new TableModifyerByCell(cell1);
8172             c1.merge(cell2);
8173         },
8174
8175         unmergeCell: function(cell) {
8176             var c = new TableModifyerByCell(cell);
8177             c.unmerge();
8178         },
8179
8180         orderSelectionEnds: function(cell, cell2) {
8181             var c = new TableModifyerByCell(cell);
8182             return c.orderSelectionEnds(cell2);
8183         },
8184
8185         indexOf: function(cell) {
8186             var c = new TableModifyerByCell(cell);
8187             c.setTableMap();
8188             return c.getMapIndex(cell);
8189         },
8190
8191         findCell: function(table, idx) {
8192             var c = new TableModifyerByCell(null, table);
8193             return c.getElementAtIndex(idx);
8194         },
8195
8196         findRowByCell: function(cell) {
8197             var c = new TableModifyerByCell(cell);
8198             return c.getRowElementsByCell();
8199         },
8200
8201         findColumnByCell: function(cell) {
8202             var c = new TableModifyerByCell(cell);
8203             return c.getColumnElementsByCell();
8204         },
8205
8206         canMerge: function(cell1, cell2) {
8207             var c = new TableModifyerByCell(cell1);
8208             return c.canMerge(cell2);
8209         }
8210     };
8211
8212
8213
8214 })(wysihtml5);
8215 ;// does a selector query on element or array of elements
8216
8217 wysihtml5.dom.query = function(elements, query) {
8218     var ret = [],
8219         q;
8220
8221     if (elements.nodeType) {
8222         elements = [elements];
8223     }
8224
8225     for (var e = 0, len = elements.length; e < len; e++) {
8226         q = elements[e].querySelectorAll(query);
8227         if (q) {
8228             for(var i = q.length; i--; ret.unshift(q[i]));
8229         }
8230     }
8231     return ret;
8232 };
8233 ;wysihtml5.dom.compareDocumentPosition = (function() {
8234   var documentElement = document.documentElement;
8235   if (documentElement.compareDocumentPosition) {
8236     return function(container, element) {
8237       return container.compareDocumentPosition(element);
8238     };
8239   } else {
8240     return function( container, element ) {
8241       // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
8242       var thisOwner, otherOwner;
8243
8244       if( container.nodeType === 9) // Node.DOCUMENT_NODE
8245         thisOwner = container;
8246       else
8247         thisOwner = container.ownerDocument;
8248
8249       if( element.nodeType === 9) // Node.DOCUMENT_NODE
8250         otherOwner = element;
8251       else
8252         otherOwner = element.ownerDocument;
8253
8254       if( container === element ) return 0;
8255       if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8256       if( container.ownerDocument === element ) return 2 + 8;  //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8257       if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
8258
8259       // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
8260       if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml5.lang.array(container.childNodes).indexOf( element ) !== -1)
8261         return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8262
8263       if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml5.lang.array(element.childNodes).indexOf( container ) !== -1)
8264         return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8265
8266       var point = container;
8267       var parents = [ ];
8268       var previous = null;
8269       while( point ) {
8270         if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8271         parents.push( point );
8272         point = point.parentNode;
8273       }
8274       point = element;
8275       previous = null;
8276       while( point ) {
8277         if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8278         var location_index = wysihtml5.lang.array(parents).indexOf( point );
8279         if( location_index !== -1) {
8280          var smallest_common_ancestor = parents[ location_index ];
8281          var this_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] );
8282          var other_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
8283          if( this_index > other_index ) {
8284                return 2; //Node.DOCUMENT_POSITION_PRECEDING;
8285          }
8286          else {
8287            return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
8288          }
8289         }
8290         previous = point;
8291         point = point.parentNode;
8292       }
8293       return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
8294     };
8295   }
8296 })();
8297 ;wysihtml5.dom.unwrap = function(node) {
8298   if (node.parentNode) {
8299     while (node.lastChild) {
8300       wysihtml5.dom.insert(node.lastChild).after(node);
8301     }
8302     node.parentNode.removeChild(node);
8303   }
8304 };;/* 
8305  * Methods for fetching pasted html before it gets inserted into content
8306 **/
8307
8308 /* Modern event.clipboardData driven approach.
8309  * Advantage is that it does not have to loose selection or modify dom to catch the data. 
8310  * IE does not support though.
8311 **/
8312 wysihtml5.dom.getPastedHtml = function(event) {
8313   var html;
8314   if (event.clipboardData) {
8315     if (wysihtml5.lang.array(event.clipboardData.types).contains('text/html')) {
8316       html = event.clipboardData.getData('text/html');
8317     } else if (wysihtml5.lang.array(event.clipboardData.types).contains('text/plain')) {
8318       html = wysihtml5.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
8319     }
8320   }
8321   return html;
8322 };
8323
8324 /* Older temprorary contenteditable as paste source catcher method for fallbacks */
8325 wysihtml5.dom.getPastedHtmlWithDiv = function (composer, f) {
8326   var selBookmark = composer.selection.getBookmark(),
8327       doc = composer.element.ownerDocument,
8328       cleanerDiv = doc.createElement('DIV');
8329   
8330   doc.body.appendChild(cleanerDiv);
8331
8332   cleanerDiv.style.width = "1px";
8333   cleanerDiv.style.height = "1px";
8334   cleanerDiv.style.overflow = "hidden";
8335
8336   cleanerDiv.setAttribute('contenteditable', 'true');
8337   cleanerDiv.focus();
8338
8339   setTimeout(function () {
8340     composer.selection.setBookmark(selBookmark);
8341     f(cleanerDiv.innerHTML);
8342     cleanerDiv.parentNode.removeChild(cleanerDiv);
8343   }, 0);
8344 };;/**
8345  * Fix most common html formatting misbehaviors of browsers implementation when inserting
8346  * content via copy & paste contentEditable
8347  *
8348  * @author Christopher Blum
8349  */
8350 wysihtml5.quirks.cleanPastedHTML = (function() {
8351
8352   var styleToRegex = function (styleStr) {
8353     var trimmedStr = wysihtml5.lang.string(styleStr).trim(),
8354         escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
8355
8356     return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
8357   };
8358
8359   var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
8360     var newRules = wysihtml5.lang.object(rules).clone(true),
8361         tag, style;
8362
8363     for (tag in newRules.tags) {
8364
8365       if (newRules.tags.hasOwnProperty(tag)) {
8366         if (newRules.tags[tag].keep_styles) {
8367           for (style in newRules.tags[tag].keep_styles) {
8368             if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
8369               if (exceptStyles[style]) {
8370                 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
8371               }
8372             }
8373           }
8374         }
8375       }
8376     }
8377
8378     return newRules;
8379   };
8380
8381   var pickRuleset = function(ruleset, html) {
8382     var pickedSet, defaultSet;
8383
8384     if (!ruleset) {
8385       return null;
8386     }
8387
8388     for (var i = 0, max = ruleset.length; i < max; i++) {
8389       if (!ruleset[i].condition) {
8390         defaultSet = ruleset[i].set;
8391       }
8392       if (ruleset[i].condition && ruleset[i].condition.test(html)) {
8393         return ruleset[i].set;
8394       }
8395     }
8396
8397     return defaultSet;
8398   };
8399
8400   return function(html, options) {
8401     var exceptStyles = {
8402           'color': wysihtml5.dom.getStyle("color").from(options.referenceNode),
8403           'fontSize': wysihtml5.dom.getStyle("font-size").from(options.referenceNode)
8404         },
8405         rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
8406         newHtml;
8407
8408     newHtml = wysihtml5.dom.parse(html, {
8409       "rules": rules,
8410       "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content
8411       "context": options.referenceNode.ownerDocument,
8412       "uneditableClass": options.uneditableClass,
8413       "clearInternals" : true, // don't paste temprorary selection and other markings
8414       "unjoinNbsps" : true
8415     });
8416
8417     return newHtml;
8418   };
8419
8420 })();;/**
8421  * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
8422  *
8423  * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
8424  * @exaple
8425  *    wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
8426  */
8427 wysihtml5.quirks.ensureProperClearing = (function() {
8428   var clearIfNecessary = function() {
8429     var element = this;
8430     setTimeout(function() {
8431       var innerHTML = element.innerHTML.toLowerCase();
8432       if (innerHTML == "<p>&nbsp;</p>" ||
8433           innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
8434         element.innerHTML = "";
8435       }
8436     }, 0);
8437   };
8438
8439   return function(composer) {
8440     wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
8441   };
8442 })();
8443 ;// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
8444 //
8445 // In Firefox this:
8446 //      var d = document.createElement("div");
8447 //      d.innerHTML ='<a href="~"></a>';
8448 //      d.innerHTML;
8449 // will result in:
8450 //      <a href="%7E"></a>
8451 // which is wrong
8452 (function(wysihtml5) {
8453   var TILDE_ESCAPED = "%7E";
8454   wysihtml5.quirks.getCorrectInnerHTML = function(element) {
8455     var innerHTML = element.innerHTML;
8456     if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
8457       return innerHTML;
8458     }
8459
8460     var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
8461         url,
8462         urlToSearch,
8463         length,
8464         i;
8465     for (i=0, length=elementsWithTilde.length; i<length; i++) {
8466       url         = elementsWithTilde[i].href || elementsWithTilde[i].src;
8467       urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
8468       innerHTML   = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
8469     }
8470     return innerHTML;
8471   };
8472 })(wysihtml5);
8473 ;/**
8474  * Force rerendering of a given element
8475  * Needed to fix display misbehaviors of IE
8476  *
8477  * @param {Element} element The element object which needs to be rerendered
8478  * @example
8479  *    wysihtml5.quirks.redraw(document.body);
8480  */
8481 (function(wysihtml5) {
8482   var CLASS_NAME = "wysihtml5-quirks-redraw";
8483
8484   wysihtml5.quirks.redraw = function(element) {
8485     wysihtml5.dom.addClass(element, CLASS_NAME);
8486     wysihtml5.dom.removeClass(element, CLASS_NAME);
8487
8488     // Following hack is needed for firefox to make sure that image resize handles are properly removed
8489     try {
8490       var doc = element.ownerDocument;
8491       doc.execCommand("italic", false, null);
8492       doc.execCommand("italic", false, null);
8493     } catch(e) {}
8494   };
8495 })(wysihtml5);
8496 ;wysihtml5.quirks.tableCellsSelection = function(editable, editor) {
8497
8498     var dom = wysihtml5.dom,
8499         select = {
8500             table: null,
8501             start: null,
8502             end: null,
8503             cells: null,
8504             select: selectCells
8505         },
8506         selection_class = "wysiwyg-tmp-selected-cell",
8507         moveHandler = null,
8508         upHandler = null;
8509
8510     function init () {
8511
8512         dom.observe(editable, "mousedown", function(event) {
8513           var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] });
8514           if (target) {
8515               handleSelectionMousedown(target);
8516           }
8517         });
8518
8519         return select;
8520     }
8521
8522     function handleSelectionMousedown (target) {
8523       select.start = target;
8524       select.end = target;
8525       select.cells = [target];
8526       select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
8527
8528       if (select.table) {
8529         removeCellSelections();
8530         dom.addClass(target, selection_class);
8531         moveHandler = dom.observe(editable, "mousemove", handleMouseMove);
8532         upHandler = dom.observe(editable, "mouseup", handleMouseUp);
8533         editor.fire("tableselectstart").fire("tableselectstart:composer");
8534       }
8535     }
8536
8537     // remove all selection classes
8538     function removeCellSelections () {
8539         if (editable) {
8540             var selectedCells = editable.querySelectorAll('.' + selection_class);
8541             if (selectedCells.length > 0) {
8542               for (var i = 0; i < selectedCells.length; i++) {
8543                   dom.removeClass(selectedCells[i], selection_class);
8544               }
8545             }
8546         }
8547     }
8548
8549     function addSelections (cells) {
8550       for (var i = 0; i < cells.length; i++) {
8551         dom.addClass(cells[i], selection_class);
8552       }
8553     }
8554
8555     function handleMouseMove (event) {
8556       var curTable = null,
8557           cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }),
8558           oldEnd;
8559
8560       if (cell && select.table && select.start) {
8561         curTable =  dom.getParentElement(cell, { nodeName: ["TABLE"] });
8562         if (curTable && curTable === select.table) {
8563           removeCellSelections();
8564           oldEnd = select.end;
8565           select.end = cell;
8566           select.cells = dom.table.getCellsBetween(select.start, cell);
8567           if (select.cells.length > 1) {
8568             editor.composer.selection.deselect();
8569           }
8570           addSelections(select.cells);
8571           if (select.end !== oldEnd) {
8572             editor.fire("tableselectchange").fire("tableselectchange:composer");
8573           }
8574         }
8575       }
8576     }
8577
8578     function handleMouseUp (event) {
8579       moveHandler.stop();
8580       upHandler.stop();
8581       editor.fire("tableselect").fire("tableselect:composer");
8582       setTimeout(function() {
8583         bindSideclick();
8584       },0);
8585     }
8586
8587     function bindSideclick () {
8588         var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) {
8589           sideClickHandler.stop();
8590           if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) {
8591               removeCellSelections();
8592               select.table = null;
8593               select.start = null;
8594               select.end = null;
8595               editor.fire("tableunselect").fire("tableunselect:composer");
8596           }
8597         });
8598     }
8599
8600     function selectCells (start, end) {
8601         select.start = start;
8602         select.end = end;
8603         select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
8604         selectedCells = dom.table.getCellsBetween(select.start, select.end);
8605         addSelections(selectedCells);
8606         bindSideclick();
8607         editor.fire("tableselect").fire("tableselect:composer");
8608     }
8609
8610     return init();
8611
8612 };
8613 ;(function(wysihtml5) {
8614   var RGBA_REGEX     = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
8615       RGB_REGEX      = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
8616       HEX6_REGEX     = /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
8617       HEX3_REGEX     = /^#([0-9a-f])([0-9a-f])([0-9a-f])/i;
8618
8619   var param_REGX = function (p) {
8620     return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+" , "gi");
8621   };
8622
8623   wysihtml5.quirks.styleParser = {
8624
8625     parseColor: function(stylesStr, paramName) {
8626       var paramRegex = param_REGX(paramName),
8627           params = stylesStr.match(paramRegex),
8628           radix = 10,
8629           str, colorMatch;
8630
8631       if (params) {
8632         for (var i = params.length; i--;) {
8633           params[i] = wysihtml5.lang.string(params[i].split(':')[1]).trim();
8634         }
8635         str = params[params.length-1];
8636
8637         if (RGBA_REGEX.test(str)) {
8638           colorMatch = str.match(RGBA_REGEX);
8639         } else if (RGB_REGEX.test(str)) {
8640           colorMatch = str.match(RGB_REGEX);
8641         } else if (HEX6_REGEX.test(str)) {
8642           colorMatch = str.match(HEX6_REGEX);
8643           radix = 16;
8644         } else if (HEX3_REGEX.test(str)) {
8645           colorMatch = str.match(HEX3_REGEX);
8646           colorMatch.shift();
8647           colorMatch.push(1);
8648           return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8649             return (idx < 3) ? (parseInt(d, 16) * 16) + parseInt(d, 16): parseFloat(d);
8650           });
8651         }
8652
8653         if (colorMatch) {
8654           colorMatch.shift();
8655           if (!colorMatch[3]) {
8656             colorMatch.push(1);
8657           }
8658           return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8659             return (idx < 3) ? parseInt(d, radix): parseFloat(d);
8660           });
8661         }
8662       }
8663       return false;
8664     },
8665
8666     unparseColor: function(val, props) {
8667       if (props) {
8668         if (props == "hex") {
8669           return (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
8670         } else if (props == "hash") {
8671           return "#" + (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
8672         } else if (props == "rgb") {
8673           return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
8674         } else if (props == "rgba") {
8675           return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
8676         } else if (props == "csv") {
8677           return  val[0] + "," + val[1] + "," + val[2] + "," + val[3];
8678         }
8679       }
8680
8681       if (val[3] && val[3] !== 1) {
8682         return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
8683       } else {
8684         return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
8685       }
8686     },
8687
8688     parseFontSize: function(stylesStr) {
8689       var params = stylesStr.match(param_REGX('font-size'));
8690       if (params) {
8691         return wysihtml5.lang.string(params[params.length - 1].split(':')[1]).trim();
8692       }
8693       return false;
8694     }
8695   };
8696
8697 })(wysihtml5);
8698 ;/**
8699  * Selection API
8700  *
8701  * @example
8702  *    var selection = new wysihtml5.Selection(editor);
8703  */
8704 (function(wysihtml5) {
8705   var dom = wysihtml5.dom;
8706
8707   function _getCumulativeOffsetTop(element) {
8708     var top = 0;
8709     if (element.parentNode) {
8710       do {
8711         top += element.offsetTop || 0;
8712         element = element.offsetParent;
8713       } while (element);
8714     }
8715     return top;
8716   }
8717
8718   // Provides the depth of ``descendant`` relative to ``ancestor``
8719   function getDepth(ancestor, descendant) {
8720       var ret = 0;
8721       while (descendant !== ancestor) {
8722           ret++;
8723           descendant = descendant.parentNode;
8724           if (!descendant)
8725               throw new Error("not a descendant of ancestor!");
8726       }
8727       return ret;
8728   }
8729
8730   // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
8731   // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
8732   function expandRangeToSurround(range) {
8733       if (range.canSurroundContents()) return;
8734
8735       var common = range.commonAncestorContainer,
8736           start_depth = getDepth(common, range.startContainer),
8737           end_depth = getDepth(common, range.endContainer);
8738
8739       while(!range.canSurroundContents()) {
8740         // In the following branches, we cannot just decrement the depth variables because the setStartBefore/setEndAfter may move the start or end of the range more than one level relative to ``common``. So we need to recompute the depth.
8741         if (start_depth > end_depth) {
8742             range.setStartBefore(range.startContainer);
8743             start_depth = getDepth(common, range.startContainer);
8744         }
8745         else {
8746             range.setEndAfter(range.endContainer);
8747             end_depth = getDepth(common, range.endContainer);
8748         }
8749       }
8750   }
8751
8752   wysihtml5.Selection = Base.extend(
8753     /** @scope wysihtml5.Selection.prototype */ {
8754     constructor: function(editor, contain, unselectableClass) {
8755       // Make sure that our external range library is initialized
8756       window.rangy.init();
8757
8758       this.editor   = editor;
8759       this.composer = editor.composer;
8760       this.doc      = this.composer.doc;
8761       this.contain = contain;
8762       this.unselectableClass = unselectableClass || false;
8763     },
8764
8765     /**
8766      * Get the current selection as a bookmark to be able to later restore it
8767      *
8768      * @return {Object} An object that represents the current selection
8769      */
8770     getBookmark: function() {
8771       var range = this.getRange();
8772       if (range) expandRangeToSurround(range);
8773       return range && range.cloneRange();
8774     },
8775
8776     /**
8777      * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
8778      *
8779      * @param {Object} bookmark An object that represents the current selection
8780      */
8781     setBookmark: function(bookmark) {
8782       if (!bookmark) {
8783         return;
8784       }
8785
8786       this.setSelection(bookmark);
8787     },
8788
8789     /**
8790      * Set the caret in front of the given node
8791      *
8792      * @param {Object} node The element or text node where to position the caret in front of
8793      * @example
8794      *    selection.setBefore(myElement);
8795      */
8796     setBefore: function(node) {
8797       var range = rangy.createRange(this.doc);
8798       range.setStartBefore(node);
8799       range.setEndBefore(node);
8800       return this.setSelection(range);
8801     },
8802
8803     /**
8804      * Set the caret after the given node
8805      *
8806      * @param {Object} node The element or text node where to position the caret in front of
8807      * @example
8808      *    selection.setBefore(myElement);
8809      */
8810     setAfter: function(node) {
8811       var range = rangy.createRange(this.doc);
8812
8813       range.setStartAfter(node);
8814       range.setEndAfter(node);
8815       return this.setSelection(range);
8816     },
8817
8818     /**
8819      * Ability to select/mark nodes
8820      *
8821      * @param {Element} node The node/element to select
8822      * @example
8823      *    selection.selectNode(document.getElementById("my-image"));
8824      */
8825     selectNode: function(node, avoidInvisibleSpace) {
8826       var range           = rangy.createRange(this.doc),
8827           isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
8828           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
8829           content         = isElement ? node.innerHTML : node.data,
8830           isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
8831           displayStyle    = dom.getStyle("display").from(node),
8832           isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
8833
8834       if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
8835         // Make sure that caret is visible in node by inserting a zero width no breaking space
8836         try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
8837       }
8838
8839       if (canHaveHTML) {
8840         range.selectNodeContents(node);
8841       } else {
8842         range.selectNode(node);
8843       }
8844
8845       if (canHaveHTML && isEmpty && isElement) {
8846         range.collapse(isBlockElement);
8847       } else if (canHaveHTML && isEmpty) {
8848         range.setStartAfter(node);
8849         range.setEndAfter(node);
8850       }
8851
8852       this.setSelection(range);
8853     },
8854
8855     /**
8856      * Get the node which contains the selection
8857      *
8858      * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
8859      * @return {Object} The node that contains the caret
8860      * @example
8861      *    var nodeThatContainsCaret = selection.getSelectedNode();
8862      */
8863     getSelectedNode: function(controlRange) {
8864       var selection,
8865           range;
8866
8867       if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
8868         range = this.doc.selection.createRange();
8869         if (range && range.length) {
8870           return range.item(0);
8871         }
8872       }
8873
8874       selection = this.getSelection(this.doc);
8875       if (selection.focusNode === selection.anchorNode) {
8876         return selection.focusNode;
8877       } else {
8878         range = this.getRange(this.doc);
8879         return range ? range.commonAncestorContainer : this.doc.body;
8880       }
8881     },
8882
8883     fixSelBorders: function() {
8884       var range = this.getRange();
8885       expandRangeToSurround(range);
8886       this.setSelection(range);
8887     },
8888
8889     getSelectedOwnNodes: function(controlRange) {
8890       var selection,
8891           ranges = this.getOwnRanges(),
8892           ownNodes = [];
8893
8894       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
8895           ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
8896       }
8897       return ownNodes;
8898     },
8899
8900     findNodesInSelection: function(nodeTypes) {
8901       var ranges = this.getOwnRanges(),
8902           nodes = [], curNodes;
8903       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
8904         curNodes = ranges[i].getNodes([1], function(node) {
8905             return wysihtml5.lang.array(nodeTypes).contains(node.nodeName);
8906         });
8907         nodes = nodes.concat(curNodes);
8908       }
8909       return nodes;
8910     },
8911
8912     containsUneditable: function() {
8913       var uneditables = this.getOwnUneditables(),
8914           selection = this.getSelection();
8915
8916       for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
8917         if (selection.containsNode(uneditables[i])) {
8918           return true;
8919         }
8920       }
8921
8922       return false;
8923     },
8924
8925     deleteContents: function()  {
8926       var ranges = this.getOwnRanges();
8927       for (var i = ranges.length; i--;) {
8928         ranges[i].deleteContents();
8929       }
8930       this.setSelection(ranges[0]);
8931     },
8932
8933     getPreviousNode: function(node, ignoreEmpty) {
8934       if (!node) {
8935         var selection = this.getSelection();
8936         node = selection.anchorNode;
8937       }
8938
8939       if (node === this.contain) {
8940           return false;
8941       }
8942
8943       var ret = node.previousSibling,
8944           parent;
8945
8946       if (ret === this.contain) {
8947           return false;
8948       }
8949
8950       if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
8951          // do not count comments and other node types
8952          ret = this.getPreviousNode(ret, ignoreEmpty);
8953       } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
8954         // do not count empty textnodes as previus nodes
8955         ret = this.getPreviousNode(ret, ignoreEmpty);
8956       } else if (ignoreEmpty && ret && ret.nodeType === 1 && !wysihtml5.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) && (/^[\s]*$/).test(ret.innerHTML)) {
8957         // Do not count empty nodes if param set.
8958         // Contenteditable tends to bypass and delete these silently when deleting with caret
8959         ret = this.getPreviousNode(ret, ignoreEmpty);
8960       } else if (!ret && node !== this.contain) {
8961         parent = node.parentNode;
8962         if (parent !== this.contain) {
8963             ret = this.getPreviousNode(parent, ignoreEmpty);
8964         }
8965       }
8966
8967       return (ret !== this.contain) ? ret : false;
8968     },
8969
8970     getSelectionParentsByTag: function(tagName) {
8971       var nodes = this.getSelectedOwnNodes(),
8972           curEl, parents = [];
8973
8974       for (var i = 0, maxi = nodes.length; i < maxi; i++) {
8975         curEl = (nodes[i].nodeName &&  nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { nodeName: ['LI']}, false, this.contain);
8976         if (curEl) {
8977           parents.push(curEl);
8978         }
8979       }
8980       return (parents.length) ? parents : null;
8981     },
8982
8983     getRangeToNodeEnd: function() {
8984       if (this.isCollapsed()) {
8985         var range = this.getRange(),
8986             sNode = range.startContainer,
8987             pos = range.startOffset,
8988             lastR = rangy.createRange(this.doc);
8989
8990         lastR.selectNodeContents(sNode);
8991         lastR.setStart(sNode, pos);
8992         return lastR;
8993       }
8994     },
8995
8996     caretIsLastInSelection: function() {
8997       var r = rangy.createRange(this.doc),
8998           s = this.getSelection(),
8999           endc = this.getRangeToNodeEnd().cloneContents(),
9000           endtxt = endc.textContent;
9001
9002       return (/^\s*$/).test(endtxt);
9003     },
9004
9005     caretIsFirstInSelection: function() {
9006       var r = rangy.createRange(this.doc),
9007           s = this.getSelection(),
9008           range = this.getRange(),
9009           startNode = range.startContainer;
9010       
9011       if (startNode.nodeType === wysihtml5.TEXT_NODE) {
9012         return this.isCollapsed() && (startNode.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/).test(startNode.data.substr(0,range.startOffset)));
9013       } else {
9014         r.selectNodeContents(this.getRange().commonAncestorContainer);
9015         r.collapse(true);
9016         return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
9017       }
9018     },
9019
9020     caretIsInTheBeginnig: function(ofNode) {
9021         var selection = this.getSelection(),
9022             node = selection.anchorNode,
9023             offset = selection.anchorOffset;
9024         if (ofNode) {
9025           return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1)));
9026         } else {
9027           return (offset === 0 && !this.getPreviousNode(node, true));
9028         }
9029     },
9030
9031     caretIsBeforeUneditable: function() {
9032       var selection = this.getSelection(),
9033           node = selection.anchorNode,
9034           offset = selection.anchorOffset;
9035
9036       if (offset === 0) {
9037         var prevNode = this.getPreviousNode(node, true);
9038         if (prevNode) {
9039           var uneditables = this.getOwnUneditables();
9040           for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
9041             if (prevNode === uneditables[i]) {
9042               return uneditables[i];
9043             }
9044           }
9045         }
9046       }
9047       return false;
9048     },
9049
9050     // TODO: Figure out a method from following 2 that would work universally
9051     executeAndRestoreRangy: function(method, restoreScrollPosition) {
9052       var win = this.doc.defaultView || this.doc.parentWindow,
9053           sel = rangy.saveSelection(win);
9054
9055       if (!sel) {
9056         method();
9057       } else {
9058         try {
9059           method();
9060         } catch(e) {
9061           setTimeout(function() { throw e; }, 0);
9062         }
9063       }
9064       rangy.restoreSelection(sel);
9065     },
9066
9067     // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
9068     executeAndRestore: function(method, restoreScrollPosition) {
9069       var body                  = this.doc.body,
9070           oldScrollTop          = restoreScrollPosition && body.scrollTop,
9071           oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
9072           className             = "_wysihtml5-temp-placeholder",
9073           placeholderHtml       = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
9074           range                 = this.getRange(true),
9075           caretPlaceholder,
9076           newCaretPlaceholder,
9077           nextSibling, prevSibling,
9078           node, node2, range2,
9079           newRange;
9080
9081       // Nothing selected, execute and say goodbye
9082       if (!range) {
9083         method(body, body);
9084         return;
9085       }
9086
9087       if (!range.collapsed) {
9088         range2 = range.cloneRange();
9089         node2 = range2.createContextualFragment(placeholderHtml);
9090         range2.collapse(false);
9091         range2.insertNode(node2);
9092         range2.detach();
9093       }
9094
9095       node = range.createContextualFragment(placeholderHtml);
9096       range.insertNode(node);
9097
9098       if (node2) {
9099         caretPlaceholder = this.contain.querySelectorAll("." + className);
9100         range.setStartBefore(caretPlaceholder[0]);
9101         range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
9102       }
9103       this.setSelection(range);
9104
9105       // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
9106       try {
9107         method(range.startContainer, range.endContainer);
9108       } catch(e) {
9109         setTimeout(function() { throw e; }, 0);
9110       }
9111       caretPlaceholder = this.contain.querySelectorAll("." + className);
9112       if (caretPlaceholder && caretPlaceholder.length) {
9113         newRange = rangy.createRange(this.doc);
9114         nextSibling = caretPlaceholder[0].nextSibling;
9115         if (caretPlaceholder.length > 1) {
9116           prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
9117         }
9118         if (prevSibling && nextSibling) {
9119           newRange.setStartBefore(nextSibling);
9120           newRange.setEndAfter(prevSibling);
9121         } else {
9122           newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
9123           dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
9124           newRange.setStartBefore(newCaretPlaceholder);
9125           newRange.setEndAfter(newCaretPlaceholder);
9126         }
9127         this.setSelection(newRange);
9128         for (var i = caretPlaceholder.length; i--;) {
9129          caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
9130         }
9131
9132       } else {
9133         // fallback for when all hell breaks loose
9134         this.contain.focus();
9135       }
9136
9137       if (restoreScrollPosition) {
9138         body.scrollTop  = oldScrollTop;
9139         body.scrollLeft = oldScrollLeft;
9140       }
9141
9142       // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
9143       try {
9144         caretPlaceholder.parentNode.removeChild(caretPlaceholder);
9145       } catch(e2) {}
9146     },
9147
9148     set: function(node, offset) {
9149       var newRange = rangy.createRange(this.doc);
9150       newRange.setStart(node, offset || 0);
9151       this.setSelection(newRange);
9152     },
9153
9154     /**
9155      * Insert html at the caret position and move the cursor after the inserted html
9156      *
9157      * @param {String} html HTML string to insert
9158      * @example
9159      *    selection.insertHTML("<p>foobar</p>");
9160      */
9161     insertHTML: function(html) {
9162       var range     = rangy.createRange(this.doc),
9163           node = this.doc.createElement('DIV'),
9164           fragment = this.doc.createDocumentFragment(),
9165           lastChild;
9166
9167       node.innerHTML = html;
9168       lastChild = node.lastChild;
9169
9170       while (node.firstChild) {
9171         fragment.appendChild(node.firstChild);
9172       }
9173       this.insertNode(fragment);
9174
9175       if (lastChild) {
9176         this.setAfter(lastChild);
9177       }
9178     },
9179
9180     /**
9181      * Insert a node at the caret position and move the cursor behind it
9182      *
9183      * @param {Object} node HTML string to insert
9184      * @example
9185      *    selection.insertNode(document.createTextNode("foobar"));
9186      */
9187     insertNode: function(node) {
9188       var range = this.getRange();
9189       if (range) {
9190         range.insertNode(node);
9191       }
9192     },
9193
9194     /**
9195      * Wraps current selection with the given node
9196      *
9197      * @param {Object} node The node to surround the selected elements with
9198      */
9199     surround: function(nodeOptions) {
9200       var ranges = this.getOwnRanges(),
9201           node, nodes = [];
9202       if (ranges.length == 0) {
9203         return nodes;
9204       }
9205
9206       for (var i = ranges.length; i--;) {
9207         node = this.doc.createElement(nodeOptions.nodeName);
9208         nodes.push(node);
9209         if (nodeOptions.className) {
9210           node.className = nodeOptions.className;
9211         }
9212         if (nodeOptions.cssStyle) {
9213           node.setAttribute('style', nodeOptions.cssStyle);
9214         }
9215         try {
9216           // This only works when the range boundaries are not overlapping other elements
9217           ranges[i].surroundContents(node);
9218           this.selectNode(node);
9219         } catch(e) {
9220           // fallback
9221           node.appendChild(ranges[i].extractContents());
9222           ranges[i].insertNode(node);
9223         }
9224       }
9225       return nodes;
9226     },
9227
9228     deblockAndSurround: function(nodeOptions) {
9229       var tempElement = this.doc.createElement('div'),
9230           range = rangy.createRange(this.doc),
9231           tempDivElements,
9232           tempElements,
9233           firstChild;
9234
9235       tempElement.className = nodeOptions.className;
9236
9237       this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className);
9238       tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className);
9239       if (tempDivElements[0]) {
9240         tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]);
9241
9242         range.setStartBefore(tempDivElements[0]);
9243         range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
9244         tempElements = range.extractContents();
9245
9246         while (tempElements.firstChild) {
9247           firstChild = tempElements.firstChild;
9248           if (firstChild.nodeType == 1 && wysihtml5.dom.hasClass(firstChild, nodeOptions.className)) {
9249             while (firstChild.firstChild) {
9250               tempElement.appendChild(firstChild.firstChild);
9251             }
9252             if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
9253             tempElements.removeChild(firstChild);
9254           } else {
9255             tempElement.appendChild(firstChild);
9256           }
9257         }
9258       } else {
9259         tempElement = null;
9260       }
9261
9262       return tempElement;
9263     },
9264
9265     /**
9266      * Scroll the current caret position into the view
9267      * FIXME: This is a bit hacky, there might be a smarter way of doing this
9268      *
9269      * @example
9270      *    selection.scrollIntoView();
9271      */
9272     scrollIntoView: function() {
9273       var doc           = this.doc,
9274           tolerance     = 5, // px
9275           hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
9276           tempElement   = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
9277             var element = doc.createElement("span");
9278             // The element needs content in order to be able to calculate it's position properly
9279             element.innerHTML = wysihtml5.INVISIBLE_SPACE;
9280             return element;
9281           })(),
9282           offsetTop;
9283
9284       if (hasScrollBars) {
9285         this.insertNode(tempElement);
9286         offsetTop = _getCumulativeOffsetTop(tempElement);
9287         tempElement.parentNode.removeChild(tempElement);
9288         if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
9289           doc.body.scrollTop = offsetTop;
9290         }
9291       }
9292     },
9293
9294     /**
9295      * Select line where the caret is in
9296      */
9297     selectLine: function() {
9298       if (wysihtml5.browser.supportsSelectionModify()) {
9299         this._selectLine_W3C();
9300       } else if (this.doc.selection) {
9301         this._selectLine_MSIE();
9302       }
9303     },
9304
9305     /**
9306      * See https://developer.mozilla.org/en/DOM/Selection/modify
9307      */
9308     _selectLine_W3C: function() {
9309       var win       = this.doc.defaultView,
9310           selection = win.getSelection();
9311       selection.modify("move", "left", "lineboundary");
9312       selection.modify("extend", "right", "lineboundary");
9313     },
9314
9315     _selectLine_MSIE: function() {
9316       var range       = this.doc.selection.createRange(),
9317           rangeTop    = range.boundingTop,
9318           scrollWidth = this.doc.body.scrollWidth,
9319           rangeBottom,
9320           rangeEnd,
9321           measureNode,
9322           i,
9323           j;
9324
9325       if (!range.moveToPoint) {
9326         return;
9327       }
9328
9329       if (rangeTop === 0) {
9330         // Don't know why, but when the selection ends at the end of a line
9331         // range.boundingTop is 0
9332         measureNode = this.doc.createElement("span");
9333         this.insertNode(measureNode);
9334         rangeTop = measureNode.offsetTop;
9335         measureNode.parentNode.removeChild(measureNode);
9336       }
9337
9338       rangeTop += 1;
9339
9340       for (i=-10; i<scrollWidth; i+=2) {
9341         try {
9342           range.moveToPoint(i, rangeTop);
9343           break;
9344         } catch(e1) {}
9345       }
9346
9347       // Investigate the following in order to handle multi line selections
9348       // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
9349       rangeBottom = rangeTop;
9350       rangeEnd = this.doc.selection.createRange();
9351       for (j=scrollWidth; j>=0; j--) {
9352         try {
9353           rangeEnd.moveToPoint(j, rangeBottom);
9354           break;
9355         } catch(e2) {}
9356       }
9357
9358       range.setEndPoint("EndToEnd", rangeEnd);
9359       range.select();
9360     },
9361
9362     getText: function() {
9363       var selection = this.getSelection();
9364       return selection ? selection.toString() : "";
9365     },
9366
9367     getNodes: function(nodeType, filter) {
9368       var range = this.getRange();
9369       if (range) {
9370         return range.getNodes([nodeType], filter);
9371       } else {
9372         return [];
9373       }
9374     },
9375
9376     fixRangeOverflow: function(range) {
9377       if (this.contain && this.contain.firstChild && range) {
9378         var containment = range.compareNode(this.contain);
9379         if (containment !== 2) {
9380           if (containment === 1) {
9381             range.setStartBefore(this.contain.firstChild);
9382           }
9383           if (containment === 0) {
9384             range.setEndAfter(this.contain.lastChild);
9385           }
9386           if (containment === 3) {
9387             range.setStartBefore(this.contain.firstChild);
9388             range.setEndAfter(this.contain.lastChild);
9389           }
9390         } else if (this._detectInlineRangeProblems(range)) {
9391           var previousElementSibling = range.endContainer.previousElementSibling;
9392           if (previousElementSibling) {
9393             range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
9394           }
9395         }
9396       }
9397     },
9398
9399     _endOffsetForNode: function(node) {
9400       var range = document.createRange();
9401       range.selectNodeContents(node);
9402       return range.endOffset;
9403     },
9404
9405     _detectInlineRangeProblems: function(range) {
9406       var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
9407       return (
9408         range.endOffset == 0 &&
9409         position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
9410       );
9411     },
9412
9413     getRange: function(dontFix) {
9414       var selection = this.getSelection(),
9415           range = selection && selection.rangeCount && selection.getRangeAt(0);
9416
9417       if (dontFix !== true) {
9418         this.fixRangeOverflow(range);
9419       }
9420
9421       return range;
9422     },
9423
9424     getOwnUneditables: function() {
9425       var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
9426           deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
9427
9428       return wysihtml5.lang.array(allUneditables).without(deepUneditables);
9429     },
9430
9431     // Returns an array of ranges that belong only to this editable
9432     // Needed as uneditable block in contenteditabel can split range into pieces
9433     // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
9434     getOwnRanges: function()  {
9435       var ranges = [],
9436           r = this.getRange(),
9437           tmpRanges;
9438
9439       if (r) { ranges.push(r); }
9440
9441       if (this.unselectableClass && this.contain && r) {
9442           var uneditables = this.getOwnUneditables(),
9443               tmpRange;
9444           if (uneditables.length > 0) {
9445             for (var i = 0, imax = uneditables.length; i < imax; i++) {
9446               tmpRanges = [];
9447               for (var j = 0, jmax = ranges.length; j < jmax; j++) {
9448                 if (ranges[j]) {
9449                   switch (ranges[j].compareNode(uneditables[i])) {
9450                     case 2:
9451                       // all selection inside uneditable. remove
9452                     break;
9453                     case 3:
9454                       //section begins before and ends after uneditable. spilt
9455                       tmpRange = ranges[j].cloneRange();
9456                       tmpRange.setEndBefore(uneditables[i]);
9457                       tmpRanges.push(tmpRange);
9458
9459                       tmpRange = ranges[j].cloneRange();
9460                       tmpRange.setStartAfter(uneditables[i]);
9461                       tmpRanges.push(tmpRange);
9462                     break;
9463                     default:
9464                       // in all other cases uneditable does not touch selection. dont modify
9465                       tmpRanges.push(ranges[j]);
9466                   }
9467                 }
9468                 ranges = tmpRanges;
9469               }
9470             }
9471           }
9472       }
9473       return ranges;
9474     },
9475
9476     getSelection: function() {
9477       return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
9478     },
9479
9480     setSelection: function(range) {
9481       var win       = this.doc.defaultView || this.doc.parentWindow,
9482           selection = rangy.getSelection(win);
9483       return selection.setSingleRange(range);
9484     },
9485
9486     createRange: function() {
9487       return rangy.createRange(this.doc);
9488     },
9489
9490     isCollapsed: function() {
9491         return this.getSelection().isCollapsed;
9492     },
9493
9494     getHtml: function() {
9495       return this.getSelection().toHtml();
9496     },
9497
9498     isEndToEndInNode: function(nodeNames) {
9499       var range = this.getRange(),
9500           parentElement = range.commonAncestorContainer,
9501           startNode = range.startContainer,
9502           endNode = range.endContainer;
9503
9504
9505         if (parentElement.nodeType === wysihtml5.TEXT_NODE) {
9506           parentElement = parentElement.parentNode;
9507         }
9508
9509         if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
9510           return false;
9511         }
9512
9513         if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
9514           return false;
9515         }
9516
9517         while (startNode && startNode !== parentElement) {
9518           if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) {
9519             return false;
9520           }
9521           if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
9522             return false;
9523           }
9524           startNode = startNode.parentNode;
9525         }
9526
9527         while (endNode && endNode !== parentElement) {
9528           if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) {
9529             return false;
9530           }
9531           if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
9532             return false;
9533           }
9534           endNode = endNode.parentNode;
9535         }
9536
9537         return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
9538     },
9539
9540     deselect: function() {
9541       var sel = this.getSelection();
9542       sel && sel.removeAllRanges();
9543     }
9544   });
9545
9546 })(wysihtml5);
9547 ;/**
9548  * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
9549  * http://code.google.com/p/rangy/
9550  *
9551  * changed in order to be able ...
9552  *    - to use custom tags
9553  *    - to detect and replace similar css classes via reg exp
9554  */
9555 (function(wysihtml5, rangy) {
9556   var defaultTagName = "span";
9557
9558   var REG_EXP_WHITE_SPACE = /\s+/g;
9559
9560   function hasClass(el, cssClass, regExp) {
9561     if (!el.className) {
9562       return false;
9563     }
9564
9565     var matchingClassNames = el.className.match(regExp) || [];
9566     return matchingClassNames[matchingClassNames.length - 1] === cssClass;
9567   }
9568
9569   function hasStyleAttr(el, regExp) {
9570     if (!el.getAttribute || !el.getAttribute('style')) {
9571       return false;
9572     }
9573     var matchingStyles = el.getAttribute('style').match(regExp);
9574     return  (el.getAttribute('style').match(regExp)) ? true : false;
9575   }
9576
9577   function addStyle(el, cssStyle, regExp) {
9578     if (el.getAttribute('style')) {
9579       removeStyle(el, regExp);
9580       if (el.getAttribute('style') && !(/^\s*$/).test(el.getAttribute('style'))) {
9581         el.setAttribute('style', cssStyle + ";" + el.getAttribute('style'));
9582       } else {
9583         el.setAttribute('style', cssStyle);
9584       }
9585     } else {
9586       el.setAttribute('style', cssStyle);
9587     }
9588   }
9589
9590   function addClass(el, cssClass, regExp) {
9591     if (el.className) {
9592       removeClass(el, regExp);
9593       el.className += " " + cssClass;
9594     } else {
9595       el.className = cssClass;
9596     }
9597   }
9598
9599   function removeClass(el, regExp) {
9600     if (el.className) {
9601       el.className = el.className.replace(regExp, "");
9602     }
9603   }
9604
9605   function removeStyle(el, regExp) {
9606     var s,
9607         s2 = [];
9608     if (el.getAttribute('style')) {
9609       s = el.getAttribute('style').split(';');
9610       for (var i = s.length; i--;) {
9611         if (!s[i].match(regExp) && !(/^\s*$/).test(s[i])) {
9612           s2.push(s[i]);
9613         }
9614       }
9615       if (s2.length) {
9616         el.setAttribute('style', s2.join(';'));
9617       } else {
9618         el.removeAttribute('style');
9619       }
9620     }
9621   }
9622
9623   function getMatchingStyleRegexp(el, style) {
9624     var regexes = [],
9625         sSplit = style.split(';'),
9626         elStyle = el.getAttribute('style');
9627
9628     if (elStyle) {
9629       elStyle = elStyle.replace(/\s/gi, '').toLowerCase();
9630       regexes.push(new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
9631
9632       for (var i = sSplit.length; i-- > 0;) {
9633         if (!(/^\s*$/).test(sSplit[i])) {
9634           regexes.push(new RegExp("(^|\\s|;)" + sSplit[i].replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
9635         }
9636       }
9637       for (var j = 0, jmax = regexes.length; j < jmax; j++) {
9638         if (elStyle.match(regexes[j])) {
9639           return regexes[j];
9640         }
9641       }
9642     }
9643
9644     return false;
9645   }
9646
9647   function isMatchingAllready(node, tags, style, className) {
9648     if (style) {
9649       return getMatchingStyleRegexp(node, style);
9650     } else if (className) {
9651       return wysihtml5.dom.hasClass(node, className);
9652     } else {
9653       return rangy.dom.arrayContains(tags, node.tagName.toLowerCase());
9654     }
9655   }
9656
9657   function areMatchingAllready(nodes, tags, style, className) {
9658     for (var i = nodes.length; i--;) {
9659       if (!isMatchingAllready(nodes[i], tags, style, className)) {
9660         return false;
9661       }
9662     }
9663     return nodes.length ? true : false;
9664   }
9665
9666   function removeOrChangeStyle(el, style, regExp) {
9667
9668     var exactRegex = getMatchingStyleRegexp(el, style);
9669     if (exactRegex) {
9670       // adding same style value on property again removes style
9671       removeStyle(el, exactRegex);
9672       return "remove";
9673     } else {
9674       // adding new style value changes value
9675       addStyle(el, style, regExp);
9676       return "change";
9677     }
9678   }
9679
9680   function hasSameClasses(el1, el2) {
9681     return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
9682   }
9683
9684   function replaceWithOwnChildren(el) {
9685     var parent = el.parentNode;
9686     while (el.firstChild) {
9687       parent.insertBefore(el.firstChild, el);
9688     }
9689     parent.removeChild(el);
9690   }
9691
9692   function elementsHaveSameNonClassAttributes(el1, el2) {
9693     if (el1.attributes.length != el2.attributes.length) {
9694       return false;
9695     }
9696     for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
9697       attr1 = el1.attributes[i];
9698       name = attr1.name;
9699       if (name != "class") {
9700         attr2 = el2.attributes.getNamedItem(name);
9701         if (attr1.specified != attr2.specified) {
9702           return false;
9703         }
9704         if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
9705           return false;
9706         }
9707       }
9708     }
9709     return true;
9710   }
9711
9712   function isSplitPoint(node, offset) {
9713     if (rangy.dom.isCharacterDataNode(node)) {
9714       if (offset == 0) {
9715         return !!node.previousSibling;
9716       } else if (offset == node.length) {
9717         return !!node.nextSibling;
9718       } else {
9719         return true;
9720       }
9721     }
9722
9723     return offset > 0 && offset < node.childNodes.length;
9724   }
9725
9726   function splitNodeAt(node, descendantNode, descendantOffset, container) {
9727     var newNode;
9728     if (rangy.dom.isCharacterDataNode(descendantNode)) {
9729       if (descendantOffset == 0) {
9730         descendantOffset = rangy.dom.getNodeIndex(descendantNode);
9731         descendantNode = descendantNode.parentNode;
9732       } else if (descendantOffset == descendantNode.length) {
9733         descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
9734         descendantNode = descendantNode.parentNode;
9735       } else {
9736         newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
9737       }
9738     }
9739     if (!newNode) {
9740       if (!container || descendantNode !== container) {
9741
9742         newNode = descendantNode.cloneNode(false);
9743         if (newNode.id) {
9744           newNode.removeAttribute("id");
9745         }
9746         var child;
9747         while ((child = descendantNode.childNodes[descendantOffset])) {
9748           newNode.appendChild(child);
9749         }
9750         rangy.dom.insertAfter(newNode, descendantNode);
9751
9752       }
9753     }
9754     return (descendantNode == node) ? newNode :  splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode), container);
9755   }
9756
9757   function Merge(firstNode) {
9758     this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
9759     this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
9760     this.textNodes = [this.firstTextNode];
9761   }
9762
9763   Merge.prototype = {
9764     doMerge: function() {
9765       var textBits = [], textNode, parent, text;
9766       for (var i = 0, len = this.textNodes.length; i < len; ++i) {
9767         textNode = this.textNodes[i];
9768         parent = textNode.parentNode;
9769         textBits[i] = textNode.data;
9770         if (i) {
9771           parent.removeChild(textNode);
9772           if (!parent.hasChildNodes()) {
9773             parent.parentNode.removeChild(parent);
9774           }
9775         }
9776       }
9777       this.firstTextNode.data = text = textBits.join("");
9778       return text;
9779     },
9780
9781     getLength: function() {
9782       var i = this.textNodes.length, len = 0;
9783       while (i--) {
9784         len += this.textNodes[i].length;
9785       }
9786       return len;
9787     },
9788
9789     toString: function() {
9790       var textBits = [];
9791       for (var i = 0, len = this.textNodes.length; i < len; ++i) {
9792         textBits[i] = "'" + this.textNodes[i].data + "'";
9793       }
9794       return "[Merge(" + textBits.join(",") + ")]";
9795     }
9796   };
9797
9798   function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize, cssStyle, similarStyleRegExp, container) {
9799     this.tagNames = tagNames || [defaultTagName];
9800     this.cssClass = cssClass || ((cssClass === false) ? false : "");
9801     this.similarClassRegExp = similarClassRegExp;
9802     this.cssStyle = cssStyle || "";
9803     this.similarStyleRegExp = similarStyleRegExp;
9804     this.normalize = normalize;
9805     this.applyToAnyTagName = false;
9806     this.container = container;
9807   }
9808
9809   HTMLApplier.prototype = {
9810     getAncestorWithClass: function(node) {
9811       var cssClassMatch;
9812       while (node) {
9813         cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : (this.cssStyle !== "") ? false : true;
9814         if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" &&  rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
9815           return node;
9816         }
9817         node = node.parentNode;
9818       }
9819       return false;
9820     },
9821
9822     // returns parents of node with given style attribute
9823     getAncestorWithStyle: function(node) {
9824       var cssStyleMatch;
9825       while (node) {
9826         cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false;
9827
9828         if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) {
9829           return node;
9830         }
9831         node = node.parentNode;
9832       }
9833       return false;
9834     },
9835
9836     getMatchingAncestor: function(node) {
9837       var ancestor = this.getAncestorWithClass(node),
9838           matchType = false;
9839
9840       if (!ancestor) {
9841         ancestor = this.getAncestorWithStyle(node);
9842         if (ancestor) {
9843           matchType = "style";
9844         }
9845       } else {
9846         if (this.cssStyle) {
9847           matchType = "class";
9848         }
9849       }
9850
9851       return {
9852         "element": ancestor,
9853         "type": matchType
9854       };
9855     },
9856
9857     // Normalizes nodes after applying a CSS class to a Range.
9858     postApply: function(textNodes, range) {
9859       var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
9860
9861       var merges = [], currentMerge;
9862
9863       var rangeStartNode = firstNode, rangeEndNode = lastNode;
9864       var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
9865
9866       var textNode, precedingTextNode;
9867
9868       for (var i = 0, len = textNodes.length; i < len; ++i) {
9869         textNode = textNodes[i];
9870         precedingTextNode = null;
9871         if (textNode && textNode.parentNode) {
9872           precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
9873         }
9874         if (precedingTextNode) {
9875           if (!currentMerge) {
9876             currentMerge = new Merge(precedingTextNode);
9877             merges.push(currentMerge);
9878           }
9879           currentMerge.textNodes.push(textNode);
9880           if (textNode === firstNode) {
9881             rangeStartNode = currentMerge.firstTextNode;
9882             rangeStartOffset = rangeStartNode.length;
9883           }
9884           if (textNode === lastNode) {
9885             rangeEndNode = currentMerge.firstTextNode;
9886             rangeEndOffset = currentMerge.getLength();
9887           }
9888         } else {
9889           currentMerge = null;
9890         }
9891       }
9892       // Test whether the first node after the range needs merging
9893       if(lastNode && lastNode.parentNode) {
9894         var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
9895         if (nextTextNode) {
9896           if (!currentMerge) {
9897             currentMerge = new Merge(lastNode);
9898             merges.push(currentMerge);
9899           }
9900           currentMerge.textNodes.push(nextTextNode);
9901         }
9902       }
9903       // Do the merges
9904       if (merges.length) {
9905         for (i = 0, len = merges.length; i < len; ++i) {
9906           merges[i].doMerge();
9907         }
9908         // Set the range boundaries
9909         range.setStart(rangeStartNode, rangeStartOffset);
9910         range.setEnd(rangeEndNode, rangeEndOffset);
9911       }
9912     },
9913
9914     getAdjacentMergeableTextNode: function(node, forward) {
9915         var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
9916         var el = isTextNode ? node.parentNode : node;
9917         var adjacentNode;
9918         var propName = forward ? "nextSibling" : "previousSibling";
9919         if (isTextNode) {
9920           // Can merge if the node's previous/next sibling is a text node
9921           adjacentNode = node[propName];
9922           if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
9923             return adjacentNode;
9924           }
9925         } else {
9926           // Compare element with its sibling
9927           adjacentNode = el[propName];
9928           if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
9929             return adjacentNode[forward ? "firstChild" : "lastChild"];
9930           }
9931         }
9932         return null;
9933     },
9934
9935     areElementsMergeable: function(el1, el2) {
9936       return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
9937         && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
9938         && hasSameClasses(el1, el2)
9939         && elementsHaveSameNonClassAttributes(el1, el2);
9940     },
9941
9942     createContainer: function(doc) {
9943       var el = doc.createElement(this.tagNames[0]);
9944       if (this.cssClass) {
9945         el.className = this.cssClass;
9946       }
9947       if (this.cssStyle) {
9948         el.setAttribute('style', this.cssStyle);
9949       }
9950       return el;
9951     },
9952
9953     applyToTextNode: function(textNode) {
9954       var parent = textNode.parentNode;
9955       if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
9956
9957         if (this.cssClass) {
9958           addClass(parent, this.cssClass, this.similarClassRegExp);
9959         }
9960         if (this.cssStyle) {
9961           addStyle(parent, this.cssStyle, this.similarStyleRegExp);
9962         }
9963       } else {
9964         var el = this.createContainer(rangy.dom.getDocument(textNode));
9965         textNode.parentNode.insertBefore(el, textNode);
9966         el.appendChild(textNode);
9967       }
9968     },
9969
9970     isRemovable: function(el) {
9971       return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) &&
9972               wysihtml5.lang.string(el.className).trim() === "" &&
9973               (
9974                 !el.getAttribute('style') ||
9975                 wysihtml5.lang.string(el.getAttribute('style')).trim() === ""
9976               );
9977     },
9978
9979     undoToTextNode: function(textNode, range, ancestorWithClass, ancestorWithStyle) {
9980       var styleMode = (ancestorWithClass) ? false : true,
9981           ancestor = ancestorWithClass || ancestorWithStyle,
9982           styleChanged = false;
9983       if (!range.containsNode(ancestor)) {
9984         // Split out the portion of the ancestor from which we can remove the CSS class
9985         var ancestorRange = range.cloneRange();
9986             ancestorRange.selectNode(ancestor);
9987
9988         if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
9989             splitNodeAt(ancestor, range.endContainer, range.endOffset, this.container);
9990             range.setEndAfter(ancestor);
9991         }
9992         if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
9993             ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset, this.container);
9994         }
9995       }
9996
9997       if (!styleMode && this.similarClassRegExp) {
9998         removeClass(ancestor, this.similarClassRegExp);
9999       }
10000
10001       if (styleMode && this.similarStyleRegExp) {
10002         styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change");
10003       }
10004       if (this.isRemovable(ancestor) && !styleChanged) {
10005         replaceWithOwnChildren(ancestor);
10006       }
10007     },
10008
10009     applyToRange: function(range) {
10010         var textNodes;
10011         for (var ri = range.length; ri--;) {
10012             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10013
10014             if (!textNodes.length) {
10015               try {
10016                 var node = this.createContainer(range[ri].endContainer.ownerDocument);
10017                 range[ri].surroundContents(node);
10018                 this.selectNode(range[ri], node);
10019                 return;
10020               } catch(e) {}
10021             }
10022
10023             range[ri].splitBoundaries();
10024             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10025             if (textNodes.length) {
10026               var textNode;
10027
10028               for (var i = 0, len = textNodes.length; i < len; ++i) {
10029                 textNode = textNodes[i];
10030                 if (!this.getMatchingAncestor(textNode).element) {
10031                   this.applyToTextNode(textNode);
10032                 }
10033               }
10034
10035               range[ri].setStart(textNodes[0], 0);
10036               textNode = textNodes[textNodes.length - 1];
10037               range[ri].setEnd(textNode, textNode.length);
10038
10039               if (this.normalize) {
10040                 this.postApply(textNodes, range[ri]);
10041               }
10042             }
10043
10044         }
10045     },
10046
10047     undoToRange: function(range) {
10048       var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor;
10049       for (var ri = range.length; ri--;) {
10050
10051           textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10052           if (textNodes.length) {
10053             range[ri].splitBoundaries();
10054             textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10055           } else {
10056             var doc = range[ri].endContainer.ownerDocument,
10057                 node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
10058             range[ri].insertNode(node);
10059             range[ri].selectNode(node);
10060             textNodes = [node];
10061           }
10062
10063           for (var i = 0, len = textNodes.length; i < len; ++i) {
10064             if (range[ri].isValid()) {
10065               textNode = textNodes[i];
10066
10067               ancestor = this.getMatchingAncestor(textNode);
10068               if (ancestor.type === "style") {
10069                 this.undoToTextNode(textNode, range[ri], false, ancestor.element);
10070               } else if (ancestor.element) {
10071                 this.undoToTextNode(textNode, range[ri], ancestor.element);
10072               }
10073             }
10074           }
10075
10076           if (len == 1) {
10077             this.selectNode(range[ri], textNodes[0]);
10078           } else {
10079             range[ri].setStart(textNodes[0], 0);
10080             textNode = textNodes[textNodes.length - 1];
10081             range[ri].setEnd(textNode, textNode.length);
10082
10083             if (this.normalize) {
10084               this.postApply(textNodes, range[ri]);
10085             }
10086           }
10087
10088       }
10089     },
10090
10091     selectNode: function(range, node) {
10092       var isElement       = node.nodeType === wysihtml5.ELEMENT_NODE,
10093           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : true,
10094           content         = isElement ? node.innerHTML : node.data,
10095           isEmpty         = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
10096
10097       if (isEmpty && isElement && canHaveHTML) {
10098         // Make sure that caret is visible in node by inserting a zero width no breaking space
10099         try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
10100       }
10101       range.selectNodeContents(node);
10102       if (isEmpty && isElement) {
10103         range.collapse(false);
10104       } else if (isEmpty) {
10105         range.setStartAfter(node);
10106         range.setEndAfter(node);
10107       }
10108     },
10109
10110     getTextSelectedByRange: function(textNode, range) {
10111       var textRange = range.cloneRange();
10112       textRange.selectNodeContents(textNode);
10113
10114       var intersectionRange = textRange.intersection(range);
10115       var text = intersectionRange ? intersectionRange.toString() : "";
10116       textRange.detach();
10117
10118       return text;
10119     },
10120
10121     isAppliedToRange: function(range) {
10122       var ancestors = [],
10123           appliedType = "full",
10124           ancestor, styleAncestor, textNodes;
10125
10126       for (var ri = range.length; ri--;) {
10127
10128         textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10129         if (!textNodes.length) {
10130           ancestor = this.getMatchingAncestor(range[ri].startContainer).element;
10131
10132           return (ancestor) ? {
10133             "elements": [ancestor],
10134             "coverage": appliedType
10135           } : false;
10136         }
10137
10138         for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
10139           selectedText = this.getTextSelectedByRange(textNodes[i], range[ri]);
10140           ancestor = this.getMatchingAncestor(textNodes[i]).element;
10141           if (ancestor && selectedText != "") {
10142             ancestors.push(ancestor);
10143
10144             if (wysihtml5.dom.getTextNodes(ancestor, true).length === 1) {
10145               appliedType = "full";
10146             } else if (appliedType === "full") {
10147               appliedType = "inline";
10148             }
10149           } else if (!ancestor) {
10150             appliedType = "partial";
10151           }
10152         }
10153
10154       }
10155
10156       return (ancestors.length) ? {
10157         "elements": ancestors,
10158         "coverage": appliedType
10159       } : false;
10160     },
10161
10162     toggleRange: function(range) {
10163       var isApplied = this.isAppliedToRange(range),
10164           parentsExactMatch;
10165
10166       if (isApplied) {
10167         if (isApplied.coverage === "full") {
10168           this.undoToRange(range);
10169         } else if (isApplied.coverage === "inline") {
10170           parentsExactMatch = areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass);
10171           this.undoToRange(range);
10172           if (!parentsExactMatch) {
10173             this.applyToRange(range);
10174           }
10175         } else {
10176           // partial
10177           if (!areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass)) {
10178             this.undoToRange(range);
10179           }
10180           this.applyToRange(range);
10181         }
10182       } else {
10183         this.applyToRange(range);
10184       }
10185     }
10186   };
10187
10188   wysihtml5.selection.HTMLApplier = HTMLApplier;
10189
10190 })(wysihtml5, rangy);
10191 ;/**
10192  * Rich Text Query/Formatting Commands
10193  *
10194  * @example
10195  *    var commands = new wysihtml5.Commands(editor);
10196  */
10197 wysihtml5.Commands = Base.extend(
10198   /** @scope wysihtml5.Commands.prototype */ {
10199   constructor: function(editor) {
10200     this.editor   = editor;
10201     this.composer = editor.composer;
10202     this.doc      = this.composer.doc;
10203   },
10204
10205   /**
10206    * Check whether the browser supports the given command
10207    *
10208    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
10209    * @example
10210    *    commands.supports("createLink");
10211    */
10212   support: function(command) {
10213     return wysihtml5.browser.supportsCommand(this.doc, command);
10214   },
10215
10216   /**
10217    * Check whether the browser supports the given command
10218    *
10219    * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
10220    * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
10221    * @example
10222    *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
10223    */
10224   exec: function(command, value) {
10225     var obj     = wysihtml5.commands[command],
10226         args    = wysihtml5.lang.array(arguments).get(),
10227         method  = obj && obj.exec,
10228         result  = null;
10229
10230     this.editor.fire("beforecommand:composer");
10231
10232     if (method) {
10233       args.unshift(this.composer);
10234       result = method.apply(obj, args);
10235     } else {
10236       try {
10237         // try/catch for buggy firefox
10238         result = this.doc.execCommand(command, false, value);
10239       } catch(e) {}
10240     }
10241
10242     this.editor.fire("aftercommand:composer");
10243     return result;
10244   },
10245
10246   /**
10247    * Check whether the current command is active
10248    * If the caret is within a bold text, then calling this with command "bold" should return true
10249    *
10250    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
10251    * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
10252    * @return {Boolean} Whether the command is active
10253    * @example
10254    *    var isCurrentSelectionBold = commands.state("bold");
10255    */
10256   state: function(command, commandValue) {
10257     var obj     = wysihtml5.commands[command],
10258         args    = wysihtml5.lang.array(arguments).get(),
10259         method  = obj && obj.state;
10260     if (method) {
10261       args.unshift(this.composer);
10262       return method.apply(obj, args);
10263     } else {
10264       try {
10265         // try/catch for buggy firefox
10266         return this.doc.queryCommandState(command);
10267       } catch(e) {
10268         return false;
10269       }
10270     }
10271   },
10272
10273   /* Get command state parsed value if command has stateValue parsing function */
10274   stateValue: function(command) {
10275     var obj     = wysihtml5.commands[command],
10276         args    = wysihtml5.lang.array(arguments).get(),
10277         method  = obj && obj.stateValue;
10278     if (method) {
10279       args.unshift(this.composer);
10280       return method.apply(obj, args);
10281     } else {
10282       return false;
10283     }
10284   }
10285 });
10286 ;wysihtml5.commands.bold = {
10287   exec: function(composer, command) {
10288     wysihtml5.commands.formatInline.execWithToggle(composer, command, "b");
10289   },
10290
10291   state: function(composer, command) {
10292     // element.ownerDocument.queryCommandState("bold") results:
10293     // firefox: only <b>
10294     // chrome:  <b>, <strong>, <h1>, <h2>, ...
10295     // ie:      <b>, <strong>
10296     // opera:   <b>, <strong>
10297     return wysihtml5.commands.formatInline.state(composer, command, "b");
10298   }
10299 };
10300
10301 ;(function(wysihtml5) {
10302   var undef,
10303       NODE_NAME = "A",
10304       dom       = wysihtml5.dom;
10305
10306   function _format(composer, attributes) {
10307     var doc             = composer.doc,
10308         tempClass       = "_wysihtml5-temp-" + (+new Date()),
10309         tempClassRegExp = /non-matching-class/g,
10310         i               = 0,
10311         length,
10312         anchors,
10313         anchor,
10314         hasElementChild,
10315         isEmpty,
10316         elementToSetCaretAfter,
10317         textContent,
10318         whiteSpace,
10319         j;
10320     wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp, undef, undef, true, true);
10321     anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
10322     length  = anchors.length;
10323     for (; i<length; i++) {
10324       anchor = anchors[i];
10325       anchor.removeAttribute("class");
10326       for (j in attributes) {
10327         // Do not set attribute "text" as it is meant for setting string value if created link has no textual data
10328         if (j !== "text") {
10329           anchor.setAttribute(j, attributes[j]);
10330         }
10331       }
10332     }
10333
10334     elementToSetCaretAfter = anchor;
10335     if (length === 1) {
10336       textContent = dom.getTextContent(anchor);
10337       hasElementChild = !!anchor.querySelector("*");
10338       isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
10339       if (!hasElementChild && isEmpty) {
10340         dom.setTextContent(anchor, attributes.text || anchor.href);
10341         whiteSpace = doc.createTextNode(" ");
10342         composer.selection.setAfter(anchor);
10343         dom.insert(whiteSpace).after(anchor);
10344         elementToSetCaretAfter = whiteSpace;
10345       }
10346     }
10347     composer.selection.setAfter(elementToSetCaretAfter);
10348   }
10349
10350   // Changes attributes of links
10351   function _changeLinks(composer, anchors, attributes) {
10352     var oldAttrs;
10353     for (var a = anchors.length; a--;) {
10354
10355       // Remove all old attributes
10356       oldAttrs = anchors[a].attributes;
10357       for (var oa = oldAttrs.length; oa--;) {
10358         anchors[a].removeAttribute(oldAttrs.item(oa).name);
10359       }
10360
10361       // Set new attributes
10362       for (var j in attributes) {
10363         if (attributes.hasOwnProperty(j)) {
10364           anchors[a].setAttribute(j, attributes[j]);
10365         }
10366       }
10367
10368     }
10369   }
10370
10371   wysihtml5.commands.createLink = {
10372     /**
10373      * TODO: Use HTMLApplier or formatInline here
10374      *
10375      * Turns selection into a link
10376      * If selection is already a link, it just changes the attributes
10377      *
10378      * @example
10379      *    // either ...
10380      *    wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
10381      *    // ... or ...
10382      *    wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
10383      */
10384     exec: function(composer, command, value) {
10385       var anchors = this.state(composer, command);
10386       if (anchors) {
10387         // Selection contains links then change attributes of these links
10388         composer.selection.executeAndRestore(function() {
10389           _changeLinks(composer, anchors, value);
10390         });
10391       } else {
10392         // Create links
10393         value = typeof(value) === "object" ? value : { href: value };
10394         _format(composer, value);
10395       }
10396     },
10397
10398     state: function(composer, command) {
10399       return wysihtml5.commands.formatInline.state(composer, command, "A");
10400     }
10401   };
10402 })(wysihtml5);
10403 ;(function(wysihtml5) {
10404   var dom = wysihtml5.dom;
10405
10406   function _removeFormat(composer, anchors) {
10407     var length  = anchors.length,
10408         i       = 0,
10409         anchor,
10410         codeElement,
10411         textContent;
10412     for (; i<length; i++) {
10413       anchor      = anchors[i];
10414       codeElement = dom.getParentElement(anchor, { nodeName: "code" });
10415       textContent = dom.getTextContent(anchor);
10416
10417       // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
10418       // else replace <a> with its childNodes
10419       if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
10420         // <code> element is used to prevent later auto-linking of the content
10421         codeElement = dom.renameElement(anchor, "code");
10422       } else {
10423         dom.replaceWithChildNodes(anchor);
10424       }
10425     }
10426   }
10427
10428   wysihtml5.commands.removeLink = {
10429     /*
10430      * If selection is a link, it removes the link and wraps it with a <code> element
10431      * The <code> element is needed to avoid auto linking
10432      *
10433      * @example
10434      *    wysihtml5.commands.createLink.exec(composer, "removeLink");
10435      */
10436
10437     exec: function(composer, command) {
10438       var anchors = this.state(composer, command);
10439       if (anchors) {
10440         composer.selection.executeAndRestore(function() {
10441           _removeFormat(composer, anchors);
10442         });
10443       }
10444     },
10445
10446     state: function(composer, command) {
10447       return wysihtml5.commands.formatInline.state(composer, command, "A");
10448     }
10449   };
10450 })(wysihtml5);
10451 ;/**
10452  * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
10453  * which we don't want
10454  * Instead we set a css class
10455  */
10456 (function(wysihtml5) {
10457   var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g;
10458
10459   wysihtml5.commands.fontSize = {
10460     exec: function(composer, command, size) {
10461         wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10462     },
10463
10464     state: function(composer, command, size) {
10465       return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10466     }
10467   };
10468 })(wysihtml5);
10469 ;/* In case font size adjustment to any number defined by user is preferred, we cannot use classes and must use inline styles. */
10470 (function(wysihtml5) {
10471   var REG_EXP = /(\s|^)font-size\s*:\s*[^;\s]+;?/gi;
10472
10473   wysihtml5.commands.fontSizeStyle = {
10474     exec: function(composer, command, size) {
10475       size = (typeof(size) == "object") ? size.size : size;
10476       if (!(/^\s*$/).test(size)) {
10477         wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, "font-size:" + size, REG_EXP);
10478       }
10479     },
10480
10481     state: function(composer, command, size) {
10482       return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "font-size", REG_EXP);
10483     },
10484
10485     stateValue: function(composer, command) {
10486       var st = this.state(composer, command),
10487           styleStr, fontsizeMatches,
10488           val = false;
10489
10490       if (st && wysihtml5.lang.object(st).isArray()) {
10491           st = st[0];
10492       }
10493       if (st) {
10494         styleStr = st.getAttribute('style');
10495         if (styleStr) {
10496           return wysihtml5.quirks.styleParser.parseFontSize(styleStr);
10497         }
10498       }
10499       return false;
10500     }
10501   };
10502 })(wysihtml5);
10503 ;/**
10504  * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
10505  * which we don't want
10506  * Instead we set a css class
10507  */
10508 (function(wysihtml5) {
10509   var REG_EXP = /wysiwyg-color-[0-9a-z]+/g;
10510
10511   wysihtml5.commands.foreColor = {
10512     exec: function(composer, command, color) {
10513         wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10514     },
10515
10516     state: function(composer, command, color) {
10517       return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10518     }
10519   };
10520 })(wysihtml5);
10521 ;/**
10522  * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
10523  * which we don't want
10524  * Instead we set a css class
10525  */
10526 (function(wysihtml5) {
10527   var REG_EXP = /(\s|^)color\s*:\s*[^;\s]+;?/gi;
10528
10529   wysihtml5.commands.foreColorStyle = {
10530     exec: function(composer, command, color) {
10531       var colorVals  = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "color:" + color.color : "color:" + color, "color"),
10532           colString;
10533
10534       if (colorVals) {
10535         colString = "color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
10536         if (colorVals[3] !== 1) {
10537           colString += "color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
10538         }
10539         wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10540       }
10541     },
10542
10543     state: function(composer, command) {
10544       return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "color", REG_EXP);
10545     },
10546
10547     stateValue: function(composer, command, props) {
10548       var st = this.state(composer, command),
10549           colorStr;
10550
10551       if (st && wysihtml5.lang.object(st).isArray()) {
10552         st = st[0];
10553       }
10554
10555       if (st) {
10556         colorStr = st.getAttribute('style');
10557         if (colorStr) {
10558           if (colorStr) {
10559             val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
10560             return wysihtml5.quirks.styleParser.unparseColor(val, props);
10561           }
10562         }
10563       }
10564       return false;
10565     }
10566
10567   };
10568 })(wysihtml5);
10569 ;/* In case background adjustment to any color defined by user is preferred, we cannot use classes and must use inline styles. */
10570 (function(wysihtml5) {
10571   var REG_EXP = /(\s|^)background-color\s*:\s*[^;\s]+;?/gi;
10572
10573   wysihtml5.commands.bgColorStyle = {
10574     exec: function(composer, command, color) {
10575       var colorVals  = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "background-color:" + color.color : "background-color:" + color, "background-color"),
10576           colString;
10577
10578       if (colorVals) {
10579         colString = "background-color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
10580         if (colorVals[3] !== 1) {
10581           colString += "background-color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
10582         }
10583         wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10584       }
10585     },
10586
10587     state: function(composer, command) {
10588       return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "background-color", REG_EXP);
10589     },
10590
10591     stateValue: function(composer, command, props) {
10592       var st = this.state(composer, command),
10593           colorStr,
10594           val = false;
10595
10596       if (st && wysihtml5.lang.object(st).isArray()) {
10597         st = st[0];
10598       }
10599
10600       if (st) {
10601         colorStr = st.getAttribute('style');
10602         if (colorStr) {
10603           val = wysihtml5.quirks.styleParser.parseColor(colorStr, "background-color");
10604           return wysihtml5.quirks.styleParser.unparseColor(val, props);
10605         }
10606       }
10607       return false;
10608     }
10609
10610   };
10611 })(wysihtml5);
10612 ;(function(wysihtml5) {
10613   var dom                     = wysihtml5.dom,
10614       // Following elements are grouped
10615       // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
10616       // instead of creating a H4 within a H1 which would result in semantically invalid html
10617       BLOCK_ELEMENTS_GROUP    = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "PRE", "DIV"];
10618
10619   /**
10620    * Remove similiar classes (based on classRegExp)
10621    * and add the desired class name
10622    */
10623   function _addClass(element, className, classRegExp) {
10624     if (element.className) {
10625       _removeClass(element, classRegExp);
10626       element.className = wysihtml5.lang.string(element.className + " " + className).trim();
10627     } else {
10628       element.className = className;
10629     }
10630   }
10631
10632   function _addStyle(element, cssStyle, styleRegExp) {
10633     _removeStyle(element, styleRegExp);
10634     if (element.getAttribute('style')) {
10635       element.setAttribute('style', wysihtml5.lang.string(element.getAttribute('style') + " " + cssStyle).trim());
10636     } else {
10637       element.setAttribute('style', cssStyle);
10638     }
10639   }
10640
10641   function _removeClass(element, classRegExp) {
10642     var ret = classRegExp.test(element.className);
10643     element.className = element.className.replace(classRegExp, "");
10644     if (wysihtml5.lang.string(element.className).trim() == '') {
10645         element.removeAttribute('class');
10646     }
10647     return ret;
10648   }
10649
10650   function _removeStyle(element, styleRegExp) {
10651     var ret = styleRegExp.test(element.getAttribute('style'));
10652     element.setAttribute('style', (element.getAttribute('style') || "").replace(styleRegExp, ""));
10653     if (wysihtml5.lang.string(element.getAttribute('style') || "").trim() == '') {
10654       element.removeAttribute('style');
10655     }
10656     return ret;
10657   }
10658
10659   function _removeLastChildIfLineBreak(node) {
10660     var lastChild = node.lastChild;
10661     if (lastChild && _isLineBreak(lastChild)) {
10662       lastChild.parentNode.removeChild(lastChild);
10663     }
10664   }
10665
10666   function _isLineBreak(node) {
10667     return node.nodeName === "BR";
10668   }
10669
10670   /**
10671    * Execute native query command
10672    * and if necessary modify the inserted node's className
10673    */
10674   function _execCommand(doc, composer, command, nodeName, className) {
10675     var ranges = composer.selection.getOwnRanges();
10676     for (var i = ranges.length; i--;){
10677       composer.selection.getSelection().removeAllRanges();
10678       composer.selection.setSelection(ranges[i]);
10679       if (className) {
10680         var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
10681           var target = event.target,
10682               displayStyle;
10683           if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
10684             return;
10685           }
10686           displayStyle = dom.getStyle("display").from(target);
10687           if (displayStyle.substr(0, 6) !== "inline") {
10688             // Make sure that only block elements receive the given class
10689             target.className += " " + className;
10690           }
10691         });
10692       }
10693       doc.execCommand(command, false, nodeName);
10694
10695       if (eventListener) {
10696         eventListener.stop();
10697       }
10698     }
10699   }
10700
10701   function _selectionWrap(composer, options) {
10702     if (composer.selection.isCollapsed()) {
10703         composer.selection.selectLine();
10704     }
10705
10706     var surroundedNodes = composer.selection.surround(options);
10707     for (var i = 0, imax = surroundedNodes.length; i < imax; i++) {
10708       wysihtml5.dom.lineBreaks(surroundedNodes[i]).remove();
10709       _removeLastChildIfLineBreak(surroundedNodes[i]);
10710     }
10711
10712     // rethink restoring selection
10713     // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly());
10714   }
10715
10716   function _hasClasses(element) {
10717     return !!wysihtml5.lang.string(element.className).trim();
10718   }
10719
10720   function _hasStyles(element) {
10721     return !!wysihtml5.lang.string(element.getAttribute('style') || '').trim();
10722   }
10723
10724   wysihtml5.commands.formatBlock = {
10725     exec: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
10726       var doc             = composer.doc,
10727           blockElements    = this.state(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp),
10728           useLineBreaks   = composer.config.useLineBreaks,
10729           defaultNodeName = useLineBreaks ? "DIV" : "P",
10730           selectedNodes, classRemoveAction, blockRenameFound, styleRemoveAction, blockElement;
10731       nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
10732
10733       if (blockElements.length) {
10734         composer.selection.executeAndRestoreRangy(function() {
10735           for (var b = blockElements.length; b--;) {
10736             if (classRegExp) {
10737               classRemoveAction = _removeClass(blockElements[b], classRegExp);
10738             }
10739             if (styleRegExp) {
10740               styleRemoveAction = _removeStyle(blockElements[b], styleRegExp);
10741             }
10742
10743             if ((styleRemoveAction || classRemoveAction) && nodeName === null && blockElements[b].nodeName != defaultNodeName) {
10744               // dont rename or remove element when just setting block formating class or style
10745               return;
10746             }
10747
10748             var hasClasses = _hasClasses(blockElements[b]),
10749                 hasStyles = _hasStyles(blockElements[b]);
10750
10751             if (!hasClasses && !hasStyles && (useLineBreaks || nodeName === "P")) {
10752               // Insert a line break afterwards and beforewards when there are siblings
10753               // that are not of type line break or block element
10754               wysihtml5.dom.lineBreaks(blockElements[b]).add();
10755               dom.replaceWithChildNodes(blockElements[b]);
10756             } else {
10757               // Make sure that styling is kept by renaming the element to a <div> or <p> and copying over the class name
10758               dom.renameElement(blockElements[b], nodeName === "P" ? "DIV" : defaultNodeName);
10759             }
10760           }
10761         });
10762
10763         return;
10764       }
10765
10766       // Find similiar block element and rename it (<h2 class="foo"></h2>  =>  <h1 class="foo"></h1>)
10767       if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
10768         selectedNodes = composer.selection.findNodesInSelection(BLOCK_ELEMENTS_GROUP).concat(composer.selection.getSelectedOwnNodes());
10769         composer.selection.executeAndRestoreRangy(function() {
10770           for (var n = selectedNodes.length; n--;) {
10771             blockElement = dom.getParentElement(selectedNodes[n], {
10772               nodeName: BLOCK_ELEMENTS_GROUP
10773             });
10774             if (blockElement == composer.element) {
10775               blockElement = null;
10776             }
10777             if (blockElement) {
10778                 // Rename current block element to new block element and add class
10779                 if (nodeName) {
10780                   blockElement = dom.renameElement(blockElement, nodeName);
10781                 }
10782                 if (className) {
10783                   _addClass(blockElement, className, classRegExp);
10784                 }
10785                 if (cssStyle) {
10786                   _addStyle(blockElement, cssStyle, styleRegExp);
10787                 }
10788               blockRenameFound = true;
10789             }
10790           }
10791
10792         });
10793
10794         if (blockRenameFound) {
10795           return;
10796         }
10797       }
10798
10799       _selectionWrap(composer, {
10800         "nodeName": (nodeName || defaultNodeName),
10801         "className": className || null,
10802         "cssStyle": cssStyle || null
10803       });
10804     },
10805
10806     state: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
10807       var nodes = composer.selection.getSelectedOwnNodes(),
10808           parents = [],
10809           parent;
10810
10811       nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
10812
10813       //var selectedNode = composer.selection.getSelectedNode();
10814       for (var i = 0, maxi = nodes.length; i < maxi; i++) {
10815         parent = dom.getParentElement(nodes[i], {
10816           nodeName:     nodeName,
10817           className:    className,
10818           classRegExp:  classRegExp,
10819           cssStyle:     cssStyle,
10820           styleRegExp:  styleRegExp
10821         });
10822         if (parent && wysihtml5.lang.array(parents).indexOf(parent) == -1) {
10823           parents.push(parent);
10824         }
10825       }
10826       if (parents.length == 0) {
10827         return false;
10828       }
10829       return parents;
10830     }
10831
10832
10833   };
10834 })(wysihtml5);
10835 ;/* Formats block for as a <pre><code class="classname"></code></pre> block
10836  * Useful in conjuction for sytax highlight utility: highlight.js
10837  *
10838  * Usage:
10839  *
10840  * editorInstance.composer.commands.exec("formatCode", "language-html");
10841 */
10842
10843 wysihtml5.commands.formatCode = {
10844
10845   exec: function(composer, command, classname) {
10846     var pre = this.state(composer),
10847         code, range, selectedNodes;
10848     if (pre) {
10849       // caret is already within a <pre><code>...</code></pre>
10850       composer.selection.executeAndRestore(function() {
10851         code = pre.querySelector("code");
10852         wysihtml5.dom.replaceWithChildNodes(pre);
10853         if (code) {
10854           wysihtml5.dom.replaceWithChildNodes(code);
10855         }
10856       });
10857     } else {
10858       // Wrap in <pre><code>...</code></pre>
10859       range = composer.selection.getRange();
10860       selectedNodes = range.extractContents();
10861       pre = composer.doc.createElement("pre");
10862       code = composer.doc.createElement("code");
10863
10864       if (classname) {
10865         code.className = classname;
10866       }
10867
10868       pre.appendChild(code);
10869       code.appendChild(selectedNodes);
10870       range.insertNode(pre);
10871       composer.selection.selectNode(pre);
10872     }
10873   },
10874
10875   state: function(composer) {
10876     var selectedNode = composer.selection.getSelectedNode();
10877     if (selectedNode && selectedNode.nodeName && selectedNode.nodeName == "PRE"&&
10878         selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") {
10879       return selectedNode;
10880     } else {
10881       return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" });
10882     }
10883   }
10884 };;/**
10885  * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
10886  *
10887  *   #1 caret in unformatted text:
10888  *      abcdefg|
10889  *   output:
10890  *      abcdefg<b>|</b>
10891  *
10892  *   #2 unformatted text selected:
10893  *      abc|deg|h
10894  *   output:
10895  *      abc<b>|deg|</b>h
10896  *
10897  *   #3 unformatted text selected across boundaries:
10898  *      ab|c <span>defg|h</span>
10899  *   output:
10900  *      ab<b>|c </b><span><b>defg</b>|h</span>
10901  *
10902  *   #4 formatted text entirely selected
10903  *      <b>|abc|</b>
10904  *   output:
10905  *      |abc|
10906  *
10907  *   #5 formatted text partially selected
10908  *      <b>ab|c|</b>
10909  *   output:
10910  *      <b>ab</b>|c|
10911  *
10912  *   #6 formatted text selected across boundaries
10913  *      <span>ab|c</span> <b>de|fgh</b>
10914  *   output:
10915  *      <span>ab|c</span> de|<b>fgh</b>
10916  */
10917 (function(wysihtml5) {
10918   var // Treat <b> as <strong> and vice versa
10919       ALIAS_MAPPING = {
10920         "strong": "b",
10921         "em":     "i",
10922         "b":      "strong",
10923         "i":      "em"
10924       },
10925       htmlApplier = {};
10926
10927   function _getTagNames(tagName) {
10928     var alias = ALIAS_MAPPING[tagName];
10929     return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
10930   }
10931
10932   function _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, container) {
10933     var identifier = tagName;
10934     
10935     if (className) {
10936       identifier += ":" + className;
10937     }
10938     if (cssStyle) {
10939       identifier += ":" + cssStyle;
10940     }
10941
10942     if (!htmlApplier[identifier]) {
10943       htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true, cssStyle, styleRegExp, container);
10944     }
10945
10946     return htmlApplier[identifier];
10947   }
10948
10949   wysihtml5.commands.formatInline = {
10950     exec: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, dontRestoreSelect, noCleanup) {
10951       var range = composer.selection.createRange(),
10952           ownRanges = composer.selection.getOwnRanges();
10953
10954       if (!ownRanges || ownRanges.length == 0) {
10955         return false;
10956       }
10957       composer.selection.getSelection().removeAllRanges();
10958
10959       _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).toggleRange(ownRanges);
10960
10961       if (!dontRestoreSelect) {
10962         range.setStart(ownRanges[0].startContainer,  ownRanges[0].startOffset);
10963         range.setEnd(
10964           ownRanges[ownRanges.length - 1].endContainer,
10965           ownRanges[ownRanges.length - 1].endOffset
10966         );
10967         composer.selection.setSelection(range);
10968         composer.selection.executeAndRestore(function() {
10969           if (!noCleanup) {
10970             composer.cleanUp();
10971           }
10972         }, true, true);
10973       } else if (!noCleanup) {
10974         composer.cleanUp();
10975       }
10976     },
10977
10978     // Executes so that if collapsed caret is in a state and executing that state it should unformat that state
10979     // It is achieved by selecting the entire state element before executing.
10980     // This works on built in contenteditable inline format commands
10981     execWithToggle: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
10982       var that = this;
10983
10984       if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) &&
10985         composer.selection.isCollapsed() &&
10986         !composer.selection.caretIsLastInSelection() &&
10987         !composer.selection.caretIsFirstInSelection()
10988       ) {
10989         var state_element = that.state(composer, command, tagName, className, classRegExp)[0];
10990         composer.selection.executeAndRestoreRangy(function() {
10991           var parent = state_element.parentNode;
10992           composer.selection.selectNode(state_element, true);
10993           wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
10994         });
10995       } else {
10996         if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) && !composer.selection.isCollapsed()) {
10997           composer.selection.executeAndRestoreRangy(function() {
10998             wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
10999           });
11000         } else {
11001           wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp);
11002         }
11003       }
11004     },
11005
11006     state: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
11007       var doc           = composer.doc,
11008           aliasTagName  = ALIAS_MAPPING[tagName] || tagName,
11009           ownRanges, isApplied;
11010
11011       // Check whether the document contains a node with the desired tagName
11012       if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
11013           !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
11014         return false;
11015       }
11016
11017        // Check whether the document contains a node with the desired className
11018       if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
11019          return false;
11020       }
11021
11022       ownRanges = composer.selection.getOwnRanges();
11023
11024       if (!ownRanges || ownRanges.length === 0) {
11025         return false;
11026       }
11027
11028       isApplied = _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).isAppliedToRange(ownRanges);
11029
11030       return (isApplied && isApplied.elements) ? isApplied.elements : false;
11031     }
11032   };
11033 })(wysihtml5);
11034 ;(function(wysihtml5) {
11035
11036   wysihtml5.commands.insertBlockQuote = {
11037     exec: function(composer, command) {
11038       var state = this.state(composer, command),
11039           endToEndParent = composer.selection.isEndToEndInNode(['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P']),
11040           prevNode, nextNode;
11041
11042       composer.selection.executeAndRestore(function() {
11043         if (state) {
11044           if (composer.config.useLineBreaks) {
11045              wysihtml5.dom.lineBreaks(state).add();
11046           }
11047           wysihtml5.dom.unwrap(state);
11048         } else {
11049           if (composer.selection.isCollapsed()) {
11050             composer.selection.selectLine();
11051           }
11052           
11053           if (endToEndParent) {
11054             var qouteEl = endToEndParent.ownerDocument.createElement('blockquote');
11055             wysihtml5.dom.insert(qouteEl).after(endToEndParent);
11056             qouteEl.appendChild(endToEndParent);
11057           } else {
11058             composer.selection.surround({nodeName: "blockquote"});
11059           }
11060         }
11061       });
11062     },
11063     state: function(composer, command) {
11064       var selectedNode  = composer.selection.getSelectedNode(),
11065           node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "BLOCKQUOTE" }, false, composer.element);
11066
11067       return (node) ? node : false;
11068     }
11069   };
11070
11071 })(wysihtml5);;wysihtml5.commands.insertHTML = {
11072   exec: function(composer, command, html) {
11073     if (composer.commands.support(command)) {
11074       composer.doc.execCommand(command, false, html);
11075     } else {
11076       composer.selection.insertHTML(html);
11077     }
11078   },
11079
11080   state: function() {
11081     return false;
11082   }
11083 };
11084 ;(function(wysihtml5) {
11085   var NODE_NAME = "IMG";
11086
11087   wysihtml5.commands.insertImage = {
11088     /**
11089      * Inserts an <img>
11090      * If selection is already an image link, it removes it
11091      *
11092      * @example
11093      *    // either ...
11094      *    wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
11095      *    // ... or ...
11096      *    wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
11097      */
11098     exec: function(composer, command, value) {
11099       value = typeof(value) === "object" ? value : { src: value };
11100
11101       var doc     = composer.doc,
11102           image   = this.state(composer),
11103           textNode,
11104           parent;
11105
11106       if (image) {
11107         // Image already selected, set the caret before it and delete it
11108         composer.selection.setBefore(image);
11109         parent = image.parentNode;
11110         parent.removeChild(image);
11111
11112         // and it's parent <a> too if it hasn't got any other relevant child nodes
11113         wysihtml5.dom.removeEmptyTextNodes(parent);
11114         if (parent.nodeName === "A" && !parent.firstChild) {
11115           composer.selection.setAfter(parent);
11116           parent.parentNode.removeChild(parent);
11117         }
11118
11119         // firefox and ie sometimes don't remove the image handles, even though the image got removed
11120         wysihtml5.quirks.redraw(composer.element);
11121         return;
11122       }
11123
11124       image = doc.createElement(NODE_NAME);
11125
11126       for (var i in value) {
11127         image.setAttribute(i === "className" ? "class" : i, value[i]);
11128       }
11129
11130       composer.selection.insertNode(image);
11131       if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
11132         textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
11133         composer.selection.insertNode(textNode);
11134         composer.selection.setAfter(textNode);
11135       } else {
11136         composer.selection.setAfter(image);
11137       }
11138     },
11139
11140     state: function(composer) {
11141       var doc = composer.doc,
11142           selectedNode,
11143           text,
11144           imagesInSelection;
11145
11146       if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
11147         return false;
11148       }
11149
11150       selectedNode = composer.selection.getSelectedNode();
11151       if (!selectedNode) {
11152         return false;
11153       }
11154
11155       if (selectedNode.nodeName === NODE_NAME) {
11156         // This works perfectly in IE
11157         return selectedNode;
11158       }
11159
11160       if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
11161         return false;
11162       }
11163
11164       text = composer.selection.getText();
11165       text = wysihtml5.lang.string(text).trim();
11166       if (text) {
11167         return false;
11168       }
11169
11170       imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
11171         return node.nodeName === "IMG";
11172       });
11173
11174       if (imagesInSelection.length !== 1) {
11175         return false;
11176       }
11177
11178       return imagesInSelection[0];
11179     }
11180   };
11181 })(wysihtml5);
11182 ;(function(wysihtml5) {
11183   var LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
11184
11185   wysihtml5.commands.insertLineBreak = {
11186     exec: function(composer, command) {
11187       if (composer.commands.support(command)) {
11188         composer.doc.execCommand(command, false, null);
11189         if (!wysihtml5.browser.autoScrollsToCaret()) {
11190           composer.selection.scrollIntoView();
11191         }
11192       } else {
11193         composer.commands.exec("insertHTML", LINE_BREAK);
11194       }
11195     },
11196
11197     state: function() {
11198       return false;
11199     }
11200   };
11201 })(wysihtml5);
11202 ;wysihtml5.commands.insertOrderedList = {
11203   exec: function(composer, command) {
11204     wysihtml5.commands.insertList.exec(composer, command, "OL");
11205   },
11206
11207   state: function(composer, command) {
11208     return wysihtml5.commands.insertList.state(composer, command, "OL");
11209   }
11210 };
11211 ;wysihtml5.commands.insertUnorderedList = {
11212   exec: function(composer, command) {
11213     wysihtml5.commands.insertList.exec(composer, command, "UL");
11214   },
11215
11216   state: function(composer, command) {
11217     return wysihtml5.commands.insertList.state(composer, command, "UL");
11218   }
11219 };
11220 ;wysihtml5.commands.insertList = (function(wysihtml5) {
11221
11222   var isNode = function(node, name) {
11223     if (node && node.nodeName) {
11224       if (typeof name === 'string') {
11225         name = [name];
11226       }
11227       for (var n = name.length; n--;) {
11228         if (node.nodeName === name[n]) {
11229           return true;
11230         }
11231       }
11232     }
11233     return false;
11234   };
11235
11236   var findListEl = function(node, nodeName, composer) {
11237     var ret = {
11238           el: null,
11239           other: false
11240         };
11241
11242     if (node) {
11243       var parentLi = wysihtml5.dom.getParentElement(node, { nodeName: "LI" }),
11244           otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11245
11246       if (isNode(node, nodeName)) {
11247         ret.el = node;
11248       } else if (isNode(node, otherNodeName)) {
11249         ret = {
11250           el: node,
11251           other: true
11252         };
11253       } else if (parentLi) {
11254         if (isNode(parentLi.parentNode, nodeName)) {
11255           ret.el = parentLi.parentNode;
11256         } else if (isNode(parentLi.parentNode, otherNodeName)) {
11257           ret = {
11258             el : parentLi.parentNode,
11259             other: true
11260           };
11261         }
11262       }
11263     }
11264
11265     // do not count list elements outside of composer
11266     if (ret.el && !composer.element.contains(ret.el)) {
11267       ret.el = null;
11268     }
11269
11270     return ret;
11271   };
11272
11273   var handleSameTypeList = function(el, nodeName, composer) {
11274     var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
11275         otherLists, innerLists;
11276     // Unwrap list
11277     // <ul><li>foo</li><li>bar</li></ul>
11278     // becomes:
11279     // foo<br>bar<br>
11280     composer.selection.executeAndRestore(function() {
11281       var otherLists = getListsInSelection(otherNodeName, composer);
11282       if (otherLists.length) {
11283         for (var l = otherLists.length; l--;) {
11284           wysihtml5.dom.renameElement(otherLists[l], nodeName.toLowerCase());
11285         }
11286       } else {
11287         innerLists = getListsInSelection(['OL', 'UL'], composer);
11288         for (var i = innerLists.length; i--;) {
11289           wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
11290         }
11291         wysihtml5.dom.resolveList(el, composer.config.useLineBreaks);
11292       }
11293     });
11294   };
11295
11296   var handleOtherTypeList =  function(el, nodeName, composer) {
11297     var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11298     // Turn an ordered list into an unordered list
11299     // <ol><li>foo</li><li>bar</li></ol>
11300     // becomes:
11301     // <ul><li>foo</li><li>bar</li></ul>
11302     // Also rename other lists in selection
11303     composer.selection.executeAndRestore(function() {
11304       var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
11305
11306       // All selection inner lists get renamed too
11307       for (var l = renameLists.length; l--;) {
11308         wysihtml5.dom.renameElement(renameLists[l], nodeName.toLowerCase());
11309       }
11310     });
11311   };
11312
11313   var getListsInSelection = function(nodeName, composer) {
11314       var ranges = composer.selection.getOwnRanges(),
11315           renameLists = [];
11316
11317       for (var r = ranges.length; r--;) {
11318         renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
11319           return isNode(node, nodeName);
11320         }));
11321       }
11322
11323       return renameLists;
11324   };
11325
11326   var createListFallback = function(nodeName, composer) {
11327     // Fallback for Create list
11328     composer.selection.executeAndRestoreRangy(function() {
11329       var tempClassName =  "_wysihtml5-temp-" + new Date().getTime(),
11330           tempElement = composer.selection.deblockAndSurround({
11331             "nodeName": "div",
11332             "className": tempClassName
11333           }),
11334           isEmpty, list;
11335
11336       // This space causes new lists to never break on enter 
11337       var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
11338       tempElement.innerHTML = tempElement.innerHTML.replace(INVISIBLE_SPACE_REG_EXP, "");
11339       
11340       if (tempElement) {
11341         isEmpty = wysihtml5.lang.array(["", "<br>", wysihtml5.INVISIBLE_SPACE]).contains(tempElement.innerHTML);
11342         list = wysihtml5.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.uneditableContainerClassname);
11343         if (isEmpty) {
11344           composer.selection.selectNode(list.querySelector("li"), true);
11345         }
11346       }
11347     });
11348   };
11349
11350   return {
11351     exec: function(composer, command, nodeName) {
11352       var doc           = composer.doc,
11353           cmd           = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
11354           selectedNode  = composer.selection.getSelectedNode(),
11355           list          = findListEl(selectedNode, nodeName, composer);
11356
11357       if (!list.el) {
11358         if (composer.commands.support(cmd)) {
11359           doc.execCommand(cmd, false, null);
11360         } else {
11361           createListFallback(nodeName, composer);
11362         }
11363       } else if (list.other) {
11364         handleOtherTypeList(list.el, nodeName, composer);
11365       } else {
11366         handleSameTypeList(list.el, nodeName, composer);
11367       }
11368     },
11369
11370     state: function(composer, command, nodeName) {
11371       var selectedNode = composer.selection.getSelectedNode(),
11372           list         = findListEl(selectedNode, nodeName, composer);
11373
11374       return (list.el && !list.other) ? list.el : false;
11375     }
11376   };
11377
11378 })(wysihtml5);;wysihtml5.commands.italic = {
11379   exec: function(composer, command) {
11380     wysihtml5.commands.formatInline.execWithToggle(composer, command, "i");
11381   },
11382
11383   state: function(composer, command) {
11384     // element.ownerDocument.queryCommandState("italic") results:
11385     // firefox: only <i>
11386     // chrome:  <i>, <em>, <blockquote>, ...
11387     // ie:      <i>, <em>
11388     // opera:   only <i>
11389     return wysihtml5.commands.formatInline.state(composer, command, "i");
11390   }
11391 };
11392 ;(function(wysihtml5) {
11393   var CLASS_NAME  = "wysiwyg-text-align-center",
11394       REG_EXP     = /wysiwyg-text-align-[0-9a-z]+/g;
11395
11396   wysihtml5.commands.justifyCenter = {
11397     exec: function(composer, command) {
11398       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11399     },
11400
11401     state: function(composer, command) {
11402       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11403     }
11404   };
11405 })(wysihtml5);
11406 ;(function(wysihtml5) {
11407   var CLASS_NAME  = "wysiwyg-text-align-left",
11408       REG_EXP     = /wysiwyg-text-align-[0-9a-z]+/g;
11409
11410   wysihtml5.commands.justifyLeft = {
11411     exec: function(composer, command) {
11412       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11413     },
11414
11415     state: function(composer, command) {
11416       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11417     }
11418   };
11419 })(wysihtml5);
11420 ;(function(wysihtml5) {
11421   var CLASS_NAME  = "wysiwyg-text-align-right",
11422       REG_EXP     = /wysiwyg-text-align-[0-9a-z]+/g;
11423
11424   wysihtml5.commands.justifyRight = {
11425     exec: function(composer, command) {
11426       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11427     },
11428
11429     state: function(composer, command) {
11430       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11431     }
11432   };
11433 })(wysihtml5);
11434 ;(function(wysihtml5) {
11435   var CLASS_NAME  = "wysiwyg-text-align-justify",
11436       REG_EXP     = /wysiwyg-text-align-[0-9a-z]+/g;
11437
11438   wysihtml5.commands.justifyFull = {
11439     exec: function(composer, command) {
11440       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11441     },
11442
11443     state: function(composer, command) {
11444       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11445     }
11446   };
11447 })(wysihtml5);
11448 ;(function(wysihtml5) {
11449   var STYLE_STR  = "text-align: right;",
11450       REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11451
11452   wysihtml5.commands.alignRightStyle = {
11453     exec: function(composer, command) {
11454       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11455     },
11456
11457     state: function(composer, command) {
11458       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11459     }
11460   };
11461 })(wysihtml5);
11462 ;(function(wysihtml5) {
11463   var STYLE_STR  = "text-align: left;",
11464       REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11465
11466   wysihtml5.commands.alignLeftStyle = {
11467     exec: function(composer, command) {
11468       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11469     },
11470
11471     state: function(composer, command) {
11472       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11473     }
11474   };
11475 })(wysihtml5);
11476 ;(function(wysihtml5) {
11477   var STYLE_STR  = "text-align: center;",
11478       REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11479
11480   wysihtml5.commands.alignCenterStyle = {
11481     exec: function(composer, command) {
11482       return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11483     },
11484
11485     state: function(composer, command) {
11486       return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11487     }
11488   };
11489 })(wysihtml5);
11490 ;wysihtml5.commands.redo = {
11491   exec: function(composer) {
11492     return composer.undoManager.redo();
11493   },
11494
11495   state: function(composer) {
11496     return false;
11497   }
11498 };
11499 ;wysihtml5.commands.underline = {
11500   exec: function(composer, command) {
11501     wysihtml5.commands.formatInline.execWithToggle(composer, command, "u");
11502   },
11503
11504   state: function(composer, command) {
11505     return wysihtml5.commands.formatInline.state(composer, command, "u");
11506   }
11507 };
11508 ;wysihtml5.commands.undo = {
11509   exec: function(composer) {
11510     return composer.undoManager.undo();
11511   },
11512
11513   state: function(composer) {
11514     return false;
11515   }
11516 };
11517 ;wysihtml5.commands.createTable = {
11518   exec: function(composer, command, value) {
11519       var col, row, html;
11520       if (value && value.cols && value.rows && parseInt(value.cols, 10) > 0 && parseInt(value.rows, 10) > 0) {
11521           if (value.tableStyle) {
11522             html = "<table style=\"" + value.tableStyle + "\">";
11523           } else {
11524             html = "<table>";
11525           }
11526           html += "<tbody>";
11527           for (row = 0; row < value.rows; row ++) {
11528               html += '<tr>';
11529               for (col = 0; col < value.cols; col ++) {
11530                   html += "<td>&nbsp;</td>";
11531               }
11532               html += '</tr>';
11533           }
11534           html += "</tbody></table>";
11535           composer.commands.exec("insertHTML", html);
11536           //composer.selection.insertHTML(html);
11537       }
11538
11539
11540   },
11541
11542   state: function(composer, command) {
11543       return false;
11544   }
11545 };
11546 ;wysihtml5.commands.mergeTableCells = {
11547   exec: function(composer, command) {
11548       if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11549           if (this.state(composer, command)) {
11550               wysihtml5.dom.table.unmergeCell(composer.tableSelection.start);
11551           } else {
11552               wysihtml5.dom.table.mergeCellsBetween(composer.tableSelection.start, composer.tableSelection.end);
11553           }
11554       }
11555   },
11556
11557   state: function(composer, command) {
11558       if (composer.tableSelection) {
11559           var start = composer.tableSelection.start,
11560               end = composer.tableSelection.end;
11561           if (start && end && start == end &&
11562               ((
11563                   wysihtml5.dom.getAttribute(start, "colspan") &&
11564                   parseInt(wysihtml5.dom.getAttribute(start, "colspan"), 10) > 1
11565               ) || (
11566                   wysihtml5.dom.getAttribute(start, "rowspan") &&
11567                   parseInt(wysihtml5.dom.getAttribute(start, "rowspan"), 10) > 1
11568               ))
11569           ) {
11570               return [start];
11571           }
11572       }
11573       return false;
11574   }
11575 };
11576 ;wysihtml5.commands.addTableCells = {
11577   exec: function(composer, command, value) {
11578       if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11579
11580           // switches start and end if start is bigger than end (reverse selection)
11581           var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end);
11582           if (value == "before" || value == "above") {
11583               wysihtml5.dom.table.addCells(tableSelect.start, value);
11584           } else if (value == "after" || value == "below") {
11585               wysihtml5.dom.table.addCells(tableSelect.end, value);
11586           }
11587           setTimeout(function() {
11588               composer.tableSelection.select(tableSelect.start, tableSelect.end);
11589           },0);
11590       }
11591   },
11592
11593   state: function(composer, command) {
11594       return false;
11595   }
11596 };
11597 ;wysihtml5.commands.deleteTableCells = {
11598   exec: function(composer, command, value) {
11599       if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11600           var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end),
11601               idx = wysihtml5.dom.table.indexOf(tableSelect.start),
11602               selCell,
11603               table = composer.tableSelection.table;
11604
11605           wysihtml5.dom.table.removeCells(tableSelect.start, value);
11606           setTimeout(function() {
11607               // move selection to next or previous if not present
11608               selCell = wysihtml5.dom.table.findCell(table, idx);
11609
11610               if (!selCell){
11611                   if (value == "row") {
11612                       selCell = wysihtml5.dom.table.findCell(table, {
11613                           "row": idx.row - 1,
11614                           "col": idx.col
11615                       });
11616                   }
11617
11618                   if (value == "column") {
11619                       selCell = wysihtml5.dom.table.findCell(table, {
11620                           "row": idx.row,
11621                           "col": idx.col - 1
11622                       });
11623                   }
11624               }
11625               if (selCell) {
11626                   composer.tableSelection.select(selCell, selCell);
11627               }
11628           }, 0);
11629
11630       }
11631   },
11632
11633   state: function(composer, command) {
11634       return false;
11635   }
11636 };
11637 ;wysihtml5.commands.indentList = {
11638   exec: function(composer, command, value) {
11639     var listEls = composer.selection.getSelectionParentsByTag('LI');
11640     if (listEls) {
11641       return this.tryToPushLiLevel(listEls, composer.selection);
11642     }
11643     return false;
11644   },
11645
11646   state: function(composer, command) {
11647       return false;
11648   },
11649
11650   tryToPushLiLevel: function(liNodes, selection) {
11651     var listTag, list, prevLi, liNode, prevLiList,
11652         found = false;
11653
11654     selection.executeAndRestoreRangy(function() {
11655
11656       for (var i = liNodes.length; i--;) {
11657         liNode = liNodes[i];
11658         listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
11659         list = liNode.ownerDocument.createElement(listTag);
11660         prevLi = wysihtml5.dom.domNode(liNode).prev({nodeTypes: [wysihtml5.ELEMENT_NODE]});
11661         prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
11662
11663         if (prevLi) {
11664           if (prevLiList) {
11665             prevLiList.appendChild(liNode);
11666           } else {
11667             list.appendChild(liNode);
11668             prevLi.appendChild(list);
11669           }
11670           found = true;
11671         }
11672       }
11673
11674     });
11675     return found;
11676   }
11677 };
11678 ;wysihtml5.commands.outdentList = {
11679   exec: function(composer, command, value) {
11680     var listEls = composer.selection.getSelectionParentsByTag('LI');
11681     if (listEls) {
11682       return this.tryToPullLiLevel(listEls, composer);
11683     }
11684     return false;
11685   },
11686
11687   state: function(composer, command) {
11688       return false;
11689   },
11690
11691   tryToPullLiLevel: function(liNodes, composer) {
11692     var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
11693         found = false,
11694         that = this;
11695
11696     composer.selection.executeAndRestoreRangy(function() {
11697
11698       for (var i = liNodes.length; i--;) {
11699         liNode = liNodes[i];
11700         if (liNode.parentNode) {
11701           listNode = liNode.parentNode;
11702
11703           if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
11704             found = true;
11705
11706             outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['OL', 'UL']}, false, composer.element);
11707             outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['LI']}, false, composer.element);
11708
11709             if (outerListNode && outerLiNode) {
11710
11711               if (liNode.nextSibling) {
11712                 afterList = that.getAfterList(listNode, liNode);
11713                 liNode.appendChild(afterList);
11714               }
11715               outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
11716
11717             } else {
11718
11719               if (liNode.nextSibling) {
11720                 afterList = that.getAfterList(listNode, liNode);
11721                 liNode.appendChild(afterList);
11722               }
11723
11724               for (var j = liNode.childNodes.length; j--;) {
11725                 listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
11726               }
11727
11728               listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
11729               liNode.parentNode.removeChild(liNode);
11730
11731             }
11732
11733             // cleanup
11734             if (listNode.childNodes.length === 0) {
11735                 listNode.parentNode.removeChild(listNode);
11736             }
11737           }
11738         }
11739       }
11740
11741     });
11742     return found;
11743   },
11744
11745   getAfterList: function(listNode, liNode) {
11746     var nodeName = listNode.nodeName,
11747         newList = document.createElement(nodeName);
11748
11749     while (liNode.nextSibling) {
11750       newList.appendChild(liNode.nextSibling);
11751     }
11752     return newList;
11753   }
11754
11755 };;/**
11756  * Undo Manager for wysihtml5
11757  * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
11758  */
11759 (function(wysihtml5) {
11760   var Z_KEY               = 90,
11761       Y_KEY               = 89,
11762       BACKSPACE_KEY       = 8,
11763       DELETE_KEY          = 46,
11764       MAX_HISTORY_ENTRIES = 25,
11765       DATA_ATTR_NODE      = "data-wysihtml5-selection-node",
11766       DATA_ATTR_OFFSET    = "data-wysihtml5-selection-offset",
11767       UNDO_HTML           = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
11768       REDO_HTML           = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
11769       dom                 = wysihtml5.dom;
11770
11771   function cleanTempElements(doc) {
11772     var tempElement;
11773     while (tempElement = doc.querySelector("._wysihtml5-temp")) {
11774       tempElement.parentNode.removeChild(tempElement);
11775     }
11776   }
11777
11778   wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
11779     /** @scope wysihtml5.UndoManager.prototype */ {
11780     constructor: function(editor) {
11781       this.editor = editor;
11782       this.composer = editor.composer;
11783       this.element = this.composer.element;
11784
11785       this.position = 0;
11786       this.historyStr = [];
11787       this.historyDom = [];
11788
11789       this.transact();
11790
11791       this._observe();
11792     },
11793
11794     _observe: function() {
11795       var that      = this,
11796           doc       = this.composer.sandbox.getDocument(),
11797           lastKey;
11798
11799       // Catch CTRL+Z and CTRL+Y
11800       dom.observe(this.element, "keydown", function(event) {
11801         if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
11802           return;
11803         }
11804
11805         var keyCode = event.keyCode,
11806             isUndo = keyCode === Z_KEY && !event.shiftKey,
11807             isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
11808
11809         if (isUndo) {
11810           that.undo();
11811           event.preventDefault();
11812         } else if (isRedo) {
11813           that.redo();
11814           event.preventDefault();
11815         }
11816       });
11817
11818       // Catch delete and backspace
11819       dom.observe(this.element, "keydown", function(event) {
11820         var keyCode = event.keyCode;
11821         if (keyCode === lastKey) {
11822           return;
11823         }
11824
11825         lastKey = keyCode;
11826
11827         if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
11828           that.transact();
11829         }
11830       });
11831
11832       this.editor
11833         .on("newword:composer", function() {
11834           that.transact();
11835         })
11836
11837         .on("beforecommand:composer", function() {
11838           that.transact();
11839         });
11840     },
11841
11842     transact: function() {
11843       var previousHtml      = this.historyStr[this.position - 1],
11844           currentHtml       = this.composer.getValue(false, false),
11845           composerIsVisible   = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
11846           range, node, offset, element, position;
11847
11848       if (currentHtml === previousHtml) {
11849         return;
11850       }
11851
11852       var length = this.historyStr.length = this.historyDom.length = this.position;
11853       if (length > MAX_HISTORY_ENTRIES) {
11854         this.historyStr.shift();
11855         this.historyDom.shift();
11856         this.position--;
11857       }
11858
11859       this.position++;
11860
11861       if (composerIsVisible) {
11862         // Do not start saving selection if composer is not visible
11863         range   = this.composer.selection.getRange();
11864         node    = (range && range.startContainer) ? range.startContainer : this.element;
11865         offset  = (range && range.startOffset) ? range.startOffset : 0;
11866
11867         if (node.nodeType === wysihtml5.ELEMENT_NODE) {
11868           element = node;
11869         } else {
11870           element  = node.parentNode;
11871           position = this.getChildNodeIndex(element, node);
11872         }
11873
11874         element.setAttribute(DATA_ATTR_OFFSET, offset);
11875         if (typeof(position) !== "undefined") {
11876           element.setAttribute(DATA_ATTR_NODE, position);
11877         }
11878       }
11879
11880       var clone = this.element.cloneNode(!!currentHtml);
11881       this.historyDom.push(clone);
11882       this.historyStr.push(currentHtml);
11883
11884       if (element) {
11885         element.removeAttribute(DATA_ATTR_OFFSET);
11886         element.removeAttribute(DATA_ATTR_NODE);
11887       }
11888
11889     },
11890
11891     undo: function() {
11892       this.transact();
11893
11894       if (!this.undoPossible()) {
11895         return;
11896       }
11897
11898       this.set(this.historyDom[--this.position - 1]);
11899       this.editor.fire("undo:composer");
11900     },
11901
11902     redo: function() {
11903       if (!this.redoPossible()) {
11904         return;
11905       }
11906
11907       this.set(this.historyDom[++this.position - 1]);
11908       this.editor.fire("redo:composer");
11909     },
11910
11911     undoPossible: function() {
11912       return this.position > 1;
11913     },
11914
11915     redoPossible: function() {
11916       return this.position < this.historyStr.length;
11917     },
11918
11919     set: function(historyEntry) {
11920       this.element.innerHTML = "";
11921
11922       var i = 0,
11923           childNodes = historyEntry.childNodes,
11924           length = historyEntry.childNodes.length;
11925
11926       for (; i<length; i++) {
11927         this.element.appendChild(childNodes[i].cloneNode(true));
11928       }
11929
11930       // Restore selection
11931       var offset,
11932           node,
11933           position;
11934
11935       if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
11936         offset    = historyEntry.getAttribute(DATA_ATTR_OFFSET);
11937         position  = historyEntry.getAttribute(DATA_ATTR_NODE);
11938         node      = this.element;
11939       } else {
11940         node      = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
11941         offset    = node.getAttribute(DATA_ATTR_OFFSET);
11942         position  = node.getAttribute(DATA_ATTR_NODE);
11943         node.removeAttribute(DATA_ATTR_OFFSET);
11944         node.removeAttribute(DATA_ATTR_NODE);
11945       }
11946
11947       if (position !== null) {
11948         node = this.getChildNodeByIndex(node, +position);
11949       }
11950
11951       this.composer.selection.set(node, offset);
11952     },
11953
11954     getChildNodeIndex: function(parent, child) {
11955       var i           = 0,
11956           childNodes  = parent.childNodes,
11957           length      = childNodes.length;
11958       for (; i<length; i++) {
11959         if (childNodes[i] === child) {
11960           return i;
11961         }
11962       }
11963     },
11964
11965     getChildNodeByIndex: function(parent, index) {
11966       return parent.childNodes[index];
11967     }
11968   });
11969 })(wysihtml5);
11970 ;/**
11971  * TODO: the following methods still need unit test coverage
11972  */
11973 wysihtml5.views.View = Base.extend(
11974   /** @scope wysihtml5.views.View.prototype */ {
11975   constructor: function(parent, textareaElement, config) {
11976     this.parent   = parent;
11977     this.element  = textareaElement;
11978     this.config   = config;
11979     if (!this.config.noTextarea) {
11980         this._observeViewChange();
11981     }
11982   },
11983
11984   _observeViewChange: function() {
11985     var that = this;
11986     this.parent.on("beforeload", function() {
11987       that.parent.on("change_view", function(view) {
11988         if (view === that.name) {
11989           that.parent.currentView = that;
11990           that.show();
11991           // Using tiny delay here to make sure that the placeholder is set before focusing
11992           setTimeout(function() { that.focus(); }, 0);
11993         } else {
11994           that.hide();
11995         }
11996       });
11997     });
11998   },
11999
12000   focus: function() {
12001     if (this.element.ownerDocument.querySelector(":focus") === this.element) {
12002       return;
12003     }
12004
12005     try { this.element.focus(); } catch(e) {}
12006   },
12007
12008   hide: function() {
12009     this.element.style.display = "none";
12010   },
12011
12012   show: function() {
12013     this.element.style.display = "";
12014   },
12015
12016   disable: function() {
12017     this.element.setAttribute("disabled", "disabled");
12018   },
12019
12020   enable: function() {
12021     this.element.removeAttribute("disabled");
12022   }
12023 });
12024 ;(function(wysihtml5) {
12025   var dom       = wysihtml5.dom,
12026       browser   = wysihtml5.browser;
12027
12028   wysihtml5.views.Composer = wysihtml5.views.View.extend(
12029     /** @scope wysihtml5.views.Composer.prototype */ {
12030     name: "composer",
12031
12032     // Needed for firefox in order to display a proper caret in an empty contentEditable
12033     CARET_HACK: "<br>",
12034
12035     constructor: function(parent, editableElement, config) {
12036       this.base(parent, editableElement, config);
12037       if (!this.config.noTextarea) {
12038           this.textarea = this.parent.textarea;
12039       } else {
12040           this.editableArea = editableElement;
12041       }
12042       if (this.config.contentEditableMode) {
12043           this._initContentEditableArea();
12044       } else {
12045           this._initSandbox();
12046       }
12047     },
12048
12049     clear: function() {
12050       this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
12051     },
12052
12053     getValue: function(parse, clearInternals) {
12054       var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
12055       if (parse !== false) {
12056         value = this.parent.parse(value, (clearInternals === false) ? false : true);
12057       }
12058
12059       return value;
12060     },
12061
12062     setValue: function(html, parse) {
12063       if (parse) {
12064         html = this.parent.parse(html);
12065       }
12066
12067       try {
12068         this.element.innerHTML = html;
12069       } catch (e) {
12070         this.element.innerText = html;
12071       }
12072     },
12073
12074     cleanUp: function() {
12075         this.parent.parse(this.element);
12076     },
12077
12078     show: function() {
12079       this.editableArea.style.display = this._displayStyle || "";
12080
12081       if (!this.config.noTextarea && !this.textarea.element.disabled) {
12082         // Firefox needs this, otherwise contentEditable becomes uneditable
12083         this.disable();
12084         this.enable();
12085       }
12086     },
12087
12088     hide: function() {
12089       this._displayStyle = dom.getStyle("display").from(this.editableArea);
12090       if (this._displayStyle === "none") {
12091         this._displayStyle = null;
12092       }
12093       this.editableArea.style.display = "none";
12094     },
12095
12096     disable: function() {
12097       this.parent.fire("disable:composer");
12098       this.element.removeAttribute("contentEditable");
12099     },
12100
12101     enable: function() {
12102       this.parent.fire("enable:composer");
12103       this.element.setAttribute("contentEditable", "true");
12104     },
12105
12106     focus: function(setToEnd) {
12107       // IE 8 fires the focus event after .focus()
12108       // This is needed by our simulate_placeholder.js to work
12109       // therefore we clear it ourselves this time
12110       if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
12111         this.clear();
12112       }
12113
12114       this.base();
12115
12116       var lastChild = this.element.lastChild;
12117       if (setToEnd && lastChild && this.selection) {
12118         if (lastChild.nodeName === "BR") {
12119           this.selection.setBefore(this.element.lastChild);
12120         } else {
12121           this.selection.setAfter(this.element.lastChild);
12122         }
12123       }
12124     },
12125
12126     getTextContent: function() {
12127       return dom.getTextContent(this.element);
12128     },
12129
12130     hasPlaceholderSet: function() {
12131       return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
12132     },
12133
12134     isEmpty: function() {
12135       var innerHTML = this.element.innerHTML.toLowerCase();
12136       return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML)  ||
12137              innerHTML === ""            ||
12138              innerHTML === "<br>"        ||
12139              innerHTML === "<p></p>"     ||
12140              innerHTML === "<p><br></p>" ||
12141              this.hasPlaceholderSet();
12142     },
12143
12144     _initContentEditableArea: function() {
12145         var that = this;
12146
12147         if (this.config.noTextarea) {
12148             this.sandbox = new dom.ContentEditableArea(function() {
12149                 that._create();
12150             }, {}, this.editableArea);
12151         } else {
12152             this.sandbox = new dom.ContentEditableArea(function() {
12153                 that._create();
12154             });
12155             this.editableArea = this.sandbox.getContentEditable();
12156             dom.insert(this.editableArea).after(this.textarea.element);
12157             this._createWysiwygFormField();
12158         }
12159     },
12160
12161     _initSandbox: function() {
12162       var that = this;
12163
12164       this.sandbox = new dom.Sandbox(function() {
12165         that._create();
12166       }, {
12167         stylesheets:  this.config.stylesheets
12168       });
12169       this.editableArea  = this.sandbox.getIframe();
12170
12171       var textareaElement = this.textarea.element;
12172       dom.insert(this.editableArea).after(textareaElement);
12173
12174       this._createWysiwygFormField();
12175     },
12176
12177     // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
12178     _createWysiwygFormField: function() {
12179         if (this.textarea.element.form) {
12180           var hiddenField = document.createElement("input");
12181           hiddenField.type   = "hidden";
12182           hiddenField.name   = "_wysihtml5_mode";
12183           hiddenField.value  = 1;
12184           dom.insert(hiddenField).after(this.textarea.element);
12185         }
12186     },
12187
12188     _create: function() {
12189       var that = this;
12190       this.doc                = this.sandbox.getDocument();
12191       this.element            = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
12192       if (!this.config.noTextarea) {
12193           this.textarea           = this.parent.textarea;
12194           this.element.innerHTML  = this.textarea.getValue(true, false);
12195       } else {
12196           this.cleanUp(); // cleans contenteditable on initiation as it may contain html
12197       }
12198
12199       // Make sure our selection handler is ready
12200       this.selection = new wysihtml5.Selection(this.parent, this.element, this.config.uneditableContainerClassname);
12201
12202       // Make sure commands dispatcher is ready
12203       this.commands  = new wysihtml5.Commands(this.parent);
12204
12205       if (!this.config.noTextarea) {
12206           dom.copyAttributes([
12207               "className", "spellcheck", "title", "lang", "dir", "accessKey"
12208           ]).from(this.textarea.element).to(this.element);
12209       }
12210
12211       dom.addClass(this.element, this.config.composerClassName);
12212       //
12213       // Make the editor look like the original textarea, by syncing styles
12214       if (this.config.style && !this.config.contentEditableMode) {
12215         this.style();
12216       }
12217
12218       this.observe();
12219
12220       var name = this.config.name;
12221       if (name) {
12222         dom.addClass(this.element, name);
12223         if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
12224       }
12225
12226       this.enable();
12227
12228       if (!this.config.noTextarea && this.textarea.element.disabled) {
12229         this.disable();
12230       }
12231
12232       // Simulate html5 placeholder attribute on contentEditable element
12233       var placeholderText = typeof(this.config.placeholder) === "string"
12234         ? this.config.placeholder
12235         : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
12236       if (placeholderText) {
12237         dom.simulatePlaceholder(this.parent, this, placeholderText);
12238       }
12239
12240       // Make sure that the browser avoids using inline styles whenever possible
12241       this.commands.exec("styleWithCSS", false);
12242
12243       this._initAutoLinking();
12244       this._initObjectResizing();
12245       this._initUndoManager();
12246       this._initLineBreaking();
12247
12248       // Simulate html5 autofocus on contentEditable element
12249       // This doesn't work on IOS (5.1.1)
12250       if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
12251         setTimeout(function() { that.focus(true); }, 100);
12252       }
12253
12254       // IE sometimes leaves a single paragraph, which can't be removed by the user
12255       if (!browser.clearsContentEditableCorrectly()) {
12256         wysihtml5.quirks.ensureProperClearing(this);
12257       }
12258
12259       // Set up a sync that makes sure that textarea and editor have the same content
12260       if (this.initSync && this.config.sync) {
12261         this.initSync();
12262       }
12263
12264       // Okay hide the textarea, we are ready to go
12265       if (!this.config.noTextarea) { this.textarea.hide(); }
12266
12267       // Fire global (before-)load event
12268       this.parent.fire("beforeload").fire("load");
12269     },
12270
12271     _initAutoLinking: function() {
12272       var that                           = this,
12273           supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
12274           supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
12275       if (supportsDisablingOfAutoLinking) {
12276         this.commands.exec("autoUrlDetect", false);
12277       }
12278
12279       if (!this.config.autoLink) {
12280         return;
12281       }
12282
12283       // Only do the auto linking by ourselves when the browser doesn't support auto linking
12284       // OR when he supports auto linking but we were able to turn it off (IE9+)
12285       if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
12286         this.parent.on("newword:composer", function() {
12287           if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
12288             that.selection.executeAndRestore(function(startContainer, endContainer) {
12289               var uneditables = that.element.querySelectorAll("." + that.config.uneditableContainerClassname),
12290                   isInUneditable = false;
12291
12292               for (var i = uneditables.length; i--;) {
12293                 if (wysihtml5.dom.contains(uneditables[i], endContainer)) {
12294                   isInUneditable = true;
12295                 }
12296               }
12297
12298               if (!isInUneditable) dom.autoLink(endContainer.parentNode, [that.config.uneditableContainerClassname]);
12299             });
12300           }
12301         });
12302
12303         dom.observe(this.element, "blur", function() {
12304           dom.autoLink(that.element, [that.config.uneditableContainerClassname]);
12305         });
12306       }
12307
12308       // Assuming we have the following:
12309       //  <a href="http://www.google.de">http://www.google.de</a>
12310       // If a user now changes the url in the innerHTML we want to make sure that
12311       // it's synchronized with the href attribute (as long as the innerHTML is still a url)
12312       var // Use a live NodeList to check whether there are any links in the document
12313           links           = this.sandbox.getDocument().getElementsByTagName("a"),
12314           // The autoLink helper method reveals a reg exp to detect correct urls
12315           urlRegExp       = dom.autoLink.URL_REG_EXP,
12316           getTextContent  = function(element) {
12317             var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
12318             if (textContent.substr(0, 4) === "www.") {
12319               textContent = "http://" + textContent;
12320             }
12321             return textContent;
12322           };
12323
12324       dom.observe(this.element, "keydown", function(event) {
12325         if (!links.length) {
12326           return;
12327         }
12328
12329         var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
12330             link         = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
12331             textContent;
12332
12333         if (!link) {
12334           return;
12335         }
12336
12337         textContent = getTextContent(link);
12338         // keydown is fired before the actual content is changed
12339         // therefore we set a timeout to change the href
12340         setTimeout(function() {
12341           var newTextContent = getTextContent(link);
12342           if (newTextContent === textContent) {
12343             return;
12344           }
12345
12346           // Only set href when new href looks like a valid url
12347           if (newTextContent.match(urlRegExp)) {
12348             link.setAttribute("href", newTextContent);
12349           }
12350         }, 0);
12351       });
12352     },
12353
12354     _initObjectResizing: function() {
12355       this.commands.exec("enableObjectResizing", true);
12356
12357       // IE sets inline styles after resizing objects
12358       // The following lines make sure that the width/height css properties
12359       // are copied over to the width/height attributes
12360       if (browser.supportsEvent("resizeend")) {
12361         var properties        = ["width", "height"],
12362             propertiesLength  = properties.length,
12363             element           = this.element;
12364
12365         dom.observe(element, "resizeend", function(event) {
12366           var target = event.target || event.srcElement,
12367               style  = target.style,
12368               i      = 0,
12369               property;
12370
12371           if (target.nodeName !== "IMG") {
12372             return;
12373           }
12374
12375           for (; i<propertiesLength; i++) {
12376             property = properties[i];
12377             if (style[property]) {
12378               target.setAttribute(property, parseInt(style[property], 10));
12379               style[property] = "";
12380             }
12381           }
12382
12383           // After resizing IE sometimes forgets to remove the old resize handles
12384           wysihtml5.quirks.redraw(element);
12385         });
12386       }
12387     },
12388
12389     _initUndoManager: function() {
12390       this.undoManager = new wysihtml5.UndoManager(this.parent);
12391     },
12392
12393     _initLineBreaking: function() {
12394       var that                              = this,
12395           USE_NATIVE_LINE_BREAK_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
12396           LIST_TAGS                         = ["UL", "OL", "MENU"];
12397
12398       function adjust(selectedNode) {
12399         var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
12400         if (parentElement && dom.contains(that.element, parentElement)) {
12401           that.selection.executeAndRestore(function() {
12402             if (that.config.useLineBreaks) {
12403               dom.replaceWithChildNodes(parentElement);
12404             } else if (parentElement.nodeName !== "P") {
12405               dom.renameElement(parentElement, "p");
12406             }
12407           });
12408         }
12409       }
12410
12411       if (!this.config.useLineBreaks) {
12412         dom.observe(this.element, ["focus", "keydown"], function() {
12413           if (that.isEmpty()) {
12414             var paragraph = that.doc.createElement("P");
12415             that.element.innerHTML = "";
12416             that.element.appendChild(paragraph);
12417             if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
12418               paragraph.innerHTML = "<br>";
12419               that.selection.setBefore(paragraph.firstChild);
12420             } else {
12421               that.selection.selectNode(paragraph, true);
12422             }
12423           }
12424         });
12425       }
12426
12427       // Under certain circumstances Chrome + Safari create nested <p> or <hX> tags after paste
12428       // Inserting an invisible white space in front of it fixes the issue
12429       // This is too hacky and causes selection not to replace content on paste in chrome
12430      /* if (browser.createsNestedInvalidMarkupAfterPaste()) {
12431         dom.observe(this.element, "paste", function(event) {
12432           var invisibleSpace = that.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
12433           that.selection.insertNode(invisibleSpace);
12434         });
12435       }*/
12436
12437
12438       dom.observe(this.element, "keydown", function(event) {
12439         var keyCode = event.keyCode;
12440
12441         if (event.shiftKey) {
12442           return;
12443         }
12444
12445         if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) {
12446           return;
12447         }
12448         var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { nodeName: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
12449         if (blockElement) {
12450           setTimeout(function() {
12451             // Unwrap paragraph after leaving a list or a H1-6
12452             var selectedNode = that.selection.getSelectedNode(),
12453                 list;
12454
12455             if (blockElement.nodeName === "LI") {
12456               if (!selectedNode) {
12457                 return;
12458               }
12459
12460               list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2);
12461
12462               if (!list) {
12463                 adjust(selectedNode);
12464               }
12465             }
12466
12467             if (keyCode === wysihtml5.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
12468               adjust(selectedNode);
12469             }
12470           }, 0);
12471           return;
12472         }
12473
12474         if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
12475           event.preventDefault();
12476           that.commands.exec("insertLineBreak");
12477
12478         }
12479       });
12480     }
12481   });
12482 })(wysihtml5);
12483 ;(function(wysihtml5) {
12484   var dom             = wysihtml5.dom,
12485       doc             = document,
12486       win             = window,
12487       HOST_TEMPLATE   = doc.createElement("div"),
12488       /**
12489        * Styles to copy from textarea to the composer element
12490        */
12491       TEXT_FORMATTING = [
12492         "background-color",
12493         "color", "cursor",
12494         "font-family", "font-size", "font-style", "font-variant", "font-weight",
12495         "line-height", "letter-spacing",
12496         "text-align", "text-decoration", "text-indent", "text-rendering",
12497         "word-break", "word-wrap", "word-spacing"
12498       ],
12499       /**
12500        * Styles to copy from textarea to the iframe
12501        */
12502       BOX_FORMATTING = [
12503         "background-color",
12504         "border-collapse",
12505         "border-bottom-color", "border-bottom-style", "border-bottom-width",
12506         "border-left-color", "border-left-style", "border-left-width",
12507         "border-right-color", "border-right-style", "border-right-width",
12508         "border-top-color", "border-top-style", "border-top-width",
12509         "clear", "display", "float",
12510         "margin-bottom", "margin-left", "margin-right", "margin-top",
12511         "outline-color", "outline-offset", "outline-width", "outline-style",
12512         "padding-left", "padding-right", "padding-top", "padding-bottom",
12513         "position", "top", "left", "right", "bottom", "z-index",
12514         "vertical-align", "text-align",
12515         "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
12516         "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
12517         "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
12518         "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
12519         "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
12520         "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
12521         "width", "height"
12522       ],
12523       ADDITIONAL_CSS_RULES = [
12524         "html                 { height: 100%; }",
12525         "body                 { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }",
12526         "body > p:first-child { margin-top: 0; }",
12527         "._wysihtml5-temp     { display: none; }",
12528         wysihtml5.browser.isGecko ?
12529           "body.placeholder { color: graytext !important; }" :
12530           "body.placeholder { color: #a9a9a9 !important; }",
12531         // Ensure that user see's broken images and can delete them
12532         "img:-moz-broken      { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
12533       ];
12534
12535   /**
12536    * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
12537    * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
12538    *
12539    * Other browsers need a more hacky way: (pssst don't tell my mama)
12540    * In order to prevent the element being scrolled into view when focusing it, we simply
12541    * move it out of the scrollable area, focus it, and reset it's position
12542    */
12543   var focusWithoutScrolling = function(element) {
12544     if (element.setActive) {
12545       // Following line could cause a js error when the textarea is invisible
12546       // See https://github.com/xing/wysihtml5/issues/9
12547       try { element.setActive(); } catch(e) {}
12548     } else {
12549       var elementStyle = element.style,
12550           originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
12551           originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
12552           originalStyles = {
12553             position:         elementStyle.position,
12554             top:              elementStyle.top,
12555             left:             elementStyle.left,
12556             WebkitUserSelect: elementStyle.WebkitUserSelect
12557           };
12558
12559       dom.setStyles({
12560         position:         "absolute",
12561         top:              "-99999px",
12562         left:             "-99999px",
12563         // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
12564         WebkitUserSelect: "none"
12565       }).on(element);
12566
12567       element.focus();
12568
12569       dom.setStyles(originalStyles).on(element);
12570
12571       if (win.scrollTo) {
12572         // Some browser extensions unset this method to prevent annoyances
12573         // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
12574         // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
12575         win.scrollTo(originalScrollLeft, originalScrollTop);
12576       }
12577     }
12578   };
12579
12580
12581   wysihtml5.views.Composer.prototype.style = function() {
12582     var that                  = this,
12583         originalActiveElement = doc.querySelector(":focus"),
12584         textareaElement       = this.textarea.element,
12585         hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
12586         originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder"),
12587         originalDisplayValue  = textareaElement.style.display,
12588         originalDisabled      = textareaElement.disabled,
12589         displayValueForCopying;
12590
12591     this.focusStylesHost      = HOST_TEMPLATE.cloneNode(false);
12592     this.blurStylesHost       = HOST_TEMPLATE.cloneNode(false);
12593     this.disabledStylesHost   = HOST_TEMPLATE.cloneNode(false);
12594
12595     // Remove placeholder before copying (as the placeholder has an affect on the computed style)
12596     if (hasPlaceholder) {
12597       textareaElement.removeAttribute("placeholder");
12598     }
12599
12600     if (textareaElement === originalActiveElement) {
12601       textareaElement.blur();
12602     }
12603
12604     // enable for copying styles
12605     textareaElement.disabled = false;
12606
12607     // set textarea to display="none" to get cascaded styles via getComputedStyle
12608     textareaElement.style.display = displayValueForCopying = "none";
12609
12610     if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
12611         (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
12612       textareaElement.style.display = displayValueForCopying = originalDisplayValue;
12613     }
12614
12615     // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
12616     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
12617
12618     // --------- editor styles ---------
12619     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
12620
12621     // --------- apply standard rules ---------
12622     dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
12623
12624     // --------- :disabled styles ---------
12625     textareaElement.disabled = true;
12626     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
12627     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
12628     textareaElement.disabled = originalDisabled;
12629
12630     // --------- :focus styles ---------
12631     textareaElement.style.display = originalDisplayValue;
12632     focusWithoutScrolling(textareaElement);
12633     textareaElement.style.display = displayValueForCopying;
12634
12635     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12636     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12637
12638     // reset textarea
12639     textareaElement.style.display = originalDisplayValue;
12640
12641     dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
12642
12643     // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
12644     // this is needed for when the change_view event is fired where the iframe is hidden and then
12645     // the blur event fires and re-displays it
12646     var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
12647
12648     // --------- restore focus ---------
12649     if (originalActiveElement) {
12650       originalActiveElement.focus();
12651     } else {
12652       textareaElement.blur();
12653     }
12654
12655     // --------- restore placeholder ---------
12656     if (hasPlaceholder) {
12657       textareaElement.setAttribute("placeholder", originalPlaceholder);
12658     }
12659
12660     // --------- Sync focus/blur styles ---------
12661     this.parent.on("focus:composer", function() {
12662       dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
12663       dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
12664     });
12665
12666     this.parent.on("blur:composer", function() {
12667       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
12668       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
12669     });
12670
12671     this.parent.observe("disable:composer", function() {
12672       dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
12673       dom.copyStyles(TEXT_FORMATTING)     .from(that.disabledStylesHost).to(that.element);
12674     });
12675
12676     this.parent.observe("enable:composer", function() {
12677       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
12678       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
12679     });
12680
12681     return this;
12682   };
12683 })(wysihtml5);
12684 ;/**
12685  * Taking care of events
12686  *  - Simulating 'change' event on contentEditable element
12687  *  - Handling drag & drop logic
12688  *  - Catch paste events
12689  *  - Dispatch proprietary newword:composer event
12690  *  - Keyboard shortcuts
12691  */
12692 (function(wysihtml5) {
12693   var dom       = wysihtml5.dom,
12694       browser   = wysihtml5.browser,
12695       /**
12696        * Map keyCodes to query commands
12697        */
12698       shortcuts = {
12699         "66": "bold",     // B
12700         "73": "italic",   // I
12701         "85": "underline" // U
12702       };
12703
12704   var deleteAroundEditable = function(selection, uneditable, element) {
12705     // merge node with previous node from uneditable
12706     var prevNode = selection.getPreviousNode(uneditable, true),
12707         curNode = selection.getSelectedNode();
12708
12709     if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; }
12710     if (prevNode) {
12711       if (curNode.nodeType == 1) {
12712         var first = curNode.firstChild;
12713
12714         if (prevNode.nodeType == 1) {
12715           while (curNode.firstChild) {
12716             prevNode.appendChild(curNode.firstChild);
12717           }
12718         } else {
12719           while (curNode.firstChild) {
12720             uneditable.parentNode.insertBefore(curNode.firstChild, uneditable);
12721           }
12722         }
12723         if (curNode.parentNode) {
12724           curNode.parentNode.removeChild(curNode);
12725         }
12726         selection.setBefore(first);
12727       } else {
12728         if (prevNode.nodeType == 1) {
12729           prevNode.appendChild(curNode);
12730         } else {
12731           uneditable.parentNode.insertBefore(curNode, uneditable);
12732         }
12733         selection.setBefore(curNode);
12734       }
12735     }
12736   };
12737
12738   var handleDeleteKeyPress = function(event, selection, element, composer) {
12739     if (selection.isCollapsed()) {
12740       if (selection.caretIsInTheBeginnig('LI')) {
12741         event.preventDefault();
12742         composer.commands.exec('outdentList');
12743       } else if (selection.caretIsInTheBeginnig()) {
12744         event.preventDefault();
12745       } else {
12746
12747         if (selection.caretIsFirstInSelection() &&
12748             selection.getPreviousNode() &&
12749             selection.getPreviousNode().nodeName &&
12750             (/^H\d$/gi).test(selection.getPreviousNode().nodeName)
12751         ) {
12752           var prevNode = selection.getPreviousNode();
12753           event.preventDefault();
12754           if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
12755             // heading is empty
12756             prevNode.parentNode.removeChild(prevNode);
12757           } else {
12758             var range = prevNode.ownerDocument.createRange();
12759             range.selectNodeContents(prevNode);
12760             range.collapse(false);
12761             selection.setSelection(range);
12762           }
12763         }
12764
12765         var beforeUneditable = selection.caretIsBeforeUneditable();
12766         // Do a special delete if caret would delete uneditable
12767         if (beforeUneditable) {
12768           event.preventDefault();
12769           deleteAroundEditable(selection, beforeUneditable, element);
12770         }
12771       }
12772     } else {
12773       if (selection.containsUneditable()) {
12774         event.preventDefault();
12775         selection.deleteContents();
12776       }
12777     }
12778   };
12779
12780   var handleTabKeyDown = function(composer, element) {
12781     if (!composer.selection.isCollapsed()) {
12782       composer.selection.deleteContents();
12783     } else if (composer.selection.caretIsInTheBeginnig('LI')) {
12784       if (composer.commands.exec('indentList')) return;
12785     }
12786
12787     // Is &emsp; close enough to tab. Could not find enough counter arguments for now.
12788     composer.commands.exec("insertHTML", "&emsp;");
12789   };
12790
12791   wysihtml5.views.Composer.prototype.observe = function() {
12792     var that                = this,
12793         state               = this.getValue(false, false),
12794         container           = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
12795         element             = this.element,
12796         focusBlurElement    = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? element : this.sandbox.getWindow(),
12797         pasteEvents         = ["drop", "paste", "beforepaste"],
12798         interactionEvents   = ["drop", "paste", "mouseup", "focus", "keyup"];
12799
12800     // --------- destroy:composer event ---------
12801     dom.observe(container, "DOMNodeRemoved", function() {
12802       clearInterval(domNodeRemovedInterval);
12803       that.parent.fire("destroy:composer");
12804     });
12805
12806     // DOMNodeRemoved event is not supported in IE 8
12807     if (!browser.supportsMutationEvents()) {
12808         var domNodeRemovedInterval = setInterval(function() {
12809           if (!dom.contains(document.documentElement, container)) {
12810             clearInterval(domNodeRemovedInterval);
12811             that.parent.fire("destroy:composer");
12812           }
12813         }, 250);
12814     }
12815
12816     // --------- User interaction tracking --
12817
12818     dom.observe(focusBlurElement, interactionEvents, function() {
12819       setTimeout(function() {
12820         that.parent.fire("interaction").fire("interaction:composer");
12821       }, 0);
12822     });
12823
12824
12825     if (this.config.handleTables) {
12826       if(!this.tableClickHandle && this.doc.execCommand && wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) {
12827         if (this.sandbox.getIframe) {
12828           this.tableClickHandle = dom.observe(container , ["focus", "mouseup", "mouseover"], function() {
12829             that.doc.execCommand("enableObjectResizing", false, "false");
12830             that.doc.execCommand("enableInlineTableEditing", false, "false");
12831             that.tableClickHandle.stop();
12832           });
12833         } else {
12834           setTimeout(function() {
12835             that.doc.execCommand("enableObjectResizing", false, "false");
12836             that.doc.execCommand("enableInlineTableEditing", false, "false");
12837           }, 0);
12838         }
12839       }
12840       this.tableSelection = wysihtml5.quirks.tableCellsSelection(element, that.parent);
12841     }
12842
12843     // --------- Focus & blur logic ---------
12844     dom.observe(focusBlurElement, "focus", function(event) {
12845       that.parent.fire("focus", event).fire("focus:composer", event);
12846
12847       // Delay storing of state until all focus handler are fired
12848       // especially the one which resets the placeholder
12849       setTimeout(function() { state = that.getValue(false, false); }, 0);
12850     });
12851
12852     dom.observe(focusBlurElement, "blur", function(event) {
12853       if (state !== that.getValue(false, false)) {
12854         //create change event if supported (all except IE8)
12855         var changeevent = event;
12856         if(typeof Object.create == 'function') {
12857           changeevent = Object.create(event, { type: { value: 'change' } });
12858         }
12859         that.parent.fire("change", changeevent).fire("change:composer", changeevent);
12860       }
12861       that.parent.fire("blur", event).fire("blur:composer", event);
12862     });
12863
12864     // --------- Drag & Drop logic ---------
12865     dom.observe(element, "dragenter", function() {
12866       that.parent.fire("unset_placeholder");
12867     });
12868
12869     dom.observe(element, pasteEvents, function(event) {
12870       that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12871     });
12872
12873
12874     if (this.config.copyedFromMarking) {
12875       // If supported the copied source is based directly on selection
12876       // Very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection.
12877       dom.observe(element, "copy", function(event) {
12878         if (event.clipboardData) {
12879           event.clipboardData.setData("text/html", that.config.copyedFromMarking + that.selection.getHtml());
12880           event.preventDefault();
12881         }
12882         that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12883       });
12884     }
12885
12886     // --------- neword event ---------
12887     dom.observe(element, "keyup", function(event) {
12888       var keyCode = event.keyCode;
12889       if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
12890         that.parent.fire("newword:composer");
12891       }
12892     });
12893
12894     this.parent.on("paste:composer", function() {
12895       setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
12896     });
12897
12898     // --------- Make sure that images are selected when clicking on them ---------
12899     if (!browser.canSelectImagesInContentEditable()) {
12900       dom.observe(element, "mousedown", function(event) {
12901         var target = event.target;
12902         var allImages = element.querySelectorAll('img'),
12903             notMyImages = element.querySelectorAll('.' + that.config.uneditableContainerClassname + ' img'),
12904             myImages = wysihtml5.lang.array(allImages).without(notMyImages);
12905
12906         if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) {
12907           that.selection.selectNode(target);
12908         }
12909       });
12910     }
12911
12912     if (!browser.canSelectImagesInContentEditable()) {
12913         dom.observe(element, "drop", function(event) {
12914             // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
12915             setTimeout(function() {
12916                 that.selection.getSelection().removeAllRanges();
12917             }, 0);
12918         });
12919     }
12920
12921     if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) {
12922       dom.observe(element, "keydown", function(event) {
12923         if (!event.metaKey && !event.ctrlKey) {
12924           return;
12925         }
12926
12927         var keyCode   = event.keyCode,
12928             win       = element.ownerDocument.defaultView,
12929             selection = win.getSelection();
12930
12931         if (keyCode === 37 || keyCode === 39) {
12932           if (keyCode === 37) {
12933             selection.modify("extend", "left", "lineboundary");
12934             if (!event.shiftKey) {
12935               selection.collapseToStart();
12936             }
12937           }
12938           if (keyCode === 39) {
12939             selection.modify("extend", "right", "lineboundary");
12940             if (!event.shiftKey) {
12941               selection.collapseToEnd();
12942             }
12943           }
12944           event.preventDefault();
12945         }
12946       });
12947     }
12948
12949     // --------- Shortcut logic ---------
12950     dom.observe(element, "keydown", function(event) {
12951       var keyCode  = event.keyCode,
12952           command  = shortcuts[keyCode];
12953       if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
12954         that.commands.exec(command);
12955         event.preventDefault();
12956       }
12957       if (keyCode === 8) {
12958         // delete key
12959         handleDeleteKeyPress(event, that.selection, element, that);
12960       } else if (that.config.handleTabKey && keyCode === 9) {
12961         event.preventDefault();
12962         handleTabKeyDown(that, element);
12963       }
12964     });
12965
12966     // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
12967     dom.observe(element, "keydown", function(event) {
12968       var target  = that.selection.getSelectedNode(true),
12969           keyCode = event.keyCode,
12970           parent;
12971       if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
12972         parent = target.parentNode;
12973         // delete the <img>
12974         parent.removeChild(target);
12975         // and it's parent <a> too if it hasn't got any other child nodes
12976         if (parent.nodeName === "A" && !parent.firstChild) {
12977           parent.parentNode.removeChild(parent);
12978         }
12979
12980         setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
12981         event.preventDefault();
12982       }
12983     });
12984
12985     // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the <body>) ---------
12986     if (!this.config.contentEditableMode && browser.hasIframeFocusIssue()) {
12987       dom.observe(container, "focus", function() {
12988         setTimeout(function() {
12989           if (that.doc.querySelector(":focus") !== that.element) {
12990             that.focus();
12991           }
12992         }, 0);
12993       });
12994
12995       dom.observe(this.element, "blur", function() {
12996         setTimeout(function() {
12997           that.selection.getSelection().removeAllRanges();
12998         }, 0);
12999       });
13000     }
13001
13002     // --------- Show url in tooltip when hovering links or images ---------
13003     var titlePrefixes = {
13004       IMG: "Image: ",
13005       A:   "Link: "
13006     };
13007
13008     dom.observe(element, "mouseover", function(event) {
13009       var target   = event.target,
13010           nodeName = target.nodeName,
13011           title;
13012       if (nodeName !== "A" && nodeName !== "IMG") {
13013         return;
13014       }
13015       var hasTitle = target.hasAttribute("title");
13016       if(!hasTitle){
13017         title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
13018         target.setAttribute("title", title);
13019       }
13020     });
13021   };
13022 })(wysihtml5);
13023 ;/**
13024  * Class that takes care that the value of the composer and the textarea is always in sync
13025  */
13026 (function(wysihtml5) {
13027   var INTERVAL = 400;
13028
13029   wysihtml5.views.Synchronizer = Base.extend(
13030     /** @scope wysihtml5.views.Synchronizer.prototype */ {
13031
13032     constructor: function(editor, textarea, composer) {
13033       this.editor   = editor;
13034       this.textarea = textarea;
13035       this.composer = composer;
13036
13037       this._observe();
13038     },
13039
13040     /**
13041      * Sync html from composer to textarea
13042      * Takes care of placeholders
13043      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
13044      */
13045     fromComposerToTextarea: function(shouldParseHtml) {
13046       this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
13047     },
13048
13049     /**
13050      * Sync value of textarea to composer
13051      * Takes care of placeholders
13052      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
13053      */
13054     fromTextareaToComposer: function(shouldParseHtml) {
13055       var textareaValue = this.textarea.getValue(false, false);
13056       if (textareaValue) {
13057         this.composer.setValue(textareaValue, shouldParseHtml);
13058       } else {
13059         this.composer.clear();
13060         this.editor.fire("set_placeholder");
13061       }
13062     },
13063
13064     /**
13065      * Invoke syncing based on view state
13066      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
13067      */
13068     sync: function(shouldParseHtml) {
13069       if (this.editor.currentView.name === "textarea") {
13070         this.fromTextareaToComposer(shouldParseHtml);
13071       } else {
13072         this.fromComposerToTextarea(shouldParseHtml);
13073       }
13074     },
13075
13076     /**
13077      * Initializes interval-based syncing
13078      * also makes sure that on-submit the composer's content is synced with the textarea
13079      * immediately when the form gets submitted
13080      */
13081     _observe: function() {
13082       var interval,
13083           that          = this,
13084           form          = this.textarea.element.form,
13085           startInterval = function() {
13086             interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
13087           },
13088           stopInterval  = function() {
13089             clearInterval(interval);
13090             interval = null;
13091           };
13092
13093       startInterval();
13094
13095       if (form) {
13096         // If the textarea is in a form make sure that after onreset and onsubmit the composer
13097         // has the correct state
13098         wysihtml5.dom.observe(form, "submit", function() {
13099           that.sync(true);
13100         });
13101         wysihtml5.dom.observe(form, "reset", function() {
13102           setTimeout(function() { that.fromTextareaToComposer(); }, 0);
13103         });
13104       }
13105
13106       this.editor.on("change_view", function(view) {
13107         if (view === "composer" && !interval) {
13108           that.fromTextareaToComposer(true);
13109           startInterval();
13110         } else if (view === "textarea") {
13111           that.fromComposerToTextarea(true);
13112           stopInterval();
13113         }
13114       });
13115
13116       this.editor.on("destroy:composer", stopInterval);
13117     }
13118   });
13119 })(wysihtml5);
13120 ;wysihtml5.views.Textarea = wysihtml5.views.View.extend(
13121   /** @scope wysihtml5.views.Textarea.prototype */ {
13122   name: "textarea",
13123
13124   constructor: function(parent, textareaElement, config) {
13125     this.base(parent, textareaElement, config);
13126
13127     this._observe();
13128   },
13129
13130   clear: function() {
13131     this.element.value = "";
13132   },
13133
13134   getValue: function(parse) {
13135     var value = this.isEmpty() ? "" : this.element.value;
13136     if (parse !== false) {
13137       value = this.parent.parse(value);
13138     }
13139     return value;
13140   },
13141
13142   setValue: function(html, parse) {
13143     if (parse) {
13144       html = this.parent.parse(html);
13145     }
13146     this.element.value = html;
13147   },
13148
13149   cleanUp: function() {
13150       var html = this.parent.parse(this.element.value);
13151       this.element.value = html;
13152   },
13153
13154   hasPlaceholderSet: function() {
13155     var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
13156         placeholderText     = this.element.getAttribute("placeholder") || null,
13157         value               = this.element.value,
13158         isEmpty             = !value;
13159     return (supportsPlaceholder && isEmpty) || (value === placeholderText);
13160   },
13161
13162   isEmpty: function() {
13163     return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
13164   },
13165
13166   _observe: function() {
13167     var element = this.element,
13168         parent  = this.parent,
13169         eventMapping = {
13170           focusin:  "focus",
13171           focusout: "blur"
13172         },
13173         /**
13174          * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
13175          * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
13176          */
13177         events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
13178
13179     parent.on("beforeload", function() {
13180       wysihtml5.dom.observe(element, events, function(event) {
13181         var eventName = eventMapping[event.type] || event.type;
13182         parent.fire(eventName).fire(eventName + ":textarea");
13183       });
13184
13185       wysihtml5.dom.observe(element, ["paste", "drop"], function() {
13186         setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
13187       });
13188     });
13189   }
13190 });
13191 ;/**
13192  * WYSIHTML5 Editor
13193  *
13194  * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
13195  * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
13196  *
13197  * @events
13198  *    load
13199  *    beforeload (for internal use only)
13200  *    focus
13201  *    focus:composer
13202  *    focus:textarea
13203  *    blur
13204  *    blur:composer
13205  *    blur:textarea
13206  *    change
13207  *    change:composer
13208  *    change:textarea
13209  *    paste
13210  *    paste:composer
13211  *    paste:textarea
13212  *    newword:composer
13213  *    destroy:composer
13214  *    undo:composer
13215  *    redo:composer
13216  *    beforecommand:composer
13217  *    aftercommand:composer
13218  *    enable:composer
13219  *    disable:composer
13220  *    change_view
13221  */
13222 (function(wysihtml5) {
13223   var undef;
13224
13225   var defaultConfig = {
13226     // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
13227     name:                 undef,
13228     // Whether the editor should look like the textarea (by adopting styles)
13229     style:                true,
13230     // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
13231     toolbar:              undef,
13232     // Whether toolbar is displayed after init by script automatically.
13233     // Can be set to false if toolobar is set to display only on editable area focus
13234     showToolbarAfterInit: true,
13235     // Whether urls, entered by the user should automatically become clickable-links
13236     autoLink:             true,
13237     // Includes table editing events and cell selection tracking
13238     handleTables:         true,
13239     // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
13240     handleTabKey:         true,
13241     // Object which includes parser rules to apply when html gets cleaned
13242     // See parser_rules/*.js for examples
13243     parserRules:          { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
13244     // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
13245     pasteParserRulesets: null,
13246     // Parser method to use when the user inserts content
13247     parser:               wysihtml5.dom.parse,
13248     // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
13249     composerClassName:    "wysihtml5-editor",
13250     // Class name to add to the body when the wysihtml5 editor is supported
13251     bodyClassName:        "wysihtml5-supported",
13252     // By default wysihtml5 will insert a <br> for line breaks, set this to false to use <p>
13253     useLineBreaks:        true,
13254     // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
13255     stylesheets:          [],
13256     // Placeholder text to use, defaults to the placeholder attribute on the textarea element
13257     placeholderText:      undef,
13258     // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
13259     supportTouchDevices:  true,
13260     // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
13261     cleanUp:              true,
13262     // Whether to use div instead of secure iframe
13263     contentEditableMode: false,
13264     // Classname of container that editor should not touch and pass through
13265     // Pass false to disable
13266     uneditableContainerClassname: "wysihtml5-uneditable-container",
13267     // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
13268     // Also copied source is based directly on selection - 
13269     // (very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection).
13270     // If falsy value is passed source override is also disabled
13271     copyedFromMarking: '<meta name="copied-from" content="wysihtml5">'
13272   };
13273
13274   wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
13275     /** @scope wysihtml5.Editor.prototype */ {
13276     constructor: function(editableElement, config) {
13277       this.editableElement  = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
13278       this.config           = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
13279       this._isCompatible    = wysihtml5.browser.supported();
13280
13281       if (this.editableElement.nodeName.toLowerCase() != "textarea") {
13282           this.config.contentEditableMode = true;
13283           this.config.noTextarea = true;
13284       }
13285       if (!this.config.noTextarea) {
13286           this.textarea         = new wysihtml5.views.Textarea(this, this.editableElement, this.config);
13287           this.currentView      = this.textarea;
13288       }
13289
13290       // Sort out unsupported/unwanted browsers here
13291       if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
13292         var that = this;
13293         setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
13294         return;
13295       }
13296
13297       // Add class name to body, to indicate that the editor is supported
13298       wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
13299
13300       this.composer = new wysihtml5.views.Composer(this, this.editableElement, this.config);
13301       this.currentView = this.composer;
13302
13303       if (typeof(this.config.parser) === "function") {
13304         this._initParser();
13305       }
13306
13307       this.on("beforeload", this.handleBeforeLoad);
13308     },
13309
13310     handleBeforeLoad: function() {
13311         if (!this.config.noTextarea) {
13312             this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
13313         }
13314         if (this.config.toolbar) {
13315           this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar, this.config.showToolbarAfterInit);
13316         }
13317     },
13318
13319     isCompatible: function() {
13320       return this._isCompatible;
13321     },
13322
13323     clear: function() {
13324       this.currentView.clear();
13325       return this;
13326     },
13327
13328     getValue: function(parse, clearInternals) {
13329       return this.currentView.getValue(parse, clearInternals);
13330     },
13331
13332     setValue: function(html, parse) {
13333       this.fire("unset_placeholder");
13334
13335       if (!html) {
13336         return this.clear();
13337       }
13338
13339       this.currentView.setValue(html, parse);
13340       return this;
13341     },
13342
13343     cleanUp: function() {
13344         this.currentView.cleanUp();
13345     },
13346
13347     focus: function(setToEnd) {
13348       this.currentView.focus(setToEnd);
13349       return this;
13350     },
13351
13352     /**
13353      * Deactivate editor (make it readonly)
13354      */
13355     disable: function() {
13356       this.currentView.disable();
13357       return this;
13358     },
13359
13360     /**
13361      * Activate editor
13362      */
13363     enable: function() {
13364       this.currentView.enable();
13365       return this;
13366     },
13367
13368     isEmpty: function() {
13369       return this.currentView.isEmpty();
13370     },
13371
13372     hasPlaceholderSet: function() {
13373       return this.currentView.hasPlaceholderSet();
13374     },
13375
13376     parse: function(htmlOrElement, clearInternals) {
13377       var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
13378       var returnValue = this.config.parser(htmlOrElement, {
13379         "rules": this.config.parserRules,
13380         "cleanUp": this.config.cleanUp,
13381         "context": parseContext,
13382         "uneditableClass": this.config.uneditableContainerClassname,
13383         "clearInternals" : clearInternals
13384       });
13385       if (typeof(htmlOrElement) === "object") {
13386         wysihtml5.quirks.redraw(htmlOrElement);
13387       }
13388       return returnValue;
13389     },
13390
13391     /**
13392      * Prepare html parser logic
13393      *  - Observes for paste and drop
13394      */
13395     _initParser: function() {
13396       var that = this,
13397           oldHtml,
13398           cleanHtml;
13399
13400       if (wysihtml5.browser.supportsModenPaste()) {
13401         this.on("paste:composer", function(event) {
13402           event.preventDefault();
13403           oldHtml = wysihtml5.dom.getPastedHtml(event);
13404           if (oldHtml) {
13405             that._cleanAndPaste(oldHtml);
13406           }
13407         });
13408
13409       } else {
13410         this.on("beforepaste:composer", function(event) {
13411           event.preventDefault();
13412           wysihtml5.dom.getPastedHtmlWithDiv(that.composer, function(pastedHTML) {
13413             if (pastedHTML) {
13414               that._cleanAndPaste(pastedHTML);
13415             }
13416           });
13417         });
13418
13419       }
13420     },
13421
13422     _cleanAndPaste: function (oldHtml) {
13423       var cleanHtml = wysihtml5.quirks.cleanPastedHTML(oldHtml, {
13424         "referenceNode": this.composer.element,
13425         "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
13426         "uneditableClass": this.config.uneditableContainerClassname
13427       });
13428       this.composer.selection.deleteContents();
13429       this.composer.selection.insertHTML(cleanHtml);
13430     }
13431   });
13432 })(wysihtml5);
13433 ;/**
13434  * Toolbar Dialog
13435  *
13436  * @param {Element} link The toolbar link which causes the dialog to show up
13437  * @param {Element} container The dialog container
13438  *
13439  * @example
13440  *    <!-- Toolbar link -->
13441  *    <a data-wysihtml5-command="insertImage">insert an image</a>
13442  *
13443  *    <!-- Dialog -->
13444  *    <div data-wysihtml5-dialog="insertImage" style="display: none;">
13445  *      <label>
13446  *        URL: <input data-wysihtml5-dialog-field="src" value="http://">
13447  *      </label>
13448  *      <label>
13449  *        Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
13450  *      </label>
13451  *    </div>
13452  *
13453  *    <script>
13454  *      var dialog = new wysihtml5.toolbar.Dialog(
13455  *        document.querySelector("[data-wysihtml5-command='insertImage']"),
13456  *        document.querySelector("[data-wysihtml5-dialog='insertImage']")
13457  *      );
13458  *      dialog.observe("save", function(attributes) {
13459  *        // do something
13460  *      });
13461  *    </script>
13462  */
13463 (function(wysihtml5) {
13464   var dom                     = wysihtml5.dom,
13465       CLASS_NAME_OPENED       = "wysihtml5-command-dialog-opened",
13466       SELECTOR_FORM_ELEMENTS  = "input, select, textarea",
13467       SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
13468       ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
13469
13470
13471   wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
13472     /** @scope wysihtml5.toolbar.Dialog.prototype */ {
13473     constructor: function(link, container) {
13474       this.link       = link;
13475       this.container  = container;
13476     },
13477
13478     _observe: function() {
13479       if (this._observed) {
13480         return;
13481       }
13482
13483       var that = this,
13484           callbackWrapper = function(event) {
13485             var attributes = that._serialize();
13486             if (attributes == that.elementToChange) {
13487               that.fire("edit", attributes);
13488             } else {
13489               that.fire("save", attributes);
13490             }
13491             that.hide();
13492             event.preventDefault();
13493             event.stopPropagation();
13494           };
13495
13496       dom.observe(that.link, "click", function() {
13497         if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
13498           setTimeout(function() { that.hide(); }, 0);
13499         }
13500       });
13501
13502       dom.observe(this.container, "keydown", function(event) {
13503         var keyCode = event.keyCode;
13504         if (keyCode === wysihtml5.ENTER_KEY) {
13505           callbackWrapper(event);
13506         }
13507         if (keyCode === wysihtml5.ESCAPE_KEY) {
13508           that.fire("cancel");
13509           that.hide();
13510         }
13511       });
13512
13513       dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
13514
13515       dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
13516         that.fire("cancel");
13517         that.hide();
13518         event.preventDefault();
13519         event.stopPropagation();
13520       });
13521
13522       var formElements  = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
13523           i             = 0,
13524           length        = formElements.length,
13525           _clearInterval = function() { clearInterval(that.interval); };
13526       for (; i<length; i++) {
13527         dom.observe(formElements[i], "change", _clearInterval);
13528       }
13529
13530       this._observed = true;
13531     },
13532
13533     /**
13534      * Grabs all fields in the dialog and puts them in key=>value style in an object which
13535      * then gets returned
13536      */
13537     _serialize: function() {
13538       var data    = this.elementToChange || {},
13539           fields  = this.container.querySelectorAll(SELECTOR_FIELDS),
13540           length  = fields.length,
13541           i       = 0;
13542
13543       for (; i<length; i++) {
13544         data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
13545       }
13546       return data;
13547     },
13548
13549     /**
13550      * Takes the attributes of the "elementToChange"
13551      * and inserts them in their corresponding dialog input fields
13552      *
13553      * Assume the "elementToChange" looks like this:
13554      *    <a href="http://www.google.com" target="_blank">foo</a>
13555      *
13556      * and we have the following dialog:
13557      *    <input type="text" data-wysihtml5-dialog-field="href" value="">
13558      *    <input type="text" data-wysihtml5-dialog-field="target" value="">
13559      *
13560      * after calling _interpolate() the dialog will look like this
13561      *    <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
13562      *    <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
13563      *
13564      * Basically it adopted the attribute values into the corresponding input fields
13565      *
13566      */
13567     _interpolate: function(avoidHiddenFields) {
13568       var field,
13569           fieldName,
13570           newValue,
13571           focusedElement = document.querySelector(":focus"),
13572           fields         = this.container.querySelectorAll(SELECTOR_FIELDS),
13573           length         = fields.length,
13574           i              = 0;
13575       for (; i<length; i++) {
13576         field = fields[i];
13577
13578         // Never change elements where the user is currently typing in
13579         if (field === focusedElement) {
13580           continue;
13581         }
13582
13583         // Don't update hidden fields
13584         // See https://github.com/xing/wysihtml5/pull/14
13585         if (avoidHiddenFields && field.type === "hidden") {
13586           continue;
13587         }
13588
13589         fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
13590         newValue  = (this.elementToChange && typeof(this.elementToChange) !== 'boolean') ? (this.elementToChange.getAttribute(fieldName) || "") : field.defaultValue;
13591         field.value = newValue;
13592       }
13593     },
13594
13595     /**
13596      * Show the dialog element
13597      */
13598     show: function(elementToChange) {
13599       if (dom.hasClass(this.link, CLASS_NAME_OPENED)) {
13600         return;
13601       }
13602
13603       var that        = this,
13604           firstField  = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
13605       this.elementToChange = elementToChange;
13606       this._observe();
13607       this._interpolate();
13608       if (elementToChange) {
13609         this.interval = setInterval(function() { that._interpolate(true); }, 500);
13610       }
13611       dom.addClass(this.link, CLASS_NAME_OPENED);
13612       this.container.style.display = "";
13613       this.fire("show");
13614       if (firstField && !elementToChange) {
13615         try {
13616           firstField.focus();
13617         } catch(e) {}
13618       }
13619     },
13620
13621     /**
13622      * Hide the dialog element
13623      */
13624     hide: function() {
13625       clearInterval(this.interval);
13626       this.elementToChange = null;
13627       dom.removeClass(this.link, CLASS_NAME_OPENED);
13628       this.container.style.display = "none";
13629       this.fire("hide");
13630     }
13631   });
13632 })(wysihtml5);
13633 ;/**
13634  * Converts speech-to-text and inserts this into the editor
13635  * As of now (2011/03/25) this only is supported in Chrome >= 11
13636  *
13637  * Note that it sends the recorded audio to the google speech recognition api:
13638  * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
13639  *
13640  * Current HTML5 draft can be found here
13641  * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
13642  *
13643  * "Accessing Google Speech API Chrome 11"
13644  * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
13645  */
13646 (function(wysihtml5) {
13647   var dom = wysihtml5.dom;
13648
13649   var linkStyles = {
13650     position: "relative"
13651   };
13652
13653   var wrapperStyles = {
13654     left:     0,
13655     margin:   0,
13656     opacity:  0,
13657     overflow: "hidden",
13658     padding:  0,
13659     position: "absolute",
13660     top:      0,
13661     zIndex:   1
13662   };
13663
13664   var inputStyles = {
13665     cursor:     "inherit",
13666     fontSize:   "50px",
13667     height:     "50px",
13668     marginTop:  "-25px",
13669     outline:    0,
13670     padding:    0,
13671     position:   "absolute",
13672     right:      "-4px",
13673     top:        "50%"
13674   };
13675
13676   var inputAttributes = {
13677     "x-webkit-speech": "",
13678     "speech":          ""
13679   };
13680
13681   wysihtml5.toolbar.Speech = function(parent, link) {
13682     var input = document.createElement("input");
13683     if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
13684       link.style.display = "none";
13685       return;
13686     }
13687     var lang = parent.editor.textarea.element.getAttribute("lang");
13688     if (lang) {
13689       inputAttributes.lang = lang;
13690     }
13691
13692     var wrapper = document.createElement("div");
13693
13694     wysihtml5.lang.object(wrapperStyles).merge({
13695       width:  link.offsetWidth  + "px",
13696       height: link.offsetHeight + "px"
13697     });
13698
13699     dom.insert(input).into(wrapper);
13700     dom.insert(wrapper).into(link);
13701
13702     dom.setStyles(inputStyles).on(input);
13703     dom.setAttributes(inputAttributes).on(input);
13704
13705     dom.setStyles(wrapperStyles).on(wrapper);
13706     dom.setStyles(linkStyles).on(link);
13707
13708     var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
13709     dom.observe(input, eventName, function() {
13710       parent.execCommand("insertText", input.value);
13711       input.value = "";
13712     });
13713
13714     dom.observe(input, "click", function(event) {
13715       if (dom.hasClass(link, "wysihtml5-command-disabled")) {
13716         event.preventDefault();
13717       }
13718
13719       event.stopPropagation();
13720     });
13721   };
13722 })(wysihtml5);
13723 ;/**
13724  * Toolbar
13725  *
13726  * @param {Object} parent Reference to instance of Editor instance
13727  * @param {Element} container Reference to the toolbar container element
13728  *
13729  * @example
13730  *    <div id="toolbar">
13731  *      <a data-wysihtml5-command="createLink">insert link</a>
13732  *      <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
13733  *    </div>
13734  *
13735  *    <script>
13736  *      var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
13737  *    </script>
13738  */
13739 (function(wysihtml5) {
13740   var CLASS_NAME_COMMAND_DISABLED   = "wysihtml5-command-disabled",
13741       CLASS_NAME_COMMANDS_DISABLED  = "wysihtml5-commands-disabled",
13742       CLASS_NAME_COMMAND_ACTIVE     = "wysihtml5-command-active",
13743       CLASS_NAME_ACTION_ACTIVE      = "wysihtml5-action-active",
13744       dom                           = wysihtml5.dom;
13745
13746   wysihtml5.toolbar.Toolbar = Base.extend(
13747     /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
13748     constructor: function(editor, container, showOnInit) {
13749       this.editor     = editor;
13750       this.container  = typeof(container) === "string" ? document.getElementById(container) : container;
13751       this.composer   = editor.composer;
13752
13753       this._getLinks("command");
13754       this._getLinks("action");
13755
13756       this._observe();
13757       if (showOnInit) { this.show(); }
13758
13759       if (editor.config.classNameCommandDisabled != null) {
13760         CLASS_NAME_COMMAND_DISABLED = editor.config.classNameCommandDisabled;
13761       }
13762       if (editor.config.classNameCommandsDisabled != null) {
13763         CLASS_NAME_COMMANDS_DISABLED = editor.config.classNameCommandsDisabled;
13764       }
13765       if (editor.config.classNameCommandActive != null) {
13766         CLASS_NAME_COMMAND_ACTIVE = editor.config.classNameCommandActive;
13767       }
13768       if (editor.config.classNameActionActive != null) {
13769         CLASS_NAME_ACTION_ACTIVE = editor.config.classNameActionActive;
13770       }
13771
13772       var speechInputLinks  = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
13773           length            = speechInputLinks.length,
13774           i                 = 0;
13775       for (; i<length; i++) {
13776         new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
13777       }
13778     },
13779
13780     _getLinks: function(type) {
13781       var links   = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
13782           length  = links.length,
13783           i       = 0,
13784           mapping = this[type + "Mapping"] = {},
13785           link,
13786           group,
13787           name,
13788           value,
13789           dialog;
13790       for (; i<length; i++) {
13791         link    = links[i];
13792         name    = link.getAttribute("data-wysihtml5-" + type);
13793         value   = link.getAttribute("data-wysihtml5-" + type + "-value");
13794         group   = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
13795         dialog  = this._getDialog(link, name);
13796
13797         mapping[name + ":" + value] = {
13798           link:   link,
13799           group:  group,
13800           name:   name,
13801           value:  value,
13802           dialog: dialog,
13803           state:  false
13804         };
13805       }
13806     },
13807
13808     _getDialog: function(link, command) {
13809       var that          = this,
13810           dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
13811           dialog,
13812           caretBookmark;
13813
13814       if (dialogElement) {
13815         if (wysihtml5.toolbar["Dialog_" + command]) {
13816             dialog = new wysihtml5.toolbar["Dialog_" + command](link, dialogElement);
13817         } else {
13818             dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
13819         }
13820
13821         dialog.on("show", function() {
13822           caretBookmark = that.composer.selection.getBookmark();
13823
13824           that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13825         });
13826
13827         dialog.on("save", function(attributes) {
13828           if (caretBookmark) {
13829             that.composer.selection.setBookmark(caretBookmark);
13830           }
13831           that._execCommand(command, attributes);
13832
13833           that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13834         });
13835
13836         dialog.on("cancel", function() {
13837           that.editor.focus(false);
13838           that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13839         });
13840       }
13841       return dialog;
13842     },
13843
13844     /**
13845      * @example
13846      *    var toolbar = new wysihtml5.Toolbar();
13847      *    // Insert a <blockquote> element or wrap current selection in <blockquote>
13848      *    toolbar.execCommand("formatBlock", "blockquote");
13849      */
13850     execCommand: function(command, commandValue) {
13851       if (this.commandsDisabled) {
13852         return;
13853       }
13854
13855       var commandObj = this.commandMapping[command + ":" + commandValue];
13856
13857       // Show dialog when available
13858       if (commandObj && commandObj.dialog && !commandObj.state) {
13859         commandObj.dialog.show();
13860       } else {
13861         this._execCommand(command, commandValue);
13862       }
13863     },
13864
13865     _execCommand: function(command, commandValue) {
13866       // Make sure that composer is focussed (false => don't move caret to the end)
13867       this.editor.focus(false);
13868
13869       this.composer.commands.exec(command, commandValue);
13870       this._updateLinkStates();
13871     },
13872
13873     execAction: function(action) {
13874       var editor = this.editor;
13875       if (action === "change_view") {
13876         if (editor.textarea) {
13877             if (editor.currentView === editor.textarea) {
13878               editor.fire("change_view", "composer");
13879             } else {
13880               editor.fire("change_view", "textarea");
13881             }
13882         }
13883       }
13884       if (action == "showSource") {
13885           editor.fire("showSource");
13886       }
13887     },
13888
13889     _observe: function() {
13890       var that      = this,
13891           editor    = this.editor,
13892           container = this.container,
13893           links     = this.commandLinks.concat(this.actionLinks),
13894           length    = links.length,
13895           i         = 0;
13896
13897       for (; i<length; i++) {
13898         // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
13899         // (you know, a:link { ... } doesn't match anchors with missing href attribute)
13900         if (links[i].nodeName === "A") {
13901           dom.setAttributes({
13902             href:         "javascript:;",
13903             unselectable: "on"
13904           }).on(links[i]);
13905         } else {
13906           dom.setAttributes({ unselectable: "on" }).on(links[i]);
13907         }
13908       }
13909
13910       // Needed for opera and chrome
13911       dom.delegate(container, "[data-wysihtml5-command], [data-wysihtml5-action]", "mousedown", function(event) { event.preventDefault(); });
13912
13913       dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
13914         var link          = this,
13915             command       = link.getAttribute("data-wysihtml5-command"),
13916             commandValue  = link.getAttribute("data-wysihtml5-command-value");
13917         that.execCommand(command, commandValue);
13918         event.preventDefault();
13919       });
13920
13921       dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
13922         var action = this.getAttribute("data-wysihtml5-action");
13923         that.execAction(action);
13924         event.preventDefault();
13925       });
13926
13927       editor.on("interaction:composer", function() {
13928           that._updateLinkStates();
13929       });
13930
13931       editor.on("focus:composer", function() {
13932         that.bookmark = null;
13933       });
13934
13935       if (this.editor.config.handleTables) {
13936           editor.on("tableselect:composer", function() {
13937               that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "";
13938           });
13939           editor.on("tableunselect:composer", function() {
13940               that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "none";
13941           });
13942       }
13943
13944       editor.on("change_view", function(currentView) {
13945         // Set timeout needed in order to let the blur event fire first
13946         if (editor.textarea) {
13947             setTimeout(function() {
13948               that.commandsDisabled = (currentView !== "composer");
13949               that._updateLinkStates();
13950               if (that.commandsDisabled) {
13951                 dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
13952               } else {
13953                 dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
13954               }
13955             }, 0);
13956         }
13957       });
13958     },
13959
13960     _updateLinkStates: function() {
13961
13962       var commandMapping    = this.commandMapping,
13963           actionMapping     = this.actionMapping,
13964           i,
13965           state,
13966           action,
13967           command;
13968       // every millisecond counts... this is executed quite often
13969       for (i in commandMapping) {
13970         command = commandMapping[i];
13971         if (this.commandsDisabled) {
13972           state = false;
13973           dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13974           if (command.group) {
13975             dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13976           }
13977           if (command.dialog) {
13978             command.dialog.hide();
13979           }
13980         } else {
13981           state = this.composer.commands.state(command.name, command.value);
13982           dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
13983           if (command.group) {
13984             dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
13985           }
13986         }
13987         if (command.state === state) {
13988           continue;
13989         }
13990
13991         command.state = state;
13992         if (state) {
13993           dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13994           if (command.group) {
13995             dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13996           }
13997           if (command.dialog) {
13998             if (typeof(state) === "object" || wysihtml5.lang.object(state).isArray()) {
13999
14000               if (!command.dialog.multiselect && wysihtml5.lang.object(state).isArray()) {
14001                 // Grab first and only object/element in state array, otherwise convert state into boolean
14002                 // to avoid showing a dialog for multiple selected elements which may have different attributes
14003                 // eg. when two links with different href are selected, the state will be an array consisting of both link elements
14004                 // but the dialog interface can only update one
14005                 state = state.length === 1 ? state[0] : true;
14006                 command.state = state;
14007               }
14008               command.dialog.show(state);
14009             } else {
14010               command.dialog.hide();
14011             }
14012           }
14013         } else {
14014           dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14015           if (command.group) {
14016             dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14017           }
14018           if (command.dialog) {
14019             command.dialog.hide();
14020           }
14021         }
14022       }
14023
14024       for (i in actionMapping) {
14025         action = actionMapping[i];
14026
14027         if (action.name === "change_view") {
14028           action.state = this.editor.currentView === this.editor.textarea;
14029           if (action.state) {
14030             dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);
14031           } else {
14032             dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
14033           }
14034         }
14035       }
14036     },
14037
14038     show: function() {
14039       this.container.style.display = "";
14040     },
14041
14042     hide: function() {
14043       this.container.style.display = "none";
14044     }
14045   });
14046
14047 })(wysihtml5);
14048 ;(function(wysihtml5) {
14049     wysihtml5.toolbar.Dialog_createTable = wysihtml5.toolbar.Dialog.extend({
14050         show: function(elementToChange) {
14051             this.base(elementToChange);
14052
14053         }
14054
14055     });
14056 })(wysihtml5);
14057 ;(function(wysihtml5) {
14058   var dom                     = wysihtml5.dom,
14059       SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
14060       ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
14061
14062   wysihtml5.toolbar.Dialog_foreColorStyle = wysihtml5.toolbar.Dialog.extend({
14063     multiselect: true,
14064
14065     _serialize: function() {
14066       var data    = {},
14067           fields  = this.container.querySelectorAll(SELECTOR_FIELDS),
14068           length  = fields.length,
14069           i       = 0;
14070
14071       for (; i<length; i++) {
14072         data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
14073       }
14074       return data;
14075     },
14076
14077     _interpolate: function(avoidHiddenFields) {
14078       var field,
14079           fieldName,
14080           newValue,
14081           focusedElement = document.querySelector(":focus"),
14082           fields         = this.container.querySelectorAll(SELECTOR_FIELDS),
14083           length         = fields.length,
14084           i              = 0,
14085           firstElement   = (this.elementToChange) ? ((wysihtml5.lang.object(this.elementToChange).isArray()) ? this.elementToChange[0] : this.elementToChange) : null,
14086           colorStr       = (firstElement) ? firstElement.getAttribute('style') : null,
14087           color          = (colorStr) ? wysihtml5.quirks.styleParser.parseColor(colorStr, "color") : null;
14088
14089       for (; i<length; i++) {
14090         field = fields[i];
14091         // Never change elements where the user is currently typing in
14092         if (field === focusedElement) {
14093           continue;
14094         }
14095         // Don't update hidden fields3
14096         if (avoidHiddenFields && field.type === "hidden") {
14097           continue;
14098         }
14099         if (field.getAttribute(ATTRIBUTE_FIELDS) === "color") {
14100           if (color) {
14101             if (color[3] && color[3] != 1) {
14102               field.value = "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + color[3] + ");";
14103             } else {
14104               field.value = "rgb(" + color[0] + "," + color[1] + "," + color[2] + ");";
14105             }
14106           } else {
14107             field.value = "rgb(0,0,0);";
14108           }
14109         }
14110       }
14111     }
14112
14113   });
14114 })(wysihtml5);
14115 ;(function(wysihtml5) {
14116   var dom                     = wysihtml5.dom,
14117       SELECTOR_FIELDS         = "[data-wysihtml5-dialog-field]",
14118       ATTRIBUTE_FIELDS        = "data-wysihtml5-dialog-field";
14119
14120   wysihtml5.toolbar.Dialog_fontSizeStyle = wysihtml5.toolbar.Dialog.extend({
14121     multiselect: true,
14122
14123     _serialize: function() {
14124       return {"size" : this.container.querySelector('[data-wysihtml5-dialog-field="size"]').value};
14125     },
14126
14127     _interpolate: function(avoidHiddenFields) {
14128       var focusedElement = document.querySelector(":focus"),
14129           field          = this.container.querySelector("[data-wysihtml5-dialog-field='size']"),
14130           firstElement   = (this.elementToChange) ? ((wysihtml5.lang.object(this.elementToChange).isArray()) ? this.elementToChange[0] : this.elementToChange) : null,
14131           styleStr       = (firstElement) ? firstElement.getAttribute('style') : null,
14132           size           = (styleStr) ? wysihtml5.quirks.styleParser.parseFontSize(styleStr) : null;
14133
14134       if (field && field !== focusedElement && size && !(/^\s*$/).test(size)) {
14135         field.value = size;
14136       }
14137     }
14138
14139   });
14140 })(wysihtml5);
14141 /*!
14142
14143  handlebars v1.3.0
14144
14145 Copyright (C) 2011 by Yehuda Katz
14146
14147 Permission is hereby granted, free of charge, to any person obtaining a copy
14148 of this software and associated documentation files (the "Software"), to deal
14149 in the Software without restriction, including without limitation the rights
14150 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14151 copies of the Software, and to permit persons to whom the Software is
14152 furnished to do so, subject to the following conditions:
14153
14154 The above copyright notice and this permission notice shall be included in
14155 all copies or substantial portions of the Software.
14156
14157 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14158 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14159 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14160 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14161 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14162 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
14163 THE SOFTWARE.
14164
14165 @license
14166 */
14167 var Handlebars=function(){var a=function(){"use strict";function a(a){this.string=a}var b;return a.prototype.toString=function(){return""+this.string},b=a}(),b=function(a){"use strict";function b(a){return h[a]||"&amp;"}function c(a,b){for(var c in b)Object.prototype.hasOwnProperty.call(b,c)&&(a[c]=b[c])}function d(a){return a instanceof g?a.toString():a||0===a?(a=""+a,j.test(a)?a.replace(i,b):a):""}function e(a){return a||0===a?m(a)&&0===a.length?!0:!1:!0}var f={},g=a,h={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},i=/[&<>"'`]/g,j=/[&<>"'`]/;f.extend=c;var k=Object.prototype.toString;f.toString=k;var l=function(a){return"function"==typeof a};l(/x/)&&(l=function(a){return"function"==typeof a&&"[object Function]"===k.call(a)});var l;f.isFunction=l;var m=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===k.call(a):!1};return f.isArray=m,f.escapeExpression=d,f.isEmpty=e,f}(a),c=function(){"use strict";function a(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f<c.length;f++)this[c[f]]=e[c[f]];d&&(this.lineNumber=d,this.column=b.firstColumn)}var b,c=["description","fileName","lineNumber","message","name","number","stack"];return a.prototype=new Error,b=a}(),d=function(a,b){"use strict";function c(a,b){this.helpers=a||{},this.partials=b||{},d(this)}function d(a){a.registerHelper("helperMissing",function(a){if(2===arguments.length)return void 0;throw new h("Missing helper: '"+a+"'")}),a.registerHelper("blockHelperMissing",function(b,c){var d=c.inverse||function(){},e=c.fn;return m(b)&&(b=b.call(this)),b===!0?e(this):b===!1||null==b?d(this):l(b)?b.length>0?a.helpers.each(b,c):d(this):e(b)}),a.registerHelper("each",function(a,b){var c,d=b.fn,e=b.inverse,f=0,g="";if(m(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(l(a))for(var h=a.length;h>f;f++)c&&(c.index=f,c.first=0===f,c.last=f===a.length-1),g+=d(a[f],{data:c});else for(var i in a)a.hasOwnProperty(i)&&(c&&(c.key=i,c.index=f,c.first=0===f),g+=d(a[i],{data:c}),f++);return 0===f&&(g=e(this)),g}),a.registerHelper("if",function(a,b){return m(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||g.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){return m(a)&&(a=a.call(this)),g.isEmpty(a)?void 0:b.fn(a)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)})}function e(a,b){p.log(a,b)}var f={},g=a,h=b,i="1.3.0";f.VERSION=i;var j=4;f.COMPILER_REVISION=j;var k={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:">= 1.0.0"};f.REVISION_CHANGES=k;var l=g.isArray,m=g.isFunction,n=g.toString,o="[object Object]";f.HandlebarsEnvironment=c,c.prototype={constructor:c,logger:p,log:e,registerHelper:function(a,b,c){if(n.call(a)===o){if(c||b)throw new h("Arg not supported with multiple helpers");g.extend(this.helpers,a)}else c&&(b.not=c),this.helpers[a]=b},registerPartial:function(a,b){n.call(a)===o?g.extend(this.partials,a):this.partials[a]=b}};var p={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(p.level<=a){var c=p.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};f.logger=p,f.log=e;var q=function(a){var b={};return g.extend(b,a),b};return f.createFrame=q,f}(b,c),e=function(a,b,c){"use strict";function d(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function e(a,b){if(!b)throw new l("No environment passed to template");var c=function(a,c,d,e,f,g){var h=b.VM.invokePartial.apply(this,arguments);if(null!=h)return h;if(b.compile){var i={helpers:e,partials:f,data:g};return f[c]=b.compile(a,{data:void 0!==g},b),f[c](d,i)}throw new l("The partial "+c+" could not be compiled when running in runtime-only mode")},d={escapeExpression:k.escapeExpression,invokePartial:c,programs:[],program:function(a,b,c){var d=this.programs[a];return c?d=g(a,b,c):d||(d=this.programs[a]=g(a,b)),d},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c={},k.extend(c,b),k.extend(c,a)),c},programWithDepth:b.VM.programWithDepth,noop:b.VM.noop,compilerInfo:null};return function(c,e){e=e||{};var f,g,h=e.partial?e:b;e.partial||(f=e.helpers,g=e.partials);var i=a.call(d,h,c,f,g,e.data);return e.partial||b.VM.checkRevision(d.compilerInfo),i}}function f(a,b,c){var d=Array.prototype.slice.call(arguments,3),e=function(a,e){return e=e||{},b.apply(this,[a,e.data||c].concat(d))};return e.program=a,e.depth=d.length,e}function g(a,b,c){var d=function(a,d){return d=d||{},b(a,d.data||c)};return d.program=a,d.depth=0,d}function h(a,b,c,d,e,f){var g={partial:!0,helpers:d,partials:e,data:f};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,g):void 0}function i(){return""}var j={},k=a,l=b,m=c.COMPILER_REVISION,n=c.REVISION_CHANGES;return j.checkRevision=d,j.template=e,j.programWithDepth=f,j.program=g,j.invokePartial=h,j.noop=i,j}(b,c,d),f=function(a,b,c,d,e){"use strict";var f,g=a,h=b,i=c,j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();return m.create=l,f=m}(d,a,c,b,e);return f}();this["wysihtml5"] = this["wysihtml5"] || {};
14168 this["wysihtml5"]["tpl"] = this["wysihtml5"]["tpl"] || {};
14169
14170 this["wysihtml5"]["tpl"]["blockquote"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14171   this.compilerInfo = [4,'>= 1.0.0'];
14172 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14173   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14174
14175 function program1(depth0,data) {
14176   
14177   var buffer = "", stack1;
14178   buffer += "btn-"
14179     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14180   return buffer;
14181   }
14182
14183 function program3(depth0,data) {
14184   
14185   
14186   return " \n      <span class=\"fa fa-quote-left\"></span>\n    ";
14187   }
14188
14189 function program5(depth0,data) {
14190   
14191   
14192   return "\n      <span class=\"glyphicon glyphicon-quote\"></span>\n    ";
14193   }
14194
14195   buffer += "<li>\n  <a class=\"btn ";
14196   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14197   if(stack1 || stack1 === 0) { buffer += stack1; }
14198   buffer += " btn-default\" data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"blockquote\" data-wysihtml5-display-format-name=\"false\" tabindex=\"-1\">\n    ";
14199   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14200   if(stack1 || stack1 === 0) { buffer += stack1; }
14201   buffer += "\n  </a>\n</li>\n";
14202   return buffer;
14203   });
14204
14205 this["wysihtml5"]["tpl"]["color"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14206   this.compilerInfo = [4,'>= 1.0.0'];
14207 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14208   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14209
14210 function program1(depth0,data) {
14211   
14212   var buffer = "", stack1;
14213   buffer += "btn-"
14214     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14215   return buffer;
14216   }
14217
14218   buffer += "<li class=\"dropdown\">\n  <a class=\"btn btn-default dropdown-toggle ";
14219   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14220   if(stack1 || stack1 === 0) { buffer += stack1; }
14221   buffer += "\" data-toggle=\"dropdown\" tabindex=\"-1\">\n    <span class=\"current-color\">"
14222     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.black)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14223     + "</span>\n    <b class=\"caret\"></b>\n  </a>\n  <ul class=\"dropdown-menu\">\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"black\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"black\">"
14224     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.black)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14225     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"silver\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"silver\">"
14226     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.silver)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14227     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"gray\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"gray\">"
14228     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.gray)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14229     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"maroon\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"maroon\">"
14230     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.maroon)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14231     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"red\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"red\">"
14232     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.red)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14233     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"purple\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"purple\">"
14234     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.purple)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14235     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"green\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"green\">"
14236     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.green)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14237     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"olive\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"olive\">"
14238     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.olive)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14239     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"navy\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"navy\">"
14240     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.navy)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14241     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"blue\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"blue\">"
14242     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.blue)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14243     + "</a></li>\n    <li><div class=\"wysihtml5-colors\" data-wysihtml5-command-value=\"orange\"></div><a class=\"wysihtml5-colors-title\" data-wysihtml5-command=\"foreColor\" data-wysihtml5-command-value=\"orange\">"
14244     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.orange)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14245     + "</a></li>\n  </ul>\n</li>\n";
14246   return buffer;
14247   });
14248
14249 this["wysihtml5"]["tpl"]["emphasis"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14250   this.compilerInfo = [4,'>= 1.0.0'];
14251 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14252   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14253
14254 function program1(depth0,data) {
14255   
14256   var buffer = "", stack1;
14257   buffer += "btn-"
14258     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14259   return buffer;
14260   }
14261
14262 function program3(depth0,data) {
14263   
14264   var buffer = "", stack1;
14265   buffer += "\n    <a class=\"btn ";
14266   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14267   if(stack1 || stack1 === 0) { buffer += stack1; }
14268   buffer += " btn-default\" data-wysihtml5-command=\"small\" title=\"CTRL+S\" tabindex=\"-1\">"
14269     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.small)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14270     + "</a>\n    ";
14271   return buffer;
14272   }
14273
14274   buffer += "<li>\n  <div class=\"btn-group\">\n    <a class=\"btn ";
14275   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14276   if(stack1 || stack1 === 0) { buffer += stack1; }
14277   buffer += " btn-default\" data-wysihtml5-command=\"bold\" title=\"CTRL+B\" tabindex=\"-1\">"
14278     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.bold)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14279     + "</a>\n    <a class=\"btn ";
14280   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14281   if(stack1 || stack1 === 0) { buffer += stack1; }
14282   buffer += " btn-default\" data-wysihtml5-command=\"italic\" title=\"CTRL+I\" tabindex=\"-1\">"
14283     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.italic)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14284     + "</a>\n    <a class=\"btn ";
14285   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14286   if(stack1 || stack1 === 0) { buffer += stack1; }
14287   buffer += " btn-default\" data-wysihtml5-command=\"underline\" title=\"CTRL+U\" tabindex=\"-1\">"
14288     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.underline)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14289     + "</a>\n    ";
14290   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.small), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14291   if(stack1 || stack1 === 0) { buffer += stack1; }
14292   buffer += "\n  </div>\n</li>\n";
14293   return buffer;
14294   });
14295
14296 this["wysihtml5"]["tpl"]["font-styles"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14297   this.compilerInfo = [4,'>= 1.0.0'];
14298 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14299   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14300
14301 function program1(depth0,data) {
14302   
14303   var buffer = "", stack1;
14304   buffer += "btn-"
14305     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14306   return buffer;
14307   }
14308
14309 function program3(depth0,data) {
14310   
14311   
14312   return "\n      <span class=\"fa fa-font\"></span>\n    ";
14313   }
14314
14315 function program5(depth0,data) {
14316   
14317   
14318   return "\n      <span class=\"glyphicon glyphicon-font\"></span>\n    ";
14319   }
14320
14321   buffer += "<li class=\"dropdown\">\n  <a class=\"btn btn-default dropdown-toggle ";
14322   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14323   if(stack1 || stack1 === 0) { buffer += stack1; }
14324   buffer += "\" data-toggle=\"dropdown\">\n    ";
14325   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14326   if(stack1 || stack1 === 0) { buffer += stack1; }
14327   buffer += "\n    <span class=\"current-font\">"
14328     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.normal)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14329     + "</span>\n    <b class=\"caret\"></b>\n  </a>\n  <ul class=\"dropdown-menu\">\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"p\" tabindex=\"-1\">"
14330     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.normal)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14331     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h1\" tabindex=\"-1\">"
14332     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h1)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14333     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h2\" tabindex=\"-1\">"
14334     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h2)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14335     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h3\" tabindex=\"-1\">"
14336     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h3)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14337     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h4\" tabindex=\"-1\">"
14338     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h4)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14339     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h5\" tabindex=\"-1\">"
14340     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h5)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14341     + "</a></li>\n    <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h6\" tabindex=\"-1\">"
14342     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h6)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14343     + "</a></li>\n  </ul>\n</li>\n";
14344   return buffer;
14345   });
14346
14347 this["wysihtml5"]["tpl"]["html"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14348   this.compilerInfo = [4,'>= 1.0.0'];
14349 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14350   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14351
14352 function program1(depth0,data) {
14353   
14354   var buffer = "", stack1;
14355   buffer += "btn-"
14356     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14357   return buffer;
14358   }
14359
14360 function program3(depth0,data) {
14361   
14362   
14363   return "\n        <span class=\"fa fa-pencil\"></span>\n      ";
14364   }
14365
14366 function program5(depth0,data) {
14367   
14368   
14369   return "\n        <span class=\"glyphicon glyphicon-pencil\"></span>\n      ";
14370   }
14371
14372   buffer += "<li>\n  <div class=\"btn-group\">\n    <a class=\"btn ";
14373   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14374   if(stack1 || stack1 === 0) { buffer += stack1; }
14375   buffer += " btn-default\" data-wysihtml5-action=\"change_view\" title=\""
14376     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.html)),stack1 == null || stack1 === false ? stack1 : stack1.edit)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14377     + "\" tabindex=\"-1\">\n      ";
14378   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14379   if(stack1 || stack1 === 0) { buffer += stack1; }
14380   buffer += "\n    </a>\n  </div>\n</li>\n";
14381   return buffer;
14382   });
14383
14384 this["wysihtml5"]["tpl"]["image"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14385   this.compilerInfo = [4,'>= 1.0.0'];
14386 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14387   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14388
14389 function program1(depth0,data) {
14390   
14391   
14392   return "modal-sm";
14393   }
14394
14395 function program3(depth0,data) {
14396   
14397   var buffer = "", stack1;
14398   buffer += "btn-"
14399     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14400   return buffer;
14401   }
14402
14403 function program5(depth0,data) {
14404   
14405   
14406   return "\n      <span class=\"fa fa-file-image-o\"></span>\n    ";
14407   }
14408
14409 function program7(depth0,data) {
14410   
14411   
14412   return "\n      <span class=\"glyphicon glyphicon-picture\"></span>\n    ";
14413   }
14414
14415   buffer += "<li>\n  <div class=\"bootstrap-wysihtml5-insert-image-modal modal fade\" data-wysihtml5-dialog=\"insertImage\">\n    <div class=\"modal-dialog ";
14416   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.smallmodals), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14417   if(stack1 || stack1 === 0) { buffer += stack1; }
14418   buffer += "\">\n      <div class=\"modal-content\">\n        <div class=\"modal-header\">\n          <a class=\"close\" data-dismiss=\"modal\">&times;</a>\n          <h3>"
14419     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14420     + "</h3>\n        </div>\n        <div class=\"modal-body\">\n          <div class=\"form-group\">\n            <input value=\"http://\" class=\"bootstrap-wysihtml5-insert-image-url form-control\" data-wysihtml5-dialog-field=\"src\">\n          </div> \n        </div>\n        <div class=\"modal-footer\">\n          <a class=\"btn btn-default\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"cancel\" href=\"#\">"
14421     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.cancel)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14422     + "</a>\n          <a class=\"btn btn-primary\" data-dismiss=\"modal\"  data-wysihtml5-dialog-action=\"save\" href=\"#\">"
14423     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14424     + "</a>\n        </div>\n      </div>\n    </div>\n  </div>\n  <a class=\"btn ";
14425   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14426   if(stack1 || stack1 === 0) { buffer += stack1; }
14427   buffer += " btn-default\" data-wysihtml5-command=\"insertImage\" title=\""
14428     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14429     + "\" tabindex=\"-1\">\n    ";
14430   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),data:data});
14431   if(stack1 || stack1 === 0) { buffer += stack1; }
14432   buffer += "\n  </a>\n</li>\n";
14433   return buffer;
14434   });
14435
14436 this["wysihtml5"]["tpl"]["link"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14437   this.compilerInfo = [4,'>= 1.0.0'];
14438 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14439   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14440
14441 function program1(depth0,data) {
14442   
14443   
14444   return "modal-sm";
14445   }
14446
14447 function program3(depth0,data) {
14448   
14449   var buffer = "", stack1;
14450   buffer += "btn-"
14451     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14452   return buffer;
14453   }
14454
14455 function program5(depth0,data) {
14456   
14457   
14458   return "\n      <span class=\"fa fa-share-square-o\"></span>\n    ";
14459   }
14460
14461 function program7(depth0,data) {
14462   
14463   
14464   return "\n      <span class=\"glyphicon glyphicon-share\"></span>\n    ";
14465   }
14466
14467   buffer += "<li>\n  <div class=\"bootstrap-wysihtml5-insert-link-modal modal fade\" data-wysihtml5-dialog=\"createLink\">\n    <div class=\"modal-dialog ";
14468   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.smallmodals), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14469   if(stack1 || stack1 === 0) { buffer += stack1; }
14470   buffer += "\">\n      <div class=\"modal-content\">\n        <div class=\"modal-header\">\n          <a class=\"close\" data-dismiss=\"modal\">&times;</a>\n          <h3>"
14471     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14472     + "</h3>\n        </div>\n        <div class=\"modal-body\">\n          <div class=\"form-group\">\n            <input value=\"http://\" class=\"bootstrap-wysihtml5-insert-link-url form-control\" data-wysihtml5-dialog-field=\"href\">\n          </div> \n          <div class=\"checkbox\">\n            <label> \n              <input type=\"checkbox\" class=\"bootstrap-wysihtml5-insert-link-target\" checked>"
14473     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.target)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14474     + "\n            </label>\n          </div>\n        </div>\n        <div class=\"modal-footer\">\n          <a class=\"btn btn-default\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"cancel\" href=\"#\">"
14475     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.cancel)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14476     + "</a>\n          <a href=\"#\" class=\"btn btn-primary\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"save\">"
14477     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14478     + "</a>\n        </div>\n      </div>\n    </div>\n  </div>\n  <a class=\"btn ";
14479   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14480   if(stack1 || stack1 === 0) { buffer += stack1; }
14481   buffer += " btn-default\" data-wysihtml5-command=\"createLink\" title=\""
14482     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14483     + "\" tabindex=\"-1\">\n    ";
14484   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),data:data});
14485   if(stack1 || stack1 === 0) { buffer += stack1; }
14486   buffer += "\n  </a>\n</li>\n";
14487   return buffer;
14488   });
14489
14490 this["wysihtml5"]["tpl"]["lists"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14491   this.compilerInfo = [4,'>= 1.0.0'];
14492 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14493   var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14494
14495 function program1(depth0,data) {
14496   
14497   var buffer = "", stack1;
14498   buffer += "btn-"
14499     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14500   return buffer;
14501   }
14502
14503 function program3(depth0,data) {
14504   
14505   
14506   return "\n      <span class=\"fa fa-list-ul\"></span>\n    ";
14507   }
14508
14509 function program5(depth0,data) {
14510   
14511   
14512   return "\n      <span class=\"glyphicon glyphicon-list\"></span>\n    ";
14513   }
14514
14515 function program7(depth0,data) {
14516   
14517   
14518   return "\n      <span class=\"fa fa-list-ol\"></span>\n    ";
14519   }
14520
14521 function program9(depth0,data) {
14522   
14523   
14524   return "\n      <span class=\"glyphicon glyphicon-th-list\"></span>\n    ";
14525   }
14526
14527 function program11(depth0,data) {
14528   
14529   
14530   return "\n      <span class=\"fa fa-outdent\"></span>\n    ";
14531   }
14532
14533 function program13(depth0,data) {
14534   
14535   
14536   return "\n      <span class=\"glyphicon glyphicon-indent-right\"></span>\n    ";
14537   }
14538
14539 function program15(depth0,data) {
14540   
14541   
14542   return "\n      <span class=\"fa fa-indent\"></span>\n    ";
14543   }
14544
14545 function program17(depth0,data) {
14546   
14547   
14548   return "\n      <span class=\"glyphicon glyphicon-indent-left\"></span>\n    ";
14549   }
14550
14551   buffer += "<li>\n  <div class=\"btn-group\">\n    <a class=\"btn ";
14552   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14553   if(stack1 || stack1 === 0) { buffer += stack1; }
14554   buffer += " btn-default\" data-wysihtml5-command=\"insertUnorderedList\" title=\""
14555     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.unordered)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14556     + "\" tabindex=\"-1\">\n    ";
14557   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14558   if(stack1 || stack1 === 0) { buffer += stack1; }
14559   buffer += "\n    </a>\n    <a class=\"btn ";
14560   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14561   if(stack1 || stack1 === 0) { buffer += stack1; }
14562   buffer += " btn-default\" data-wysihtml5-command=\"insertOrderedList\" title=\""
14563     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.ordered)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14564     + "\" tabindex=\"-1\">\n    ";
14565   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(9, program9, data),fn:self.program(7, program7, data),data:data});
14566   if(stack1 || stack1 === 0) { buffer += stack1; }
14567   buffer += "\n    </a>\n    <a class=\"btn ";
14568   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14569   if(stack1 || stack1 === 0) { buffer += stack1; }
14570   buffer += " btn-default\" data-wysihtml5-command=\"Outdent\" title=\""
14571     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.outdent)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14572     + "\" tabindex=\"-1\">\n    ";
14573   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(13, program13, data),fn:self.program(11, program11, data),data:data});
14574   if(stack1 || stack1 === 0) { buffer += stack1; }
14575   buffer += "\n    </a>\n    <a class=\"btn ";
14576   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14577   if(stack1 || stack1 === 0) { buffer += stack1; }
14578   buffer += " btn-default\" data-wysihtml5-command=\"Indent\" title=\""
14579     + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.indent)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14580     + "\" tabindex=\"-1\">\n    ";
14581   stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(17, program17, data),fn:self.program(15, program15, data),data:data});
14582   if(stack1 || stack1 === 0) { buffer += stack1; }
14583   buffer += "\n    </a>\n  </div>\n</li>\n";
14584   return buffer;
14585   });(function (factory) {
14586   'use strict';
14587   if (typeof define === 'function' && define.amd) {
14588     // AMD. Register as an anonymous module.
14589     define('bootstrap.wysihtml5', ['jquery', 'wysihtml5', 'bootstrap', 'bootstrap.wysihtml5.templates', 'bootstrap.wysihtml5.commands'], factory);
14590   } else {
14591     // Browser globals
14592     factory(jQuery, wysihtml5); // jshint ignore:line
14593   }
14594 }(function ($, wysihtml5) {
14595   'use strict';
14596   var bsWysihtml5 = function($, wysihtml5) {
14597
14598     var templates = function(key, locale, options) {
14599       if(wysihtml5.tpl[key]) {
14600         return wysihtml5.tpl[key]({locale: locale, options: options});
14601       }
14602     };
14603
14604     var Wysihtml5 = function(el, options) {
14605       this.el = el;
14606       var toolbarOpts = $.extend(true, {}, defaultOptions, options);
14607       for(var t in toolbarOpts.customTemplates) {
14608         if (toolbarOpts.customTemplates.hasOwnProperty(t)) {
14609           wysihtml5.tpl[t] = toolbarOpts.customTemplates[t];
14610         }
14611       }
14612       this.toolbar = this.createToolbar(el, toolbarOpts);
14613       this.editor =  this.createEditor(toolbarOpts);
14614     };
14615
14616     Wysihtml5.prototype = {
14617
14618       constructor: Wysihtml5,
14619
14620       createEditor: function(options) {
14621         options = options || {};
14622
14623         // Add the toolbar to a clone of the options object so multiple instances
14624         // of the WYISYWG don't break because 'toolbar' is already defined
14625         options = $.extend(true, {}, options);
14626         options.toolbar = this.toolbar[0];
14627         
14628         this.initializeEditor(this.el[0], options);
14629       },
14630
14631
14632       initializeEditor: function(el, options) {
14633         var editor = new wysihtml5.Editor(this.el[0], options);
14634
14635         editor.on('beforeload', this.syncBootstrapDialogEvents);
14636         editor.on('beforeload', this.loadParserRules);
14637
14638         // #30 - body is in IE 10 not created by default, which leads to nullpointer
14639         // 2014/02/13 - adapted to wysihtml5-0.4, does not work in IE
14640         if(editor.composer.editableArea.contentDocument) {
14641           this.addMoreShortcuts(editor, 
14642                                 editor.composer.editableArea.contentDocument.body || editor.composer.editableArea.contentDocument, 
14643                                 options.shortcuts);
14644         } else {
14645           this.addMoreShortcuts(editor, editor.composer.editableArea, options.shortcuts);    
14646         }
14647
14648         if(options && options.events) {
14649           for(var eventName in options.events) {
14650             if (options.events.hasOwnProperty(eventName)) {
14651               editor.on(eventName, options.events[eventName]);
14652             }
14653           }
14654         }
14655
14656         return editor;
14657       },
14658
14659       loadParserRules: function() {
14660         if($.type(this.config.parserRules) === 'string') {
14661           $.ajax({
14662             dataType: 'json',
14663             url: this.config.parserRules,
14664             context: this,
14665             error: function (jqXHR, textStatus, errorThrown) {
14666               console.log(errorThrown);
14667             },
14668             success: function (parserRules) {
14669               this.config.parserRules = parserRules;
14670               console.log('parserrules loaded');
14671             }
14672           });
14673         }
14674
14675         if(this.config.pasteParserRulesets && $.type(this.config.pasteParserRulesets) === 'string') {
14676           $.ajax({
14677             dataType: 'json',
14678             url: this.config.pasteParserRulesets,
14679             context: this,
14680             error: function (jqXHR, textStatus, errorThrown) {
14681               console.log(errorThrown);
14682             },
14683             success: function (pasteParserRulesets) {
14684               this.config.pasteParserRulesets = pasteParserRulesets;
14685             }
14686           });
14687         }
14688       },
14689
14690       //sync wysihtml5 events for dialogs with bootstrap events
14691       syncBootstrapDialogEvents: function() {
14692         var editor = this;
14693         $.map(this.toolbar.commandMapping, function(value) {
14694           return [value];
14695         }).filter(function(commandObj) {
14696           return commandObj.dialog;
14697         }).map(function(commandObj) {
14698           return commandObj.dialog;
14699         }).forEach(function(dialog) {
14700           dialog.on('show', function() {
14701             $(this.container).modal('show');
14702           });
14703           dialog.on('hide', function() {
14704             $(this.container).modal('hide');
14705             setTimeout(editor.composer.focus, 0);
14706           });
14707           $(dialog.container).on('shown.bs.modal', function () {
14708             $(this).find('input, select, textarea').first().focus();
14709           });
14710         });
14711         this.on('change_view', function() {
14712           $(this.toolbar.container.children).find('a.btn').not('[data-wysihtml5-action="change_view"]').toggleClass('disabled');
14713         });
14714       },
14715
14716       createToolbar: function(el, options) {
14717         var self = this;
14718         var toolbar = $('<ul/>', {
14719           'class' : 'wysihtml5-toolbar',
14720           'style': 'display:none'
14721         });
14722         var culture = options.locale || defaultOptions.locale || 'en';
14723         if(!locale.hasOwnProperty(culture)) {
14724           console.debug('Locale \'' + culture + '\' not found. Available locales are: ' + Object.keys(locale) + '. Falling back to \'en\'.');
14725           culture = 'en';
14726         }
14727         var localeObject = $.extend(true, {}, locale.en, locale[culture]);
14728         for(var key in options.toolbar) {
14729           if(options.toolbar[key]) {
14730             toolbar.append(templates(key, localeObject, options));
14731           }
14732         }
14733
14734         toolbar.find('a[data-wysihtml5-command="formatBlock"]').click(function(e) {
14735           var target = e.delegateTarget || e.target || e.srcElement,
14736           el = $(target),
14737           showformat = el.data('wysihtml5-display-format-name'),
14738           formatname = el.data('wysihtml5-format-name') || el.html();
14739           if(showformat === undefined || showformat === 'true') {
14740             self.toolbar.find('.current-font').text(formatname);
14741           }
14742         });
14743
14744         toolbar.find('a[data-wysihtml5-command="foreColor"]').click(function(e) {
14745           var target = e.target || e.srcElement;
14746           var el = $(target);
14747           self.toolbar.find('.current-color').text(el.html());
14748         });
14749
14750         this.el.before(toolbar);
14751
14752         return toolbar;
14753       },
14754
14755       addMoreShortcuts: function(editor, el, shortcuts) {
14756         /* some additional shortcuts */
14757         wysihtml5.dom.observe(el, 'keydown', function(event) {
14758           var keyCode  = event.keyCode,
14759           command  = shortcuts[keyCode];
14760           if ((event.ctrlKey || event.metaKey || event.altKey) && command && wysihtml5.commands[command]) {
14761             var commandObj = editor.toolbar.commandMapping[command + ':null'];
14762             if (commandObj && commandObj.dialog && !commandObj.state) {
14763               commandObj.dialog.show();
14764             } else {
14765               wysihtml5.commands[command].exec(editor.composer, command);
14766             }
14767             event.preventDefault();
14768           }
14769         });
14770       }
14771     };
14772
14773     // these define our public api
14774     var methods = {
14775       resetDefaults: function() {
14776         $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache);
14777       },
14778       bypassDefaults: function(options) {
14779         return this.each(function () {
14780           var $this = $(this);
14781           $this.data('wysihtml5', new Wysihtml5($this, options));
14782         });
14783       },
14784       shallowExtend: function (options) {
14785         var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {}, $(this).data());
14786         var that = this;
14787         return methods.bypassDefaults.apply(that, [settings]);
14788       },
14789       deepExtend: function(options) {
14790         var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {});
14791         var that = this;
14792         return methods.bypassDefaults.apply(that, [settings]);
14793       },
14794       init: function(options) {
14795         var that = this;
14796         return methods.shallowExtend.apply(that, [options]);
14797       }
14798     };
14799
14800     $.fn.wysihtml5 = function ( method ) {
14801       if ( methods[method] ) {
14802         return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
14803       } else if ( typeof method === 'object' || ! method ) {
14804         return methods.init.apply( this, arguments );
14805       } else {
14806         $.error( 'Method ' +  method + ' does not exist on jQuery.wysihtml5' );
14807       }    
14808     };
14809
14810     $.fn.wysihtml5.Constructor = Wysihtml5;
14811
14812     var defaultOptions = $.fn.wysihtml5.defaultOptions = {
14813       toolbar: {
14814         'font-styles': true,
14815         'color': false,
14816         'emphasis': {
14817           'small': true
14818         },
14819         'blockquote': true,
14820         'lists': true,
14821         'html': false,
14822         'link': true,
14823         'image': true,
14824         'smallmodals': false
14825       },
14826       useLineBreaks: false,
14827       parserRules: {
14828         classes: {
14829           'wysiwyg-color-silver' : 1,
14830           'wysiwyg-color-gray' : 1,
14831           'wysiwyg-color-white' : 1,
14832           'wysiwyg-color-maroon' : 1,
14833           'wysiwyg-color-red' : 1,
14834           'wysiwyg-color-purple' : 1,
14835           'wysiwyg-color-fuchsia' : 1,
14836           'wysiwyg-color-green' : 1,
14837           'wysiwyg-color-lime' : 1,
14838           'wysiwyg-color-olive' : 1,
14839           'wysiwyg-color-yellow' : 1,
14840           'wysiwyg-color-navy' : 1,
14841           'wysiwyg-color-blue' : 1,
14842           'wysiwyg-color-teal' : 1,
14843           'wysiwyg-color-aqua' : 1,
14844           'wysiwyg-color-orange' : 1
14845         },
14846         tags: {
14847           'b':  {},
14848           'i':  {},
14849           'strong': {},
14850           'em': {},
14851           'p': {},
14852           'br': {},
14853           'ol': {},
14854           'ul': {},
14855           'li': {},
14856           'h1': {},
14857           'h2': {},
14858           'h3': {},
14859           'h4': {},
14860           'h5': {},
14861           'h6': {},
14862           'blockquote': {},
14863           'u': 1,
14864           'img': {
14865             'check_attributes': {
14866               'width': 'numbers',
14867               'alt': 'alt',
14868               'src': 'url',
14869               'height': 'numbers'
14870             }
14871           },
14872           'a':  {
14873             'check_attributes': {
14874               'href': 'url'
14875             },
14876             'set_attributes': {
14877               'target': '_blank',
14878               'rel': 'nofollow'
14879             }
14880           },
14881           'span': 1,
14882           'div': 1,
14883           'small': 1,
14884           'code': 1,
14885           'pre': 1
14886         }
14887       },
14888       locale: 'en',
14889       shortcuts: {
14890         '83': 'small',// S
14891         '75': 'createLink'// K
14892       }
14893     };
14894
14895     if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') {
14896       $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions);
14897     }
14898
14899     var locale = $.fn.wysihtml5.locale = {};
14900   };
14901   bsWysihtml5($, wysihtml5);
14902 }));
14903 (function(wysihtml5) {
14904   wysihtml5.commands.small = {
14905     exec: function(composer, command) {
14906       return wysihtml5.commands.formatInline.exec(composer, command, "small");
14907     },
14908
14909     state: function(composer, command) {
14910       return wysihtml5.commands.formatInline.state(composer, command, "small");
14911     }
14912   };
14913 })(wysihtml5);
14914
14915 /**
14916  * English translation for bootstrap-wysihtml5
14917  */
14918 (function (factory) {
14919     if (typeof define === 'function' && define.amd) {
14920         // AMD. Register as an anonymous module.
14921         define('bootstrap.wysihtml5.en-US', ['jquery', 'bootstrap.wysihtml5'], factory);
14922     } else {
14923         // Browser globals
14924         factory(jQuery);
14925     }
14926 }(function ($) {
14927   $.fn.wysihtml5.locale.en = $.fn.wysihtml5.locale['en-US'] = {
14928     font_styles: {
14929       normal: 'Normal text',
14930       h1: 'Heading 1',
14931       h2: 'Heading 2',
14932       h3: 'Heading 3',
14933       h4: 'Heading 4',
14934       h5: 'Heading 5',
14935       h6: 'Heading 6'
14936     },
14937     emphasis: {
14938       bold: 'Bold',
14939       italic: 'Italic',
14940       underline: 'Underline',
14941       small: 'Small'
14942     },
14943     lists: {
14944       unordered: 'Unordered list',
14945       ordered: 'Ordered list',
14946       outdent: 'Outdent',
14947       indent: 'Indent'
14948     },
14949     link: {
14950       insert: 'Insert link',
14951       cancel: 'Cancel',
14952       target: 'Open link in new window'
14953     },
14954     image: {
14955       insert: 'Insert image',
14956       cancel: 'Cancel'
14957     },
14958     html: {
14959       edit: 'Edit HTML'
14960     },
14961     colours: {
14962       black: 'Black',
14963       silver: 'Silver',
14964       gray: 'Grey',
14965       maroon: 'Maroon',
14966       red: 'Red',
14967       purple: 'Purple',
14968       green: 'Green',
14969       olive: 'Olive',
14970       navy: 'Navy',
14971       blue: 'Blue',
14972       orange: 'Orange'
14973     }
14974   };
14975 }));