liquid_feedback_frontend
view static/wysihtml/wysihtml.js @ 1717:f41f5c5f8fa2
User cannot be not entitled to vote in root level
| author | bsw | 
|---|---|
| date | Mon Sep 27 12:44:07 2021 +0200 (2021-09-27) | 
| parents | 32cc544d5a5b | 
| children | 
 line source
     1 /**
     2  * @license wysihtml v0.6.0-beta1
     3  * https://github.com/Voog/wysihtml
     4  *
     5  * Author: Christopher Blum (https://github.com/tiff)
     6  * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
     7  *
     8  * Copyright (C) 2012 XING AG
     9  * Licensed under the MIT license (MIT)
    10  *
    11  */
    12 var wysihtml = {
    13   version: '0.6.0-beta1',
    15   // namespaces
    16   commands:   {},
    17   dom:        {},
    18   quirks:     {},
    19   toolbar:    {},
    20   lang:       {},
    21   selection:  {},
    22   views:      {},
    24   editorExtenders: [],
    25   extendEditor: function(extender) {
    26     this.editorExtenders.push(extender);
    27   },
    29   INVISIBLE_SPACE: '\uFEFF',
    30   INVISIBLE_SPACE_REG_EXP: /\uFEFF/g,
    32   VOID_ELEMENTS: 'area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr',
    33   PERMITTED_PHRASING_CONTENT_ONLY: 'h1, h2, h3, h4, h5, h6, p, pre',
    35   EMPTY_FUNCTION: function() {},
    37   ELEMENT_NODE: 1,
    38   TEXT_NODE:    3,
    40   BACKSPACE_KEY:  8,
    41   ENTER_KEY:      13,
    42   ESCAPE_KEY:     27,
    43   SPACE_KEY:      32,
    44   TAB_KEY:        9,
    45   DELETE_KEY:     46
    46 };
    48 wysihtml.polyfills = function(win, doc) {
    50   var methods = {
    52     // Safary has a bug of not restoring selection after node.normalize correctly.
    53     // Detects the misbegaviour and patches it
    54     normalizeHasCaretError: function() {
    55       if ("createRange" in doc && "getSelection" in win) {
    56         var originalTarget,
    57             scrollTop = window.pageYOffset,
    58             scrollLeft = window.pageXOffset,
    59             e = doc.createElement('div'),
    60             t1 = doc.createTextNode('a'),
    61             t2 = doc.createTextNode('a'),
    62             t3 = doc.createTextNode('a'),
    63             r = doc.createRange(),
    64             s, ret;
    66         if (document.activeElement) {
    67           if (document.activeElement.nodeType === 1 && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].indexOf(document.activeElement.nodeName) > -1) {
    68             originalTarget = {
    69               type: 'form',
    70               node: document.activeElement,
    71               start: document.activeElement.selectionStart,
    72               end: document.activeElement.selectionEnd,
    73             };
    74           } else {
    75             s = win.getSelection();
    76             if (s && s.anchorNode) {
    77               originalTarget = {
    78                 type: 'range',
    79                 anchorNode: s.anchorNode,
    80                 anchorOffset: s.anchorOffset,
    81                 focusNode: s.focusNode,
    82                 focusOffset: s.focusOffset
    83               };
    84             }
    85           }
    86         }
    88         e.setAttribute('contenteditable', 'true');
    89         e.appendChild(t1);
    90         e.appendChild(t2);
    91         e.appendChild(t3);
    92         doc.body.appendChild(e);
    93         r.setStart(t2, 1);
    94         r.setEnd(t2, 1);
    96         s = win.getSelection();
    97         s.removeAllRanges();
    98         s.addRange(r);
    99         e.normalize();
   100         s = win.getSelection();
   102         ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2);
   103         e.parentNode.removeChild(e);
   104         s.removeAllRanges();
   106         if (originalTarget) {
   107           if (originalTarget.type === 'form') {
   108             // The selection parameters are not present for all form elements
   109             if (typeof originalTarget.start !== 'undefined' && typeof originalTarget.end !== 'undefined') {
   110               originalTarget.node.setSelectionRange(originalTarget.start, originalTarget.end);
   111             }
   112             originalTarget.node.focus();
   113           } else if (originalTarget.type === 'range') {
   114             r = doc.createRange();
   115             r.setStart(originalTarget.anchorNode, originalTarget.anchorOffset);
   116             r.setEnd(originalTarget.focusNode, originalTarget.focusOffset);
   117             s.addRange(r);
   118           }
   119         }
   121         if (scrollTop !== window.pageYOffset || scrollLeft !== window.pageXOffset) {
   122           win.scrollTo(scrollLeft, scrollTop);
   123         }
   125         return ret;
   126       }
   127     },
   129     apply: function() {
   130       // closest, matches, and remove polyfill
   131       // https://github.com/jonathantneal/closest
   132       (function (ELEMENT) {
   133         ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
   134           var
   135           element = this,
   136           elements = (element.document || element.ownerDocument).querySelectorAll(selector),
   137           index = 0;
   139           while (elements[index] && elements[index] !== element) {
   140             ++index;
   141           }
   143           return elements[index] ? true : false;
   144         };
   146         ELEMENT.closest = ELEMENT.closest || function closest(selector) {
   147           var element = this;
   149           while (element) {
   150             if (element.matches(selector)) {
   151               break;
   152             }
   154             element = element.parentElement;
   155           }
   157           return element;
   158         };
   160         ELEMENT.remove = ELEMENT.remove || function remove() {
   161           if (this.parentNode) {
   162             this.parentNode.removeChild(this);
   163           }
   164         };
   166       }(win.Element.prototype));
   168       if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof win.HTMLElement !== 'undefined') {
   169         win.Object.defineProperty(win.HTMLElement.prototype, 'classList', {
   170           get: function() {
   171             var self = this;
   172             function update(fn) {
   173               return function(value) {
   174                 var classes = self.className.split(/\s+/),
   175                     index = classes.indexOf(value);
   177                 fn(classes, index, value);
   178                 self.className = classes.join(' ');
   179               };
   180             }
   182             var ret = {
   183                 add: update(function(classes, index, value) {
   184                   ~index || classes.push(value);
   185                 }),
   187                 remove: update(function(classes, index) {
   188                   ~index && classes.splice(index, 1);
   189                 }),
   191                 toggle: update(function(classes, index, value) {
   192                   ~index ? classes.splice(index, 1) : classes.push(value);
   193                 }),
   195                 contains: function(value) {
   196                   return !!~self.className.split(/\s+/).indexOf(value);
   197                 },
   199                 item: function(i) {
   200                   return self.className.split(/\s+/)[i] || null;
   201                 }
   202               };
   204             win.Object.defineProperty(ret, 'length', {
   205               get: function() {
   206                 return self.className.split(/\s+/).length;
   207               }
   208             });
   210             return ret;
   211           }
   212         });
   213       }
   215       var getTextNodes = function(node){
   216         var all = [];
   217         for (node=node.firstChild;node;node=node.nextSibling){
   218           if (node.nodeType == 3) {
   219               all.push(node);
   220           } else {
   221             all = all.concat(getTextNodes(node));
   222           }
   223         }
   224         return all;
   225       };
   227       var isInDom = function(node) {
   228         var doc = node.ownerDocument,
   229             n = node;
   231         do {
   232           if (n === doc) {
   233             return true;
   234           }
   235           n = n.parentNode;
   236         } while(n);
   238         return false;
   239       };
   241       var normalizeFix = function() {
   242         var f = win.Node.prototype.normalize;
   243         var nf = function() {
   244           var texts = getTextNodes(this),
   245               s = this.ownerDocument.defaultView.getSelection(),
   246               anode = s.anchorNode,
   247               aoffset = s.anchorOffset,
   248               aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined,
   249               fnode = s.focusNode,
   250               foffset = s.focusOffset,
   251               felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined,
   252               r = this.ownerDocument.createRange(),
   253               prevTxt = texts.shift(),
   254               curText = prevTxt ? texts.shift() : null;
   256           if (felement && felement.nodeType === 3) {
   257             fnode = felement;
   258             foffset = felement.nodeValue.length;
   259             felement = undefined;
   260           }
   262           if (aelement && aelement.nodeType === 3) {
   263             anode = aelement;
   264             aoffset = 0;
   265             aelement = undefined;
   266           }
   268           if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) {
   269             fnode = [anode, anode = fnode][0];
   270             foffset = [aoffset, aoffset = foffset][0];
   271           }
   273           while(prevTxt && curText) {
   274             if (curText.previousSibling && curText.previousSibling === prevTxt) {
   275               if (anode === curText) {
   276                 anode = prevTxt;
   277                 aoffset = prevTxt.nodeValue.length +  aoffset;
   278               }
   279               if (fnode === curText) {
   280                 fnode = prevTxt;
   281                 foffset = prevTxt.nodeValue.length +  foffset;
   282               }
   283               prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue;
   284               curText.parentNode.removeChild(curText);
   285               curText = texts.shift();
   286             } else {
   287               prevTxt = curText;
   288               curText = texts.shift();
   289             }
   290           }
   292           if (felement) {
   293             foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1;
   294           }
   296           if (aelement) {
   297             aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement);
   298           }
   300           if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) {
   301             r.setStart(anode, aoffset);
   302             r.setEnd(fnode, foffset);
   303             s.removeAllRanges();
   304             s.addRange(r);
   305           }
   306         };
   307         win.Node.prototype.normalize = nf;
   308       };
   310       var F = function() {
   311         win.removeEventListener("load", F);
   312         if ("Node" in win && "normalize" in win.Node.prototype && methods.normalizeHasCaretError()) {
   313           normalizeFix();
   314         }
   315       };
   317       if (doc.readyState !== "complete") {
   318         win.addEventListener("load", F);
   319       } else {
   320         F();
   321       }
   323       // CustomEvent for ie9 and up
   324       function nativeCustomEventSupported() {
   325         try {
   326           var p = new win.CustomEvent('cat', {detail: {foo: 'bar'}});
   327           return  'cat' === p.type && 'bar' === p.detail.foo;
   328         } catch (e) {}
   329         return false;
   330       }
   332       // Polyfills CustomEvent object for IE9 and up
   333       (function() {
   334         if (!nativeCustomEventSupported() && "CustomEvent" in win) {
   335           function CustomEvent(event, params) {
   336             params = params || {bubbles: false, cancelable: false, detail: undefined};
   337             var evt = doc.createEvent('CustomEvent');
   338             evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
   339             return evt;
   340           }
   341           CustomEvent.prototype = win.Event.prototype;
   342           win.CustomEvent = CustomEvent;
   343         }
   344       })();
   345     }
   346   }
   348   return methods;
   349 };
   351 wysihtml.polyfills(window, document).apply();
   353 /*
   354 	Base.js, version 1.1a
   355 	Copyright 2006-2010, Dean Edwards
   356 	License: http://www.opensource.org/licenses/mit-license.php
   357 */
   359 var Base = function() {
   360 	// dummy
   361 };
   363 Base.extend = function(_instance, _static) { // subclass
   364 	var extend = Base.prototype.extend;
   366 	// build the prototype
   367 	Base._prototyping = true;
   368 	var proto = new this;
   369 	extend.call(proto, _instance);
   370   proto.base = function() {
   371     // call this method from any other method to invoke that method's ancestor
   372   };
   373 	delete Base._prototyping;
   375 	// create the wrapper for the constructor function
   376 	//var constructor = proto.constructor.valueOf(); //-dean
   377 	var constructor = proto.constructor;
   378 	var klass = proto.constructor = function() {
   379 		if (!Base._prototyping) {
   380 			if (this._constructing || this.constructor == klass) { // instantiation
   381 				this._constructing = true;
   382 				constructor.apply(this, arguments);
   383 				delete this._constructing;
   384 			} else if (arguments[0] != null) { // casting
   385 				return (arguments[0].extend || extend).call(arguments[0], proto);
   386 			}
   387 		}
   388 	};
   390 	// build the class interface
   391 	klass.ancestor = this;
   392 	klass.extend = this.extend;
   393 	klass.forEach = this.forEach;
   394 	klass.implement = this.implement;
   395 	klass.prototype = proto;
   396 	klass.toString = this.toString;
   397 	klass.valueOf = function(type) {
   398 		//return (type == "object") ? klass : constructor; //-dean
   399 		return (type == "object") ? klass : constructor.valueOf();
   400 	};
   401 	extend.call(klass, _static);
   402 	// class initialisation
   403 	if (typeof klass.init == "function") klass.init();
   404 	return klass;
   405 };
   407 Base.prototype = {	
   408 	extend: function(source, value) {
   409 		if (arguments.length > 1) { // extending with a name/value pair
   410 			var ancestor = this[source];
   411 			if (ancestor && (typeof value == "function") && // overriding a method?
   412 				// the valueOf() comparison is to avoid circular references
   413 				(!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
   414 				/\bbase\b/.test(value)) {
   415 				// get the underlying method
   416 				var method = value.valueOf();
   417 				// override
   418 				value = function() {
   419 					var previous = this.base || Base.prototype.base;
   420 					this.base = ancestor;
   421 					var returnValue = method.apply(this, arguments);
   422 					this.base = previous;
   423 					return returnValue;
   424 				};
   425 				// point to the underlying method
   426 				value.valueOf = function(type) {
   427 					return (type == "object") ? value : method;
   428 				};
   429 				value.toString = Base.toString;
   430 			}
   431 			this[source] = value;
   432 		} else if (source) { // extending with an object literal
   433 			var extend = Base.prototype.extend;
   434 			// if this object has a customised extend method then use it
   435 			if (!Base._prototyping && typeof this != "function") {
   436 				extend = this.extend || extend;
   437 			}
   438 			var proto = {toSource: null};
   439 			// do the "toString" and other methods manually
   440 			var hidden = ["constructor", "toString", "valueOf"];
   441 			// if we are prototyping then include the constructor
   442 			var i = Base._prototyping ? 0 : 1;
   443 			while (key = hidden[i++]) {
   444 				if (source[key] != proto[key]) {
   445 					extend.call(this, key, source[key]);
   447 				}
   448 			}
   449 			// copy each of the source object's properties to this object
   450 			for (var key in source) {
   451 				if (!proto[key]) extend.call(this, key, source[key]);
   452 			}
   453 		}
   454 		return this;
   455 	}
   456 };
   458 // initialise
   459 Base = Base.extend({
   460 	constructor: function() {
   461 		this.extend(arguments[0]);
   462 	}
   463 }, {
   464 	ancestor: Object,
   465 	version: "1.1",
   467 	forEach: function(object, block, context) {
   468 		for (var key in object) {
   469 			if (this.prototype[key] === undefined) {
   470 				block.call(context, object[key], key, object);
   471 			}
   472 		}
   473 	},
   475 	implement: function() {
   476 		for (var i = 0; i < arguments.length; i++) {
   477 			if (typeof arguments[i] == "function") {
   478 				// if it's a function, call it
   479 				arguments[i](this.prototype);
   480 			} else {
   481 				// add the interface using the extend method
   482 				this.prototype.extend(arguments[i]);
   483 			}
   484 		}
   485 		return this;
   486 	},
   488 	toString: function() {
   489 		return String(this.valueOf());
   490 	}
   491 });
   492 /**
   493  * Rangy, a cross-browser JavaScript range and selection library
   494  * https://github.com/timdown/rangy
   495  *
   496  * Copyright 2015, Tim Down
   497  * Licensed under the MIT license.
   498  * Version: 1.3.1-dev
   499  * Build date: 20 May 2015
   500  *
   501  * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
   502  */
   503 var rangy;
   505 (function() {
   506     var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
   508     // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
   509     // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
   510     var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
   511         "commonAncestorContainer"];
   513     // Minimal set of methods required for DOM Level 2 Range compliance
   514     var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
   515         "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
   516         "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
   518     var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
   520     // Subset of TextRange's full set of methods that we're interested in
   521     var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
   522         "setEndPoint", "getBoundingClientRect"];
   524     /*----------------------------------------------------------------------------------------------------------------*/
   526     // Trio of functions taken from Peter Michaux's article:
   527     // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
   528     function isHostMethod(o, p) {
   529         var t = typeof o[p];
   530         return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
   531     }
   533     function isHostObject(o, p) {
   534         return !!(typeof o[p] == OBJECT && o[p]);
   535     }
   537     function isHostProperty(o, p) {
   538         return typeof o[p] != UNDEFINED;
   539     }
   541     // Creates a convenience function to save verbose repeated calls to tests functions
   542     function createMultiplePropertyTest(testFunc) {
   543         return function(o, props) {
   544             var i = props.length;
   545             while (i--) {
   546                 if (!testFunc(o, props[i])) {
   547                     return false;
   548                 }
   549             }
   550             return true;
   551         };
   552     }
   554     // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
   555     var areHostMethods = createMultiplePropertyTest(isHostMethod);
   556     var areHostObjects = createMultiplePropertyTest(isHostObject);
   557     var areHostProperties = createMultiplePropertyTest(isHostProperty);
   559     function isTextRange(range) {
   560         return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
   561     }
   563     function getBody(doc) {
   564         return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
   565     }
   567     var forEach = [].forEach ?
   568         function(arr, func) {
   569             arr.forEach(func);
   570         } :
   571         function(arr, func) {
   572             for (var i = 0, len = arr.length; i < len; ++i) {
   573                 func(arr[i], i);
   574             }
   575         };
   577     var modules = {};
   579     var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
   581     var util = {
   582         isHostMethod: isHostMethod,
   583         isHostObject: isHostObject,
   584         isHostProperty: isHostProperty,
   585         areHostMethods: areHostMethods,
   586         areHostObjects: areHostObjects,
   587         areHostProperties: areHostProperties,
   588         isTextRange: isTextRange,
   589         getBody: getBody,
   590         forEach: forEach
   591     };
   593     var api = {
   594         version: "1.3.1-dev",
   595         initialized: false,
   596         isBrowser: isBrowser,
   597         supported: true,
   598         util: util,
   599         features: {},
   600         modules: modules,
   601         config: {
   602             alertOnFail: false,
   603             alertOnWarn: false,
   604             preferTextRange: false,
   605             autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
   606         }
   607     };
   609     function consoleLog(msg) {
   610         if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
   611             console.log(msg);
   612         }
   613     }
   615     function alertOrLog(msg, shouldAlert) {
   616         if (isBrowser && shouldAlert) {
   617             alert(msg);
   618         } else  {
   619             consoleLog(msg);
   620         }
   621     }
   623     function fail(reason) {
   624         api.initialized = true;
   625         api.supported = false;
   626         alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
   627     }
   629     api.fail = fail;
   631     function warn(msg) {
   632         alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
   633     }
   635     api.warn = warn;
   637     // Add utility extend() method
   638     var extend;
   639     if ({}.hasOwnProperty) {
   640         util.extend = extend = function(obj, props, deep) {
   641             var o, p;
   642             for (var i in props) {
   643                 if (props.hasOwnProperty(i)) {
   644                     o = obj[i];
   645                     p = props[i];
   646                     if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
   647                         extend(o, p, true);
   648                     }
   649                     obj[i] = p;
   650                 }
   651             }
   652             // Special case for toString, which does not show up in for...in loops in IE <= 8
   653             if (props.hasOwnProperty("toString")) {
   654                 obj.toString = props.toString;
   655             }
   656             return obj;
   657         };
   659         util.createOptions = function(optionsParam, defaults) {
   660             var options = {};
   661             extend(options, defaults);
   662             if (optionsParam) {
   663                 extend(options, optionsParam);
   664             }
   665             return options;
   666         };
   667     } else {
   668         fail("hasOwnProperty not supported");
   669     }
   671     // Test whether we're in a browser and bail out if not
   672     if (!isBrowser) {
   673         fail("Rangy can only run in a browser");
   674     }
   676     // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
   677     (function() {
   678         var toArray;
   680         if (isBrowser) {
   681             var el = document.createElement("div");
   682             el.appendChild(document.createElement("span"));
   683             var slice = [].slice;
   684             try {
   685                 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
   686                     toArray = function(arrayLike) {
   687                         return slice.call(arrayLike, 0);
   688                     };
   689                 }
   690             } catch (e) {}
   691         }
   693         if (!toArray) {
   694             toArray = function(arrayLike) {
   695                 var arr = [];
   696                 for (var i = 0, len = arrayLike.length; i < len; ++i) {
   697                     arr[i] = arrayLike[i];
   698                 }
   699                 return arr;
   700             };
   701         }
   703         util.toArray = toArray;
   704     })();
   706     // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
   707     // normalization of event properties
   708     var addListener;
   709     if (isBrowser) {
   710         if (isHostMethod(document, "addEventListener")) {
   711             addListener = function(obj, eventType, listener) {
   712                 obj.addEventListener(eventType, listener, false);
   713             };
   714         } else if (isHostMethod(document, "attachEvent")) {
   715             addListener = function(obj, eventType, listener) {
   716                 obj.attachEvent("on" + eventType, listener);
   717             };
   718         } else {
   719             fail("Document does not have required addEventListener or attachEvent method");
   720         }
   722         util.addListener = addListener;
   723     }
   725     var initListeners = [];
   727     function getErrorDesc(ex) {
   728         return ex.message || ex.description || String(ex);
   729     }
   731     // Initialization
   732     function init() {
   733         if (!isBrowser || api.initialized) {
   734             return;
   735         }
   736         var testRange;
   737         var implementsDomRange = false, implementsTextRange = false;
   739         // First, perform basic feature tests
   741         if (isHostMethod(document, "createRange")) {
   742             testRange = document.createRange();
   743             if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
   744                 implementsDomRange = true;
   745             }
   746         }
   748         var body = getBody(document);
   749         if (!body || body.nodeName.toLowerCase() != "body") {
   750             fail("No body element found");
   751             return;
   752         }
   754         if (body && isHostMethod(body, "createTextRange")) {
   755             testRange = body.createTextRange();
   756             if (isTextRange(testRange)) {
   757                 implementsTextRange = true;
   758             }
   759         }
   761         if (!implementsDomRange && !implementsTextRange) {
   762             fail("Neither Range nor TextRange are available");
   763             return;
   764         }
   766         api.initialized = true;
   767         api.features = {
   768             implementsDomRange: implementsDomRange,
   769             implementsTextRange: implementsTextRange
   770         };
   772         // Initialize modules
   773         var module, errorMessage;
   774         for (var moduleName in modules) {
   775             if ( (module = modules[moduleName]) instanceof Module ) {
   776                 module.init(module, api);
   777             }
   778         }
   780         // Call init listeners
   781         for (var i = 0, len = initListeners.length; i < len; ++i) {
   782             try {
   783                 initListeners[i](api);
   784             } catch (ex) {
   785                 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
   786                 consoleLog(errorMessage);
   787             }
   788         }
   789     }
   791     function deprecationNotice(deprecated, replacement, module) {
   792         if (module) {
   793             deprecated += " in module " + module.name;
   794         }
   795         api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
   796         replacement + " instead.");
   797     }
   799     function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
   800         owner[deprecated] = function() {
   801             deprecationNotice(deprecated, replacement, module);
   802             return owner[replacement].apply(owner, util.toArray(arguments));
   803         };
   804     }
   806     util.deprecationNotice = deprecationNotice;
   807     util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
   809     // Allow external scripts to initialize this library in case it's loaded after the document has loaded
   810     api.init = init;
   812     // Execute listener immediately if already initialized
   813     api.addInitListener = function(listener) {
   814         if (api.initialized) {
   815             listener(api);
   816         } else {
   817             initListeners.push(listener);
   818         }
   819     };
   821     var shimListeners = [];
   823     api.addShimListener = function(listener) {
   824         shimListeners.push(listener);
   825     };
   827     function shim(win) {
   828         win = win || window;
   829         init();
   831         // Notify listeners
   832         for (var i = 0, len = shimListeners.length; i < len; ++i) {
   833             shimListeners[i](win);
   834         }
   835     }
   837     if (isBrowser) {
   838         api.shim = api.createMissingNativeApi = shim;
   839         createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
   840     }
   842     function Module(name, dependencies, initializer) {
   843         this.name = name;
   844         this.dependencies = dependencies;
   845         this.initialized = false;
   846         this.supported = false;
   847         this.initializer = initializer;
   848     }
   850     Module.prototype = {
   851         init: function() {
   852             var requiredModuleNames = this.dependencies || [];
   853             for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
   854                 moduleName = requiredModuleNames[i];
   856                 requiredModule = modules[moduleName];
   857                 if (!requiredModule || !(requiredModule instanceof Module)) {
   858                     throw new Error("required module '" + moduleName + "' not found");
   859                 }
   861                 requiredModule.init();
   863                 if (!requiredModule.supported) {
   864                     throw new Error("required module '" + moduleName + "' not supported");
   865                 }
   866             }
   868             // Now run initializer
   869             this.initializer(this);
   870         },
   872         fail: function(reason) {
   873             this.initialized = true;
   874             this.supported = false;
   875             throw new Error(reason);
   876         },
   878         warn: function(msg) {
   879             api.warn("Module " + this.name + ": " + msg);
   880         },
   882         deprecationNotice: function(deprecated, replacement) {
   883             api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
   884                 replacement + " instead");
   885         },
   887         createError: function(msg) {
   888             return new Error("Error in Rangy " + this.name + " module: " + msg);
   889         }
   890     };
   892     function createModule(name, dependencies, initFunc) {
   893         var newModule = new Module(name, dependencies, function(module) {
   894             if (!module.initialized) {
   895                 module.initialized = true;
   896                 try {
   897                     initFunc(api, module);
   898                     module.supported = true;
   899                 } catch (ex) {
   900                     var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
   901                     consoleLog(errorMessage);
   902                     if (ex.stack) {
   903                         consoleLog(ex.stack);
   904                     }
   905                 }
   906             }
   907         });
   908         modules[name] = newModule;
   909         return newModule;
   910     }
   912     api.createModule = function(name) {
   913         // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
   914         var initFunc, dependencies;
   915         if (arguments.length == 2) {
   916             initFunc = arguments[1];
   917             dependencies = [];
   918         } else {
   919             initFunc = arguments[2];
   920             dependencies = arguments[1];
   921         }
   923         var module = createModule(name, dependencies, initFunc);
   925         // Initialize the module immediately if the core is already initialized
   926         if (api.initialized && api.supported) {
   927             module.init();
   928         }
   929     };
   931     api.createCoreModule = function(name, dependencies, initFunc) {
   932         createModule(name, dependencies, initFunc);
   933     };
   935     /*----------------------------------------------------------------------------------------------------------------*/
   937     // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
   939     function RangePrototype() {}
   940     api.RangePrototype = RangePrototype;
   941     api.rangePrototype = new RangePrototype();
   943     function SelectionPrototype() {}
   944     api.selectionPrototype = new SelectionPrototype();
   946     /*----------------------------------------------------------------------------------------------------------------*/
   948     // DOM utility methods used by Rangy
   949     api.createCoreModule("DomUtil", [], function(api, module) {
   950         var UNDEF = "undefined";
   951         var util = api.util;
   952         var getBody = util.getBody;
   954         // Perform feature tests
   955         if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
   956             module.fail("document missing a Node creation method");
   957         }
   959         if (!util.isHostMethod(document, "getElementsByTagName")) {
   960             module.fail("document missing getElementsByTagName method");
   961         }
   963         var el = document.createElement("div");
   964         if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
   965                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
   966             module.fail("Incomplete Element implementation");
   967         }
   969         // innerHTML is required for Range's createContextualFragment method
   970         if (!util.isHostProperty(el, "innerHTML")) {
   971             module.fail("Element is missing innerHTML property");
   972         }
   974         var textNode = document.createTextNode("test");
   975         if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
   976                 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
   977                 !util.areHostProperties(textNode, ["data"]))) {
   978             module.fail("Incomplete Text Node implementation");
   979         }
   981         /*----------------------------------------------------------------------------------------------------------------*/
   983         // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
   984         // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
   985         // contains just the document as a single element and the value searched for is the document.
   986         var arrayContains = /*Array.prototype.indexOf ?
   987             function(arr, val) {
   988                 return arr.indexOf(val) > -1;
   989             }:*/
   991             function(arr, val) {
   992                 var i = arr.length;
   993                 while (i--) {
   994                     if (arr[i] === val) {
   995                         return true;
   996                     }
   997                 }
   998                 return false;
   999             };
  1001         // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
  1002         function isHtmlNamespace(node) {
  1003             var ns;
  1004             return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
  1005         }
  1007         function parentElement(node) {
  1008             var parent = node.parentNode;
  1009             return (parent.nodeType == 1) ? parent : null;
  1010         }
  1012         function getNodeIndex(node) {
  1013             var i = 0;
  1014             while( (node = node.previousSibling) ) {
  1015                 ++i;
  1016             }
  1017             return i;
  1018         }
  1020         function getNodeLength(node) {
  1021             switch (node.nodeType) {
  1022                 case 7:
  1023                 case 10:
  1024                     return 0;
  1025                 case 3:
  1026                 case 8:
  1027                     return node.length;
  1028                 default:
  1029                     return node.childNodes.length;
  1030             }
  1031         }
  1033         function getCommonAncestor(node1, node2) {
  1034             var ancestors = [], n;
  1035             for (n = node1; n; n = n.parentNode) {
  1036                 ancestors.push(n);
  1037             }
  1039             for (n = node2; n; n = n.parentNode) {
  1040                 if (arrayContains(ancestors, n)) {
  1041                     return n;
  1042                 }
  1043             }
  1045             return null;
  1046         }
  1048         function isAncestorOf(ancestor, descendant, selfIsAncestor) {
  1049             var n = selfIsAncestor ? descendant : descendant.parentNode;
  1050             while (n) {
  1051                 if (n === ancestor) {
  1052                     return true;
  1053                 } else {
  1054                     n = n.parentNode;
  1055                 }
  1056             }
  1057             return false;
  1058         }
  1060         function isOrIsAncestorOf(ancestor, descendant) {
  1061             return isAncestorOf(ancestor, descendant, true);
  1062         }
  1064         function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
  1065             var p, n = selfIsAncestor ? node : node.parentNode;
  1066             while (n) {
  1067                 p = n.parentNode;
  1068                 if (p === ancestor) {
  1069                     return n;
  1070                 }
  1071                 n = p;
  1072             }
  1073             return null;
  1074         }
  1076         function isCharacterDataNode(node) {
  1077             var t = node.nodeType;
  1078             return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
  1079         }
  1081         function isTextOrCommentNode(node) {
  1082             if (!node) {
  1083                 return false;
  1084             }
  1085             var t = node.nodeType;
  1086             return t == 3 || t == 8 ; // Text or Comment
  1087         }
  1089         function insertAfter(node, precedingNode) {
  1090             var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
  1091             if (nextNode) {
  1092                 parent.insertBefore(node, nextNode);
  1093             } else {
  1094                 parent.appendChild(node);
  1095             }
  1096             return node;
  1097         }
  1099         // Note that we cannot use splitText() because it is bugridden in IE 9.
  1100         function splitDataNode(node, index, positionsToPreserve) {
  1101             var newNode = node.cloneNode(false);
  1102             newNode.deleteData(0, index);
  1103             node.deleteData(index, node.length - index);
  1104             insertAfter(newNode, node);
  1106             // Preserve positions
  1107             if (positionsToPreserve) {
  1108                 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
  1109                     // Handle case where position was inside the portion of node after the split point
  1110                     if (position.node == node && position.offset > index) {
  1111                         position.node = newNode;
  1112                         position.offset -= index;
  1113                     }
  1114                     // Handle the case where the position is a node offset within node's parent
  1115                     else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
  1116                         ++position.offset;
  1117                     }
  1118                 }
  1119             }
  1120             return newNode;
  1121         }
  1123         function getDocument(node) {
  1124             if (node.nodeType == 9) {
  1125                 return node;
  1126             } else if (typeof node.ownerDocument != UNDEF) {
  1127                 return node.ownerDocument;
  1128             } else if (typeof node.document != UNDEF) {
  1129                 return node.document;
  1130             } else if (node.parentNode) {
  1131                 return getDocument(node.parentNode);
  1132             } else {
  1133                 throw module.createError("getDocument: no document found for node");
  1134             }
  1135         }
  1137         function getWindow(node) {
  1138             var doc = getDocument(node);
  1139             if (typeof doc.defaultView != UNDEF) {
  1140                 return doc.defaultView;
  1141             } else if (typeof doc.parentWindow != UNDEF) {
  1142                 return doc.parentWindow;
  1143             } else {
  1144                 throw module.createError("Cannot get a window object for node");
  1145             }
  1146         }
  1148         function getIframeDocument(iframeEl) {
  1149             if (typeof iframeEl.contentDocument != UNDEF) {
  1150                 return iframeEl.contentDocument;
  1151             } else if (typeof iframeEl.contentWindow != UNDEF) {
  1152                 return iframeEl.contentWindow.document;
  1153             } else {
  1154                 throw module.createError("getIframeDocument: No Document object found for iframe element");
  1155             }
  1156         }
  1158         function getIframeWindow(iframeEl) {
  1159             if (typeof iframeEl.contentWindow != UNDEF) {
  1160                 return iframeEl.contentWindow;
  1161             } else if (typeof iframeEl.contentDocument != UNDEF) {
  1162                 return iframeEl.contentDocument.defaultView;
  1163             } else {
  1164                 throw module.createError("getIframeWindow: No Window object found for iframe element");
  1165             }
  1166         }
  1168         // This looks bad. Is it worth it?
  1169         function isWindow(obj) {
  1170             return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
  1171         }
  1173         function getContentDocument(obj, module, methodName) {
  1174             var doc;
  1176             if (!obj) {
  1177                 doc = document;
  1178             }
  1180             // Test if a DOM node has been passed and obtain a document object for it if so
  1181             else if (util.isHostProperty(obj, "nodeType")) {
  1182                 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
  1183                     getIframeDocument(obj) : getDocument(obj);
  1184             }
  1186             // Test if the doc parameter appears to be a Window object
  1187             else if (isWindow(obj)) {
  1188                 doc = obj.document;
  1189             }
  1191             if (!doc) {
  1192                 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
  1193             }
  1195             return doc;
  1196         }
  1198         function getRootContainer(node) {
  1199             var parent;
  1200             while ( (parent = node.parentNode) ) {
  1201                 node = parent;
  1202             }
  1203             return node;
  1204         }
  1206         function comparePoints(nodeA, offsetA, nodeB, offsetB) {
  1207             // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
  1208             var nodeC, root, childA, childB, n;
  1209             if (nodeA == nodeB) {
  1210                 // Case 1: nodes are the same
  1211                 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
  1212             } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
  1213                 // Case 2: node C (container B or an ancestor) is a child node of A
  1214                 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
  1215             } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
  1216                 // Case 3: node C (container A or an ancestor) is a child node of B
  1217                 return getNodeIndex(nodeC) < offsetB  ? -1 : 1;
  1218             } else {
  1219                 root = getCommonAncestor(nodeA, nodeB);
  1220                 if (!root) {
  1221                     throw new Error("comparePoints error: nodes have no common ancestor");
  1222                 }
  1224                 // Case 4: containers are siblings or descendants of siblings
  1225                 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
  1226                 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
  1228                 if (childA === childB) {
  1229                     // This shouldn't be possible
  1230                     throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
  1231                 } else {
  1232                     n = root.firstChild;
  1233                     while (n) {
  1234                         if (n === childA) {
  1235                             return -1;
  1236                         } else if (n === childB) {
  1237                             return 1;
  1238                         }
  1239                         n = n.nextSibling;
  1240                     }
  1241                 }
  1242             }
  1243         }
  1245         /*----------------------------------------------------------------------------------------------------------------*/
  1247         // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
  1248         var crashyTextNodes = false;
  1250         function isBrokenNode(node) {
  1251             var n;
  1252             try {
  1253                 n = node.parentNode;
  1254                 return false;
  1255             } catch (e) {
  1256                 return true;
  1257             }
  1258         }
  1260         (function() {
  1261             var el = document.createElement("b");
  1262             el.innerHTML = "1";
  1263             var textNode = el.firstChild;
  1264             el.innerHTML = "<br />";
  1265             crashyTextNodes = isBrokenNode(textNode);
  1267             api.features.crashyTextNodes = crashyTextNodes;
  1268         })();
  1270         /*----------------------------------------------------------------------------------------------------------------*/
  1272         function inspectNode(node) {
  1273             if (!node) {
  1274                 return "[No node]";
  1275             }
  1276             if (crashyTextNodes && isBrokenNode(node)) {
  1277                 return "[Broken node]";
  1278             }
  1279             if (isCharacterDataNode(node)) {
  1280                 return '"' + node.data + '"';
  1281             }
  1282             if (node.nodeType == 1) {
  1283                 var idAttr = node.id ? ' id="' + node.id + '"' : "";
  1284                 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
  1285             }
  1286             return node.nodeName;
  1287         }
  1289         function fragmentFromNodeChildren(node) {
  1290             var fragment = getDocument(node).createDocumentFragment(), child;
  1291             while ( (child = node.firstChild) ) {
  1292                 fragment.appendChild(child);
  1293             }
  1294             return fragment;
  1295         }
  1297         var getComputedStyleProperty;
  1298         if (typeof window.getComputedStyle != UNDEF) {
  1299             getComputedStyleProperty = function(el, propName) {
  1300                 return getWindow(el).getComputedStyle(el, null)[propName];
  1301             };
  1302         } else if (typeof document.documentElement.currentStyle != UNDEF) {
  1303             getComputedStyleProperty = function(el, propName) {
  1304                 return el.currentStyle ? el.currentStyle[propName] : "";
  1305             };
  1306         } else {
  1307             module.fail("No means of obtaining computed style properties found");
  1308         }
  1310         function createTestElement(doc, html, contentEditable) {
  1311             var body = getBody(doc);
  1312             var el = doc.createElement("div");
  1313             el.contentEditable = "" + !!contentEditable;
  1314             if (html) {
  1315                 el.innerHTML = html;
  1316             }
  1318             // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
  1319             var bodyFirstChild = body.firstChild;
  1320             if (bodyFirstChild) {
  1321                 body.insertBefore(el, bodyFirstChild);
  1322             } else {
  1323                 body.appendChild(el);
  1324             }
  1326             return el;
  1327         }
  1329         function removeNode(node) {
  1330             return node.parentNode.removeChild(node);
  1331         }
  1333         function NodeIterator(root) {
  1334             this.root = root;
  1335             this._next = root;
  1336         }
  1338         NodeIterator.prototype = {
  1339             _current: null,
  1341             hasNext: function() {
  1342                 return !!this._next;
  1343             },
  1345             next: function() {
  1346                 var n = this._current = this._next;
  1347                 var child, next;
  1348                 if (this._current) {
  1349                     child = n.firstChild;
  1350                     if (child) {
  1351                         this._next = child;
  1352                     } else {
  1353                         next = null;
  1354                         while ((n !== this.root) && !(next = n.nextSibling)) {
  1355                             n = n.parentNode;
  1356                         }
  1357                         this._next = next;
  1358                     }
  1359                 }
  1360                 return this._current;
  1361             },
  1363             detach: function() {
  1364                 this._current = this._next = this.root = null;
  1365             }
  1366         };
  1368         function createIterator(root) {
  1369             return new NodeIterator(root);
  1370         }
  1372         function DomPosition(node, offset) {
  1373             this.node = node;
  1374             this.offset = offset;
  1375         }
  1377         DomPosition.prototype = {
  1378             equals: function(pos) {
  1379                 return !!pos && this.node === pos.node && this.offset == pos.offset;
  1380             },
  1382             inspect: function() {
  1383                 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
  1384             },
  1386             toString: function() {
  1387                 return this.inspect();
  1388             }
  1389         };
  1391         function DOMException(codeName) {
  1392             this.code = this[codeName];
  1393             this.codeName = codeName;
  1394             this.message = "DOMException: " + this.codeName;
  1395         }
  1397         DOMException.prototype = {
  1398             INDEX_SIZE_ERR: 1,
  1399             HIERARCHY_REQUEST_ERR: 3,
  1400             WRONG_DOCUMENT_ERR: 4,
  1401             NO_MODIFICATION_ALLOWED_ERR: 7,
  1402             NOT_FOUND_ERR: 8,
  1403             NOT_SUPPORTED_ERR: 9,
  1404             INVALID_STATE_ERR: 11,
  1405             INVALID_NODE_TYPE_ERR: 24
  1406         };
  1408         DOMException.prototype.toString = function() {
  1409             return this.message;
  1410         };
  1412         api.dom = {
  1413             arrayContains: arrayContains,
  1414             isHtmlNamespace: isHtmlNamespace,
  1415             parentElement: parentElement,
  1416             getNodeIndex: getNodeIndex,
  1417             getNodeLength: getNodeLength,
  1418             getCommonAncestor: getCommonAncestor,
  1419             isAncestorOf: isAncestorOf,
  1420             isOrIsAncestorOf: isOrIsAncestorOf,
  1421             getClosestAncestorIn: getClosestAncestorIn,
  1422             isCharacterDataNode: isCharacterDataNode,
  1423             isTextOrCommentNode: isTextOrCommentNode,
  1424             insertAfter: insertAfter,
  1425             splitDataNode: splitDataNode,
  1426             getDocument: getDocument,
  1427             getWindow: getWindow,
  1428             getIframeWindow: getIframeWindow,
  1429             getIframeDocument: getIframeDocument,
  1430             getBody: getBody,
  1431             isWindow: isWindow,
  1432             getContentDocument: getContentDocument,
  1433             getRootContainer: getRootContainer,
  1434             comparePoints: comparePoints,
  1435             isBrokenNode: isBrokenNode,
  1436             inspectNode: inspectNode,
  1437             getComputedStyleProperty: getComputedStyleProperty,
  1438             createTestElement: createTestElement,
  1439             removeNode: removeNode,
  1440             fragmentFromNodeChildren: fragmentFromNodeChildren,
  1441             createIterator: createIterator,
  1442             DomPosition: DomPosition
  1443         };
  1445         api.DOMException = DOMException;
  1446     });
  1448     /*----------------------------------------------------------------------------------------------------------------*/
  1450     // Pure JavaScript implementation of DOM Range
  1451     api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
  1452         var dom = api.dom;
  1453         var util = api.util;
  1454         var DomPosition = dom.DomPosition;
  1455         var DOMException = api.DOMException;
  1457         var isCharacterDataNode = dom.isCharacterDataNode;
  1458         var getNodeIndex = dom.getNodeIndex;
  1459         var isOrIsAncestorOf = dom.isOrIsAncestorOf;
  1460         var getDocument = dom.getDocument;
  1461         var comparePoints = dom.comparePoints;
  1462         var splitDataNode = dom.splitDataNode;
  1463         var getClosestAncestorIn = dom.getClosestAncestorIn;
  1464         var getNodeLength = dom.getNodeLength;
  1465         var arrayContains = dom.arrayContains;
  1466         var getRootContainer = dom.getRootContainer;
  1467         var crashyTextNodes = api.features.crashyTextNodes;
  1469         var removeNode = dom.removeNode;
  1471         /*----------------------------------------------------------------------------------------------------------------*/
  1473         // Utility functions
  1475         function isNonTextPartiallySelected(node, range) {
  1476             return (node.nodeType != 3) &&
  1477                    (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
  1478         }
  1480         function getRangeDocument(range) {
  1481             return range.document || getDocument(range.startContainer);
  1482         }
  1484         function getRangeRoot(range) {
  1485             return getRootContainer(range.startContainer);
  1486         }
  1488         function getBoundaryBeforeNode(node) {
  1489             return new DomPosition(node.parentNode, getNodeIndex(node));
  1490         }
  1492         function getBoundaryAfterNode(node) {
  1493             return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
  1494         }
  1496         function insertNodeAtPosition(node, n, o) {
  1497             var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
  1498             if (isCharacterDataNode(n)) {
  1499                 if (o == n.length) {
  1500                     dom.insertAfter(node, n);
  1501                 } else {
  1502                     n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
  1503                 }
  1504             } else if (o >= n.childNodes.length) {
  1505                 n.appendChild(node);
  1506             } else {
  1507                 n.insertBefore(node, n.childNodes[o]);
  1508             }
  1509             return firstNodeInserted;
  1510         }
  1512         function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
  1513             assertRangeValid(rangeA);
  1514             assertRangeValid(rangeB);
  1516             if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
  1517                 throw new DOMException("WRONG_DOCUMENT_ERR");
  1518             }
  1520             var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
  1521                 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
  1523             return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
  1524         }
  1526         function cloneSubtree(iterator) {
  1527             var partiallySelected;
  1528             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
  1529                 partiallySelected = iterator.isPartiallySelectedSubtree();
  1530                 node = node.cloneNode(!partiallySelected);
  1531                 if (partiallySelected) {
  1532                     subIterator = iterator.getSubtreeIterator();
  1533                     node.appendChild(cloneSubtree(subIterator));
  1534                     subIterator.detach();
  1535                 }
  1537                 if (node.nodeType == 10) { // DocumentType
  1538                     throw new DOMException("HIERARCHY_REQUEST_ERR");
  1539                 }
  1540                 frag.appendChild(node);
  1541             }
  1542             return frag;
  1543         }
  1545         function iterateSubtree(rangeIterator, func, iteratorState) {
  1546             var it, n;
  1547             iteratorState = iteratorState || { stop: false };
  1548             for (var node, subRangeIterator; node = rangeIterator.next(); ) {
  1549                 if (rangeIterator.isPartiallySelectedSubtree()) {
  1550                     if (func(node) === false) {
  1551                         iteratorState.stop = true;
  1552                         return;
  1553                     } else {
  1554                         // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
  1555                         // the node selected by the Range.
  1556                         subRangeIterator = rangeIterator.getSubtreeIterator();
  1557                         iterateSubtree(subRangeIterator, func, iteratorState);
  1558                         subRangeIterator.detach();
  1559                         if (iteratorState.stop) {
  1560                             return;
  1561                         }
  1562                     }
  1563                 } else {
  1564                     // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
  1565                     // descendants
  1566                     it = dom.createIterator(node);
  1567                     while ( (n = it.next()) ) {
  1568                         if (func(n) === false) {
  1569                             iteratorState.stop = true;
  1570                             return;
  1571                         }
  1572                     }
  1573                 }
  1574             }
  1575         }
  1577         function deleteSubtree(iterator) {
  1578             var subIterator;
  1579             while (iterator.next()) {
  1580                 if (iterator.isPartiallySelectedSubtree()) {
  1581                     subIterator = iterator.getSubtreeIterator();
  1582                     deleteSubtree(subIterator);
  1583                     subIterator.detach();
  1584                 } else {
  1585                     iterator.remove();
  1586                 }
  1587             }
  1588         }
  1590         function extractSubtree(iterator) {
  1591             for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
  1593                 if (iterator.isPartiallySelectedSubtree()) {
  1594                     node = node.cloneNode(false);
  1595                     subIterator = iterator.getSubtreeIterator();
  1596                     node.appendChild(extractSubtree(subIterator));
  1597                     subIterator.detach();
  1598                 } else {
  1599                     iterator.remove();
  1600                 }
  1601                 if (node.nodeType == 10) { // DocumentType
  1602                     throw new DOMException("HIERARCHY_REQUEST_ERR");
  1603                 }
  1604                 frag.appendChild(node);
  1605             }
  1606             return frag;
  1607         }
  1609         function getNodesInRange(range, nodeTypes, filter) {
  1610             var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
  1611             var filterExists = !!filter;
  1612             if (filterNodeTypes) {
  1613                 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
  1614             }
  1616             var nodes = [];
  1617             iterateSubtree(new RangeIterator(range, false), function(node) {
  1618                 if (filterNodeTypes && !regex.test(node.nodeType)) {
  1619                     return;
  1620                 }
  1621                 if (filterExists && !filter(node)) {
  1622                     return;
  1623                 }
  1624                 // Don't include a boundary container if it is a character data node and the range does not contain any
  1625                 // of its character data. See issue 190.
  1626                 var sc = range.startContainer;
  1627                 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
  1628                     return;
  1629                 }
  1631                 var ec = range.endContainer;
  1632                 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
  1633                     return;
  1634                 }
  1636                 nodes.push(node);
  1637             });
  1638             return nodes;
  1639         }
  1641         function inspect(range) {
  1642             var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
  1643             return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
  1644                     dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
  1645         }
  1647         /*----------------------------------------------------------------------------------------------------------------*/
  1649         // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
  1651         function RangeIterator(range, clonePartiallySelectedTextNodes) {
  1652             this.range = range;
  1653             this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
  1656             if (!range.collapsed) {
  1657                 this.sc = range.startContainer;
  1658                 this.so = range.startOffset;
  1659                 this.ec = range.endContainer;
  1660                 this.eo = range.endOffset;
  1661                 var root = range.commonAncestorContainer;
  1663                 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
  1664                     this.isSingleCharacterDataNode = true;
  1665                     this._first = this._last = this._next = this.sc;
  1666                 } else {
  1667                     this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
  1668                         this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
  1669                     this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
  1670                         this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
  1671                 }
  1672             }
  1673         }
  1675         RangeIterator.prototype = {
  1676             _current: null,
  1677             _next: null,
  1678             _first: null,
  1679             _last: null,
  1680             isSingleCharacterDataNode: false,
  1682             reset: function() {
  1683                 this._current = null;
  1684                 this._next = this._first;
  1685             },
  1687             hasNext: function() {
  1688                 return !!this._next;
  1689             },
  1691             next: function() {
  1692                 // Move to next node
  1693                 var current = this._current = this._next;
  1694                 if (current) {
  1695                     this._next = (current !== this._last) ? current.nextSibling : null;
  1697                     // Check for partially selected text nodes
  1698                     if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
  1699                         if (current === this.ec) {
  1700                             (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
  1701                         }
  1702                         if (this._current === this.sc) {
  1703                             (current = current.cloneNode(true)).deleteData(0, this.so);
  1704                         }
  1705                     }
  1706                 }
  1708                 return current;
  1709             },
  1711             remove: function() {
  1712                 var current = this._current, start, end;
  1714                 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
  1715                     start = (current === this.sc) ? this.so : 0;
  1716                     end = (current === this.ec) ? this.eo : current.length;
  1717                     if (start != end) {
  1718                         current.deleteData(start, end - start);
  1719                     }
  1720                 } else {
  1721                     if (current.parentNode) {
  1722                         removeNode(current);
  1723                     } else {
  1724                     }
  1725                 }
  1726             },
  1728             // Checks if the current node is partially selected
  1729             isPartiallySelectedSubtree: function() {
  1730                 var current = this._current;
  1731                 return isNonTextPartiallySelected(current, this.range);
  1732             },
  1734             getSubtreeIterator: function() {
  1735                 var subRange;
  1736                 if (this.isSingleCharacterDataNode) {
  1737                     subRange = this.range.cloneRange();
  1738                     subRange.collapse(false);
  1739                 } else {
  1740                     subRange = new Range(getRangeDocument(this.range));
  1741                     var current = this._current;
  1742                     var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
  1744                     if (isOrIsAncestorOf(current, this.sc)) {
  1745                         startContainer = this.sc;
  1746                         startOffset = this.so;
  1747                     }
  1748                     if (isOrIsAncestorOf(current, this.ec)) {
  1749                         endContainer = this.ec;
  1750                         endOffset = this.eo;
  1751                     }
  1753                     updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
  1754                 }
  1755                 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
  1756             },
  1758             detach: function() {
  1759                 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
  1760             }
  1761         };
  1763         /*----------------------------------------------------------------------------------------------------------------*/
  1765         var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
  1766         var rootContainerNodeTypes = [2, 9, 11];
  1767         var readonlyNodeTypes = [5, 6, 10, 12];
  1768         var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
  1769         var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
  1771         function createAncestorFinder(nodeTypes) {
  1772             return function(node, selfIsAncestor) {
  1773                 var t, n = selfIsAncestor ? node : node.parentNode;
  1774                 while (n) {
  1775                     t = n.nodeType;
  1776                     if (arrayContains(nodeTypes, t)) {
  1777                         return n;
  1778                     }
  1779                     n = n.parentNode;
  1780                 }
  1781                 return null;
  1782             };
  1783         }
  1785         var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
  1786         var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
  1787         var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
  1789         function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
  1790             if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
  1791                 throw new DOMException("INVALID_NODE_TYPE_ERR");
  1792             }
  1793         }
  1795         function assertValidNodeType(node, invalidTypes) {
  1796             if (!arrayContains(invalidTypes, node.nodeType)) {
  1797                 throw new DOMException("INVALID_NODE_TYPE_ERR");
  1798             }
  1799         }
  1801         function assertValidOffset(node, offset) {
  1802             if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
  1803                 throw new DOMException("INDEX_SIZE_ERR");
  1804             }
  1805         }
  1807         function assertSameDocumentOrFragment(node1, node2) {
  1808             if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
  1809                 throw new DOMException("WRONG_DOCUMENT_ERR");
  1810             }
  1811         }
  1813         function assertNodeNotReadOnly(node) {
  1814             if (getReadonlyAncestor(node, true)) {
  1815                 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
  1816             }
  1817         }
  1819         function assertNode(node, codeName) {
  1820             if (!node) {
  1821                 throw new DOMException(codeName);
  1822             }
  1823         }
  1825         function isValidOffset(node, offset) {
  1826             return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
  1827         }
  1829         function isRangeValid(range) {
  1830             return (!!range.startContainer && !!range.endContainer &&
  1831                     !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
  1832                     getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
  1833                     isValidOffset(range.startContainer, range.startOffset) &&
  1834                     isValidOffset(range.endContainer, range.endOffset));
  1835         }
  1837         function assertRangeValid(range) {
  1838             if (!isRangeValid(range)) {
  1839                 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
  1840             }
  1841         }
  1843         /*----------------------------------------------------------------------------------------------------------------*/
  1845         // Test the browser's innerHTML support to decide how to implement createContextualFragment
  1846         var styleEl = document.createElement("style");
  1847         var htmlParsingConforms = false;
  1848         try {
  1849             styleEl.innerHTML = "<b>x</b>";
  1850             htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
  1851         } catch (e) {
  1852             // IE 6 and 7 throw
  1853         }
  1855         api.features.htmlParsingConforms = htmlParsingConforms;
  1857         var createContextualFragment = htmlParsingConforms ?
  1859             // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
  1860             // discussion and base code for this implementation at issue 67.
  1861             // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
  1862             // Thanks to Aleks Williams.
  1863             function(fragmentStr) {
  1864                 // "Let node the context object's start's node."
  1865                 var node = this.startContainer;
  1866                 var doc = getDocument(node);
  1868                 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
  1869                 // exception and abort these steps."
  1870                 if (!node) {
  1871                     throw new DOMException("INVALID_STATE_ERR");
  1872                 }
  1874                 // "Let element be as follows, depending on node's interface:"
  1875                 // Document, Document Fragment: null
  1876                 var el = null;
  1878                 // "Element: node"
  1879                 if (node.nodeType == 1) {
  1880                     el = node;
  1882                 // "Text, Comment: node's parentElement"
  1883                 } else if (isCharacterDataNode(node)) {
  1884                     el = dom.parentElement(node);
  1885                 }
  1887                 // "If either element is null or element's ownerDocument is an HTML document
  1888                 // and element's local name is "html" and element's namespace is the HTML
  1889                 // namespace"
  1890                 if (el === null || (
  1891                     el.nodeName == "HTML" &&
  1892                     dom.isHtmlNamespace(getDocument(el).documentElement) &&
  1893                     dom.isHtmlNamespace(el)
  1894                 )) {
  1896                 // "let element be a new Element with "body" as its local name and the HTML
  1897                 // namespace as its namespace.""
  1898                     el = doc.createElement("body");
  1899                 } else {
  1900                     el = el.cloneNode(false);
  1901                 }
  1903                 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
  1904                 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
  1905                 // "In either case, the algorithm must be invoked with fragment as the input
  1906                 // and element as the context element."
  1907                 el.innerHTML = fragmentStr;
  1909                 // "If this raises an exception, then abort these steps. Otherwise, let new
  1910                 // children be the nodes returned."
  1912                 // "Let fragment be a new DocumentFragment."
  1913                 // "Append all new children to fragment."
  1914                 // "Return fragment."
  1915                 return dom.fragmentFromNodeChildren(el);
  1916             } :
  1918             // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
  1919             // previous versions of Rangy used (with the exception of using a body element rather than a div)
  1920             function(fragmentStr) {
  1921                 var doc = getRangeDocument(this);
  1922                 var el = doc.createElement("body");
  1923                 el.innerHTML = fragmentStr;
  1925                 return dom.fragmentFromNodeChildren(el);
  1926             };
  1928         function splitRangeBoundaries(range, positionsToPreserve) {
  1929             assertRangeValid(range);
  1931             var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
  1932             var startEndSame = (sc === ec);
  1934             if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
  1935                 splitDataNode(ec, eo, positionsToPreserve);
  1936             }
  1938             if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
  1939                 sc = splitDataNode(sc, so, positionsToPreserve);
  1940                 if (startEndSame) {
  1941                     eo -= so;
  1942                     ec = sc;
  1943                 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
  1944                     eo++;
  1945                 }
  1946                 so = 0;
  1947             }
  1948             range.setStartAndEnd(sc, so, ec, eo);
  1949         }
  1951         function rangeToHtml(range) {
  1952             assertRangeValid(range);
  1953             var container = range.commonAncestorContainer.parentNode.cloneNode(false);
  1954             container.appendChild( range.cloneContents() );
  1955             return container.innerHTML;
  1956         }
  1958         /*----------------------------------------------------------------------------------------------------------------*/
  1960         var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
  1961             "commonAncestorContainer"];
  1963         var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
  1964         var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
  1966         util.extend(api.rangePrototype, {
  1967             compareBoundaryPoints: function(how, range) {
  1968                 assertRangeValid(this);
  1969                 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
  1971                 var nodeA, offsetA, nodeB, offsetB;
  1972                 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
  1973                 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
  1974                 nodeA = this[prefixA + "Container"];
  1975                 offsetA = this[prefixA + "Offset"];
  1976                 nodeB = range[prefixB + "Container"];
  1977                 offsetB = range[prefixB + "Offset"];
  1978                 return comparePoints(nodeA, offsetA, nodeB, offsetB);
  1979             },
  1981             insertNode: function(node) {
  1982                 assertRangeValid(this);
  1983                 assertValidNodeType(node, insertableNodeTypes);
  1984                 assertNodeNotReadOnly(this.startContainer);
  1986                 if (isOrIsAncestorOf(node, this.startContainer)) {
  1987                     throw new DOMException("HIERARCHY_REQUEST_ERR");
  1988                 }
  1990                 // No check for whether the container of the start of the Range is of a type that does not allow
  1991                 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
  1992                 // to add the node
  1994                 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
  1995                 this.setStartBefore(firstNodeInserted);
  1996             },
  1998             cloneContents: function() {
  1999                 assertRangeValid(this);
  2001                 var clone, frag;
  2002                 if (this.collapsed) {
  2003                     return getRangeDocument(this).createDocumentFragment();
  2004                 } else {
  2005                     if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
  2006                         clone = this.startContainer.cloneNode(true);
  2007                         clone.data = clone.data.slice(this.startOffset, this.endOffset);
  2008                         frag = getRangeDocument(this).createDocumentFragment();
  2009                         frag.appendChild(clone);
  2010                         return frag;
  2011                     } else {
  2012                         var iterator = new RangeIterator(this, true);
  2013                         clone = cloneSubtree(iterator);
  2014                         iterator.detach();
  2015                     }
  2016                     return clone;
  2017                 }
  2018             },
  2020             canSurroundContents: function() {
  2021                 assertRangeValid(this);
  2022                 assertNodeNotReadOnly(this.startContainer);
  2023                 assertNodeNotReadOnly(this.endContainer);
  2025                 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
  2026                 // no non-text nodes.
  2027                 var iterator = new RangeIterator(this, true);
  2028                 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
  2029                         (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
  2030                 iterator.detach();
  2031                 return !boundariesInvalid;
  2032             },
  2034             surroundContents: function(node) {
  2035                 assertValidNodeType(node, surroundNodeTypes);
  2037                 if (!this.canSurroundContents()) {
  2038                     throw new DOMException("INVALID_STATE_ERR");
  2039                 }
  2041                 // Extract the contents
  2042                 var content = this.extractContents();
  2044                 // Clear the children of the node
  2045                 if (node.hasChildNodes()) {
  2046                     while (node.lastChild) {
  2047                         node.removeChild(node.lastChild);
  2048                     }
  2049                 }
  2051                 // Insert the new node and add the extracted contents
  2052                 insertNodeAtPosition(node, this.startContainer, this.startOffset);
  2053                 node.appendChild(content);
  2055                 this.selectNode(node);
  2056             },
  2058             cloneRange: function() {
  2059                 assertRangeValid(this);
  2060                 var range = new Range(getRangeDocument(this));
  2061                 var i = rangeProperties.length, prop;
  2062                 while (i--) {
  2063                     prop = rangeProperties[i];
  2064                     range[prop] = this[prop];
  2065                 }
  2066                 return range;
  2067             },
  2069             toString: function() {
  2070                 assertRangeValid(this);
  2071                 var sc = this.startContainer;
  2072                 if (sc === this.endContainer && isCharacterDataNode(sc)) {
  2073                     return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
  2074                 } else {
  2075                     var textParts = [], iterator = new RangeIterator(this, true);
  2076                     iterateSubtree(iterator, function(node) {
  2077                         // Accept only text or CDATA nodes, not comments
  2078                         if (node.nodeType == 3 || node.nodeType == 4) {
  2079                             textParts.push(node.data);
  2080                         }
  2081                     });
  2082                     iterator.detach();
  2083                     return textParts.join("");
  2084                 }
  2085             },
  2087             // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
  2088             // been removed from Mozilla.
  2090             compareNode: function(node) {
  2091                 assertRangeValid(this);
  2093                 var parent = node.parentNode;
  2094                 var nodeIndex = getNodeIndex(node);
  2096                 if (!parent) {
  2097                     throw new DOMException("NOT_FOUND_ERR");
  2098                 }
  2100                 var startComparison = this.comparePoint(parent, nodeIndex),
  2101                     endComparison = this.comparePoint(parent, nodeIndex + 1);
  2103                 if (startComparison < 0) { // Node starts before
  2104                     return (endComparison > 0) ? n_b_a : n_b;
  2105                 } else {
  2106                     return (endComparison > 0) ? n_a : n_i;
  2107                 }
  2108             },
  2110             comparePoint: function(node, offset) {
  2111                 assertRangeValid(this);
  2112                 assertNode(node, "HIERARCHY_REQUEST_ERR");
  2113                 assertSameDocumentOrFragment(node, this.startContainer);
  2115                 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
  2116                     return -1;
  2117                 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
  2118                     return 1;
  2119                 }
  2120                 return 0;
  2121             },
  2123             createContextualFragment: createContextualFragment,
  2125             toHtml: function() {
  2126                 return rangeToHtml(this);
  2127             },
  2129             // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
  2130             // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
  2131             intersectsNode: function(node, touchingIsIntersecting) {
  2132                 assertRangeValid(this);
  2133                 if (getRootContainer(node) != getRangeRoot(this)) {
  2134                     return false;
  2135                 }
  2137                 var parent = node.parentNode, offset = getNodeIndex(node);
  2138                 if (!parent) {
  2139                     return true;
  2140                 }
  2142                 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
  2143                     endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
  2145                 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
  2146             },
  2148             isPointInRange: function(node, offset) {
  2149                 assertRangeValid(this);
  2150                 assertNode(node, "HIERARCHY_REQUEST_ERR");
  2151                 assertSameDocumentOrFragment(node, this.startContainer);
  2153                 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
  2154                        (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
  2155             },
  2157             // The methods below are non-standard and invented by me.
  2159             // Sharing a boundary start-to-end or end-to-start does not count as intersection.
  2160             intersectsRange: function(range) {
  2161                 return rangesIntersect(this, range, false);
  2162             },
  2164             // Sharing a boundary start-to-end or end-to-start does count as intersection.
  2165             intersectsOrTouchesRange: function(range) {
  2166                 return rangesIntersect(this, range, true);
  2167             },
  2169             intersection: function(range) {
  2170                 if (this.intersectsRange(range)) {
  2171                     var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
  2172                         endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
  2174                     var intersectionRange = this.cloneRange();
  2175                     if (startComparison == -1) {
  2176                         intersectionRange.setStart(range.startContainer, range.startOffset);
  2177                     }
  2178                     if (endComparison == 1) {
  2179                         intersectionRange.setEnd(range.endContainer, range.endOffset);
  2180                     }
  2181                     return intersectionRange;
  2182                 }
  2183                 return null;
  2184             },
  2186             union: function(range) {
  2187                 if (this.intersectsOrTouchesRange(range)) {
  2188                     var unionRange = this.cloneRange();
  2189                     if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
  2190                         unionRange.setStart(range.startContainer, range.startOffset);
  2191                     }
  2192                     if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
  2193                         unionRange.setEnd(range.endContainer, range.endOffset);
  2194                     }
  2195                     return unionRange;
  2196                 } else {
  2197                     throw new DOMException("Ranges do not intersect");
  2198                 }
  2199             },
  2201             containsNode: function(node, allowPartial) {
  2202                 if (allowPartial) {
  2203                     return this.intersectsNode(node, false);
  2204                 } else {
  2205                     return this.compareNode(node) == n_i;
  2206                 }
  2207             },
  2209             containsNodeContents: function(node) {
  2210                 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
  2211             },
  2213             containsRange: function(range) {
  2214                 var intersection = this.intersection(range);
  2215                 return intersection !== null && range.equals(intersection);
  2216             },
  2218             containsNodeText: function(node) {
  2219                 var nodeRange = this.cloneRange();
  2220                 nodeRange.selectNode(node);
  2221                 var textNodes = nodeRange.getNodes([3]);
  2222                 if (textNodes.length > 0) {
  2223                     nodeRange.setStart(textNodes[0], 0);
  2224                     var lastTextNode = textNodes.pop();
  2225                     nodeRange.setEnd(lastTextNode, lastTextNode.length);
  2226                     return this.containsRange(nodeRange);
  2227                 } else {
  2228                     return this.containsNodeContents(node);
  2229                 }
  2230             },
  2232             getNodes: function(nodeTypes, filter) {
  2233                 assertRangeValid(this);
  2234                 return getNodesInRange(this, nodeTypes, filter);
  2235             },
  2237             getDocument: function() {
  2238                 return getRangeDocument(this);
  2239             },
  2241             collapseBefore: function(node) {
  2242                 this.setEndBefore(node);
  2243                 this.collapse(false);
  2244             },
  2246             collapseAfter: function(node) {
  2247                 this.setStartAfter(node);
  2248                 this.collapse(true);
  2249             },
  2251             getBookmark: function(containerNode) {
  2252                 var doc = getRangeDocument(this);
  2253                 var preSelectionRange = api.createRange(doc);
  2254                 containerNode = containerNode || dom.getBody(doc);
  2255                 preSelectionRange.selectNodeContents(containerNode);
  2256                 var range = this.intersection(preSelectionRange);
  2257                 var start = 0, end = 0;
  2258                 if (range) {
  2259                     preSelectionRange.setEnd(range.startContainer, range.startOffset);
  2260                     start = preSelectionRange.toString().length;
  2261                     end = start + range.toString().length;
  2262                 }
  2264                 return {
  2265                     start: start,
  2266                     end: end,
  2267                     containerNode: containerNode
  2268                 };
  2269             },
  2271             moveToBookmark: function(bookmark) {
  2272                 var containerNode = bookmark.containerNode;
  2273                 var charIndex = 0;
  2274                 this.setStart(containerNode, 0);
  2275                 this.collapse(true);
  2276                 var nodeStack = [containerNode], node, foundStart = false, stop = false;
  2277                 var nextCharIndex, i, childNodes;
  2279                 while (!stop && (node = nodeStack.pop())) {
  2280                     if (node.nodeType == 3) {
  2281                         nextCharIndex = charIndex + node.length;
  2282                         if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
  2283                             this.setStart(node, bookmark.start - charIndex);
  2284                             foundStart = true;
  2285                         }
  2286                         if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
  2287                             this.setEnd(node, bookmark.end - charIndex);
  2288                             stop = true;
  2289                         }
  2290                         charIndex = nextCharIndex;
  2291                     } else {
  2292                         childNodes = node.childNodes;
  2293                         i = childNodes.length;
  2294                         while (i--) {
  2295                             nodeStack.push(childNodes[i]);
  2296                         }
  2297                     }
  2298                 }
  2299             },
  2301             getName: function() {
  2302                 return "DomRange";
  2303             },
  2305             equals: function(range) {
  2306                 return Range.rangesEqual(this, range);
  2307             },
  2309             isValid: function() {
  2310                 return isRangeValid(this);
  2311             },
  2313             inspect: function() {
  2314                 return inspect(this);
  2315             },
  2317             detach: function() {
  2318                 // In DOM4, detach() is now a no-op.
  2319             }
  2320         });
  2322         function copyComparisonConstantsToObject(obj) {
  2323             obj.START_TO_START = s2s;
  2324             obj.START_TO_END = s2e;
  2325             obj.END_TO_END = e2e;
  2326             obj.END_TO_START = e2s;
  2328             obj.NODE_BEFORE = n_b;
  2329             obj.NODE_AFTER = n_a;
  2330             obj.NODE_BEFORE_AND_AFTER = n_b_a;
  2331             obj.NODE_INSIDE = n_i;
  2332         }
  2334         function copyComparisonConstants(constructor) {
  2335             copyComparisonConstantsToObject(constructor);
  2336             copyComparisonConstantsToObject(constructor.prototype);
  2337         }
  2339         function createRangeContentRemover(remover, boundaryUpdater) {
  2340             return function() {
  2341                 assertRangeValid(this);
  2343                 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
  2345                 var iterator = new RangeIterator(this, true);
  2347                 // Work out where to position the range after content removal
  2348                 var node, boundary;
  2349                 if (sc !== root) {
  2350                     node = getClosestAncestorIn(sc, root, true);
  2351                     boundary = getBoundaryAfterNode(node);
  2352                     sc = boundary.node;
  2353                     so = boundary.offset;
  2354                 }
  2356                 // Check none of the range is read-only
  2357                 iterateSubtree(iterator, assertNodeNotReadOnly);
  2359                 iterator.reset();
  2361                 // Remove the content
  2362                 var returnValue = remover(iterator);
  2363                 iterator.detach();
  2365                 // Move to the new position
  2366                 boundaryUpdater(this, sc, so, sc, so);
  2368                 return returnValue;
  2369             };
  2370         }
  2372         function createPrototypeRange(constructor, boundaryUpdater) {
  2373             function createBeforeAfterNodeSetter(isBefore, isStart) {
  2374                 return function(node) {
  2375                     assertValidNodeType(node, beforeAfterNodeTypes);
  2376                     assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
  2378                     var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
  2379                     (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
  2380                 };
  2381             }
  2383             function setRangeStart(range, node, offset) {
  2384                 var ec = range.endContainer, eo = range.endOffset;
  2385                 if (node !== range.startContainer || offset !== range.startOffset) {
  2386                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
  2387                     // is after the current end. In either case, collapse the range to the new position
  2388                     if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
  2389                         ec = node;
  2390                         eo = offset;
  2391                     }
  2392                     boundaryUpdater(range, node, offset, ec, eo);
  2393                 }
  2394             }
  2396             function setRangeEnd(range, node, offset) {
  2397                 var sc = range.startContainer, so = range.startOffset;
  2398                 if (node !== range.endContainer || offset !== range.endOffset) {
  2399                     // Check the root containers of the range and the new boundary, and also check whether the new boundary
  2400                     // is after the current end. In either case, collapse the range to the new position
  2401                     if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
  2402                         sc = node;
  2403                         so = offset;
  2404                     }
  2405                     boundaryUpdater(range, sc, so, node, offset);
  2406                 }
  2407             }
  2409             // Set up inheritance
  2410             var F = function() {};
  2411             F.prototype = api.rangePrototype;
  2412             constructor.prototype = new F();
  2414             util.extend(constructor.prototype, {
  2415                 setStart: function(node, offset) {
  2416                     assertNoDocTypeNotationEntityAncestor(node, true);
  2417                     assertValidOffset(node, offset);
  2419                     setRangeStart(this, node, offset);
  2420                 },
  2422                 setEnd: function(node, offset) {
  2423                     assertNoDocTypeNotationEntityAncestor(node, true);
  2424                     assertValidOffset(node, offset);
  2426                     setRangeEnd(this, node, offset);
  2427                 },
  2429                 /**
  2430                  * Convenience method to set a range's start and end boundaries. Overloaded as follows:
  2431                  * - Two parameters (node, offset) creates a collapsed range at that position
  2432                  * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
  2433                  *   startOffset and ending at endOffset
  2434                  * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
  2435                  *   startNode and ending at endOffset in endNode
  2436                  */
  2437                 setStartAndEnd: function() {
  2438                     var args = arguments;
  2439                     var sc = args[0], so = args[1], ec = sc, eo = so;
  2441                     switch (args.length) {
  2442                         case 3:
  2443                             eo = args[2];
  2444                             break;
  2445                         case 4:
  2446                             ec = args[2];
  2447                             eo = args[3];
  2448                             break;
  2449                     }
  2451                     boundaryUpdater(this, sc, so, ec, eo);
  2452                 },
  2454                 setBoundary: function(node, offset, isStart) {
  2455                     this["set" + (isStart ? "Start" : "End")](node, offset);
  2456                 },
  2458                 setStartBefore: createBeforeAfterNodeSetter(true, true),
  2459                 setStartAfter: createBeforeAfterNodeSetter(false, true),
  2460                 setEndBefore: createBeforeAfterNodeSetter(true, false),
  2461                 setEndAfter: createBeforeAfterNodeSetter(false, false),
  2463                 collapse: function(isStart) {
  2464                     assertRangeValid(this);
  2465                     if (isStart) {
  2466                         boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
  2467                     } else {
  2468                         boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
  2469                     }
  2470                 },
  2472                 selectNodeContents: function(node) {
  2473                     assertNoDocTypeNotationEntityAncestor(node, true);
  2475                     boundaryUpdater(this, node, 0, node, getNodeLength(node));
  2476                 },
  2478                 selectNode: function(node) {
  2479                     assertNoDocTypeNotationEntityAncestor(node, false);
  2480                     assertValidNodeType(node, beforeAfterNodeTypes);
  2482                     var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
  2483                     boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
  2484                 },
  2486                 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
  2488                 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
  2490                 canSurroundContents: function() {
  2491                     assertRangeValid(this);
  2492                     assertNodeNotReadOnly(this.startContainer);
  2493                     assertNodeNotReadOnly(this.endContainer);
  2495                     // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
  2496                     // no non-text nodes.
  2497                     var iterator = new RangeIterator(this, true);
  2498                     var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
  2499                             (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
  2500                     iterator.detach();
  2501                     return !boundariesInvalid;
  2502                 },
  2504                 splitBoundaries: function() {
  2505                     splitRangeBoundaries(this);
  2506                 },
  2508                 splitBoundariesPreservingPositions: function(positionsToPreserve) {
  2509                     splitRangeBoundaries(this, positionsToPreserve);
  2510                 },
  2512                 normalizeBoundaries: function() {
  2513                     assertRangeValid(this);
  2515                     var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
  2517                     var mergeForward = function(node) {
  2518                         var sibling = node.nextSibling;
  2519                         if (sibling && sibling.nodeType == node.nodeType) {
  2520                             ec = node;
  2521                             eo = node.length;
  2522                             node.appendData(sibling.data);
  2523                             removeNode(sibling);
  2524                         }
  2525                     };
  2527                     var mergeBackward = function(node) {
  2528                         var sibling = node.previousSibling;
  2529                         if (sibling && sibling.nodeType == node.nodeType) {
  2530                             sc = node;
  2531                             var nodeLength = node.length;
  2532                             so = sibling.length;
  2533                             node.insertData(0, sibling.data);
  2534                             removeNode(sibling);
  2535                             if (sc == ec) {
  2536                                 eo += so;
  2537                                 ec = sc;
  2538                             } else if (ec == node.parentNode) {
  2539                                 var nodeIndex = getNodeIndex(node);
  2540                                 if (eo == nodeIndex) {
  2541                                     ec = node;
  2542                                     eo = nodeLength;
  2543                                 } else if (eo > nodeIndex) {
  2544                                     eo--;
  2545                                 }
  2546                             }
  2547                         }
  2548                     };
  2550                     var normalizeStart = true;
  2551                     var sibling;
  2553                     if (isCharacterDataNode(ec)) {
  2554                         if (eo == ec.length) {
  2555                             mergeForward(ec);
  2556                         } else if (eo == 0) {
  2557                             sibling = ec.previousSibling;
  2558                             if (sibling && sibling.nodeType == ec.nodeType) {
  2559                                 eo = sibling.length;
  2560                                 if (sc == ec) {
  2561                                     normalizeStart = false;
  2562                                 }
  2563                                 sibling.appendData(ec.data);
  2564                                 removeNode(ec);
  2565                                 ec = sibling;
  2566                             }
  2567                         }
  2568                     } else {
  2569                         if (eo > 0) {
  2570                             var endNode = ec.childNodes[eo - 1];
  2571                             if (endNode && isCharacterDataNode(endNode)) {
  2572                                 mergeForward(endNode);
  2573                             }
  2574                         }
  2575                         normalizeStart = !this.collapsed;
  2576                     }
  2578                     if (normalizeStart) {
  2579                         if (isCharacterDataNode(sc)) {
  2580                             if (so == 0) {
  2581                                 mergeBackward(sc);
  2582                             } else if (so == sc.length) {
  2583                                 sibling = sc.nextSibling;
  2584                                 if (sibling && sibling.nodeType == sc.nodeType) {
  2585                                     if (ec == sibling) {
  2586                                         ec = sc;
  2587                                         eo += sc.length;
  2588                                     }
  2589                                     sc.appendData(sibling.data);
  2590                                     removeNode(sibling);
  2591                                 }
  2592                             }
  2593                         } else {
  2594                             if (so < sc.childNodes.length) {
  2595                                 var startNode = sc.childNodes[so];
  2596                                 if (startNode && isCharacterDataNode(startNode)) {
  2597                                     mergeBackward(startNode);
  2598                                 }
  2599                             }
  2600                         }
  2601                     } else {
  2602                         sc = ec;
  2603                         so = eo;
  2604                     }
  2606                     boundaryUpdater(this, sc, so, ec, eo);
  2607                 },
  2609                 collapseToPoint: function(node, offset) {
  2610                     assertNoDocTypeNotationEntityAncestor(node, true);
  2611                     assertValidOffset(node, offset);
  2612                     this.setStartAndEnd(node, offset);
  2613                 }
  2614             });
  2616             copyComparisonConstants(constructor);
  2617         }
  2619         /*----------------------------------------------------------------------------------------------------------------*/
  2621         // Updates commonAncestorContainer and collapsed after boundary change
  2622         function updateCollapsedAndCommonAncestor(range) {
  2623             range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
  2624             range.commonAncestorContainer = range.collapsed ?
  2625                 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
  2626         }
  2628         function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
  2629             range.startContainer = startContainer;
  2630             range.startOffset = startOffset;
  2631             range.endContainer = endContainer;
  2632             range.endOffset = endOffset;
  2633             range.document = dom.getDocument(startContainer);
  2635             updateCollapsedAndCommonAncestor(range);
  2636         }
  2638         function Range(doc) {
  2639             this.startContainer = doc;
  2640             this.startOffset = 0;
  2641             this.endContainer = doc;
  2642             this.endOffset = 0;
  2643             this.document = doc;
  2644             updateCollapsedAndCommonAncestor(this);
  2645         }
  2647         createPrototypeRange(Range, updateBoundaries);
  2649         util.extend(Range, {
  2650             rangeProperties: rangeProperties,
  2651             RangeIterator: RangeIterator,
  2652             copyComparisonConstants: copyComparisonConstants,
  2653             createPrototypeRange: createPrototypeRange,
  2654             inspect: inspect,
  2655             toHtml: rangeToHtml,
  2656             getRangeDocument: getRangeDocument,
  2657             rangesEqual: function(r1, r2) {
  2658                 return r1.startContainer === r2.startContainer &&
  2659                     r1.startOffset === r2.startOffset &&
  2660                     r1.endContainer === r2.endContainer &&
  2661                     r1.endOffset === r2.endOffset;
  2662             }
  2663         });
  2665         api.DomRange = Range;
  2666     });
  2668     /*----------------------------------------------------------------------------------------------------------------*/
  2670     // Wrappers for the browser's native DOM Range and/or TextRange implementation
  2671     api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
  2672         var WrappedRange, WrappedTextRange;
  2673         var dom = api.dom;
  2674         var util = api.util;
  2675         var DomPosition = dom.DomPosition;
  2676         var DomRange = api.DomRange;
  2677         var getBody = dom.getBody;
  2678         var getContentDocument = dom.getContentDocument;
  2679         var isCharacterDataNode = dom.isCharacterDataNode;
  2682         /*----------------------------------------------------------------------------------------------------------------*/
  2684         if (api.features.implementsDomRange) {
  2685             // This is a wrapper around the browser's native DOM Range. It has two aims:
  2686             // - Provide workarounds for specific browser bugs
  2687             // - provide convenient extensions, which are inherited from Rangy's DomRange
  2689             (function() {
  2690                 var rangeProto;
  2691                 var rangeProperties = DomRange.rangeProperties;
  2693                 function updateRangeProperties(range) {
  2694                     var i = rangeProperties.length, prop;
  2695                     while (i--) {
  2696                         prop = rangeProperties[i];
  2697                         range[prop] = range.nativeRange[prop];
  2698                     }
  2699                     // Fix for broken collapsed property in IE 9.
  2700                     range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
  2701                 }
  2703                 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
  2704                     var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
  2705                     var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
  2706                     var nativeRangeDifferent = !range.equals(range.nativeRange);
  2708                     // Always set both boundaries for the benefit of IE9 (see issue 35)
  2709                     if (startMoved || endMoved || nativeRangeDifferent) {
  2710                         range.setEnd(endContainer, endOffset);
  2711                         range.setStart(startContainer, startOffset);
  2712                     }
  2713                 }
  2715                 var createBeforeAfterNodeSetter;
  2717                 WrappedRange = function(range) {
  2718                     if (!range) {
  2719                         throw module.createError("WrappedRange: Range must be specified");
  2720                     }
  2721                     this.nativeRange = range;
  2722                     updateRangeProperties(this);
  2723                 };
  2725                 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
  2727                 rangeProto = WrappedRange.prototype;
  2729                 rangeProto.selectNode = function(node) {
  2730                     this.nativeRange.selectNode(node);
  2731                     updateRangeProperties(this);
  2732                 };
  2734                 rangeProto.cloneContents = function() {
  2735                     return this.nativeRange.cloneContents();
  2736                 };
  2738                 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
  2739                 // insertNode() is never delegated to the native range.
  2741                 rangeProto.surroundContents = function(node) {
  2742                     this.nativeRange.surroundContents(node);
  2743                     updateRangeProperties(this);
  2744                 };
  2746                 rangeProto.collapse = function(isStart) {
  2747                     this.nativeRange.collapse(isStart);
  2748                     updateRangeProperties(this);
  2749                 };
  2751                 rangeProto.cloneRange = function() {
  2752                     return new WrappedRange(this.nativeRange.cloneRange());
  2753                 };
  2755                 rangeProto.refresh = function() {
  2756                     updateRangeProperties(this);
  2757                 };
  2759                 rangeProto.toString = function() {
  2760                     return this.nativeRange.toString();
  2761                 };
  2763                 // Create test range and node for feature detection
  2765                 var testTextNode = document.createTextNode("test");
  2766                 getBody(document).appendChild(testTextNode);
  2767                 var range = document.createRange();
  2769                 /*--------------------------------------------------------------------------------------------------------*/
  2771                 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
  2772                 // correct for it
  2774                 range.setStart(testTextNode, 0);
  2775                 range.setEnd(testTextNode, 0);
  2777                 try {
  2778                     range.setStart(testTextNode, 1);
  2780                     rangeProto.setStart = function(node, offset) {
  2781                         this.nativeRange.setStart(node, offset);
  2782                         updateRangeProperties(this);
  2783                     };
  2785                     rangeProto.setEnd = function(node, offset) {
  2786                         this.nativeRange.setEnd(node, offset);
  2787                         updateRangeProperties(this);
  2788                     };
  2790                     createBeforeAfterNodeSetter = function(name) {
  2791                         return function(node) {
  2792                             this.nativeRange[name](node);
  2793                             updateRangeProperties(this);
  2794                         };
  2795                     };
  2797                 } catch(ex) {
  2799                     rangeProto.setStart = function(node, offset) {
  2800                         try {
  2801                             this.nativeRange.setStart(node, offset);
  2802                         } catch (ex) {
  2803                             this.nativeRange.setEnd(node, offset);
  2804                             this.nativeRange.setStart(node, offset);
  2805                         }
  2806                         updateRangeProperties(this);
  2807                     };
  2809                     rangeProto.setEnd = function(node, offset) {
  2810                         try {
  2811                             this.nativeRange.setEnd(node, offset);
  2812                         } catch (ex) {
  2813                             this.nativeRange.setStart(node, offset);
  2814                             this.nativeRange.setEnd(node, offset);
  2815                         }
  2816                         updateRangeProperties(this);
  2817                     };
  2819                     createBeforeAfterNodeSetter = function(name, oppositeName) {
  2820                         return function(node) {
  2821                             try {
  2822                                 this.nativeRange[name](node);
  2823                             } catch (ex) {
  2824                                 this.nativeRange[oppositeName](node);
  2825                                 this.nativeRange[name](node);
  2826                             }
  2827                             updateRangeProperties(this);
  2828                         };
  2829                     };
  2830                 }
  2832                 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
  2833                 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
  2834                 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
  2835                 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
  2837                 /*--------------------------------------------------------------------------------------------------------*/
  2839                 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
  2840                 // whether the native implementation can be trusted
  2841                 rangeProto.selectNodeContents = function(node) {
  2842                     this.setStartAndEnd(node, 0, dom.getNodeLength(node));
  2843                 };
  2845                 /*--------------------------------------------------------------------------------------------------------*/
  2847                 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
  2848                 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
  2850                 range.selectNodeContents(testTextNode);
  2851                 range.setEnd(testTextNode, 3);
  2853                 var range2 = document.createRange();
  2854                 range2.selectNodeContents(testTextNode);
  2855                 range2.setEnd(testTextNode, 4);
  2856                 range2.setStart(testTextNode, 2);
  2858                 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
  2859                         range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
  2860                     // This is the wrong way round, so correct for it
  2862                     rangeProto.compareBoundaryPoints = function(type, range) {
  2863                         range = range.nativeRange || range;
  2864                         if (type == range.START_TO_END) {
  2865                             type = range.END_TO_START;
  2866                         } else if (type == range.END_TO_START) {
  2867                             type = range.START_TO_END;
  2868                         }
  2869                         return this.nativeRange.compareBoundaryPoints(type, range);
  2870                     };
  2871                 } else {
  2872                     rangeProto.compareBoundaryPoints = function(type, range) {
  2873                         return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
  2874                     };
  2875                 }
  2877                 /*--------------------------------------------------------------------------------------------------------*/
  2879                 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
  2881                 var el = document.createElement("div");
  2882                 el.innerHTML = "123";
  2883                 var textNode = el.firstChild;
  2884                 var body = getBody(document);
  2885                 body.appendChild(el);
  2887                 range.setStart(textNode, 1);
  2888                 range.setEnd(textNode, 2);
  2889                 range.deleteContents();
  2891                 if (textNode.data == "13") {
  2892                     // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
  2893                     // extractContents()
  2894                     rangeProto.deleteContents = function() {
  2895                         this.nativeRange.deleteContents();
  2896                         updateRangeProperties(this);
  2897                     };
  2899                     rangeProto.extractContents = function() {
  2900                         var frag = this.nativeRange.extractContents();
  2901                         updateRangeProperties(this);
  2902                         return frag;
  2903                     };
  2904                 } else {
  2905                 }
  2907                 body.removeChild(el);
  2908                 body = null;
  2910                 /*--------------------------------------------------------------------------------------------------------*/
  2912                 // Test for existence of createContextualFragment and delegate to it if it exists
  2913                 if (util.isHostMethod(range, "createContextualFragment")) {
  2914                     rangeProto.createContextualFragment = function(fragmentStr) {
  2915                         return this.nativeRange.createContextualFragment(fragmentStr);
  2916                     };
  2917                 }
  2919                 /*--------------------------------------------------------------------------------------------------------*/
  2921                 // Clean up
  2922                 getBody(document).removeChild(testTextNode);
  2924                 rangeProto.getName = function() {
  2925                     return "WrappedRange";
  2926                 };
  2928                 api.WrappedRange = WrappedRange;
  2930                 api.createNativeRange = function(doc) {
  2931                     doc = getContentDocument(doc, module, "createNativeRange");
  2932                     return doc.createRange();
  2933                 };
  2934             })();
  2935         }
  2937         if (api.features.implementsTextRange) {
  2938             /*
  2939             This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
  2940             method. For example, in the following (where pipes denote the selection boundaries):
  2942             <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
  2944             var range = document.selection.createRange();
  2945             alert(range.parentElement().id); // Should alert "ul" but alerts "b"
  2947             This method returns the common ancestor node of the following:
  2948             - the parentElement() of the textRange
  2949             - the parentElement() of the textRange after calling collapse(true)
  2950             - the parentElement() of the textRange after calling collapse(false)
  2951             */
  2952             var getTextRangeContainerElement = function(textRange) {
  2953                 var parentEl = textRange.parentElement();
  2954                 var range = textRange.duplicate();
  2955                 range.collapse(true);
  2956                 var startEl = range.parentElement();
  2957                 range = textRange.duplicate();
  2958                 range.collapse(false);
  2959                 var endEl = range.parentElement();
  2960                 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
  2962                 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
  2963             };
  2965             var textRangeIsCollapsed = function(textRange) {
  2966                 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
  2967             };
  2969             // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
  2970             // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
  2971             // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
  2972             // bugs, handling for inputs and images, plus optimizations.
  2973             var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
  2974                 var workingRange = textRange.duplicate();
  2975                 workingRange.collapse(isStart);
  2976                 var containerElement = workingRange.parentElement();
  2978                 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
  2979                 // check for that
  2980                 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
  2981                     containerElement = wholeRangeContainerElement;
  2982                 }
  2985                 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
  2986                 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
  2987                 if (!containerElement.canHaveHTML) {
  2988                     var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
  2989                     return {
  2990                         boundaryPosition: pos,
  2991                         nodeInfo: {
  2992                             nodeIndex: pos.offset,
  2993                             containerElement: pos.node
  2994                         }
  2995                     };
  2996                 }
  2998                 var workingNode = dom.getDocument(containerElement).createElement("span");
  3000                 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
  3001                 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
  3002                 if (workingNode.parentNode) {
  3003                     dom.removeNode(workingNode);
  3004                 }
  3006                 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
  3007                 var previousNode, nextNode, boundaryPosition, boundaryNode;
  3008                 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
  3009                 var childNodeCount = containerElement.childNodes.length;
  3010                 var end = childNodeCount;
  3012                 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
  3013                 // after the range boundary.
  3014                 var nodeIndex = end;
  3016                 while (true) {
  3017                     if (nodeIndex == childNodeCount) {
  3018                         containerElement.appendChild(workingNode);
  3019                     } else {
  3020                         containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
  3021                     }
  3022                     workingRange.moveToElementText(workingNode);
  3023                     comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
  3024                     if (comparison == 0 || start == end) {
  3025                         break;
  3026                     } else if (comparison == -1) {
  3027                         if (end == start + 1) {
  3028                             // We know the endth child node is after the range boundary, so we must be done.
  3029                             break;
  3030                         } else {
  3031                             start = nodeIndex;
  3032                         }
  3033                     } else {
  3034                         end = (end == start + 1) ? start : nodeIndex;
  3035                     }
  3036                     nodeIndex = Math.floor((start + end) / 2);
  3037                     containerElement.removeChild(workingNode);
  3038                 }
  3041                 // We've now reached or gone past the boundary of the text range we're interested in
  3042                 // so have identified the node we want
  3043                 boundaryNode = workingNode.nextSibling;
  3045                 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
  3046                     // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
  3047                     // the node containing the text range's boundary, so we move the end of the working range to the
  3048                     // boundary point and measure the length of its text to get the boundary's offset within the node.
  3049                     workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
  3051                     var offset;
  3053                     if (/[\r\n]/.test(boundaryNode.data)) {
  3054                         /*
  3055                         For the particular case of a boundary within a text node containing rendered line breaks (within a
  3056                         <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
  3057                         IE. The facts:
  3059                         - Each line break is represented as \r in the text node's data/nodeValue properties
  3060                         - Each line break is represented as \r\n in the TextRange's 'text' property
  3061                         - The 'text' property of the TextRange does not contain trailing line breaks
  3063                         To get round the problem presented by the final fact above, we can use the fact that TextRange's
  3064                         moveStart() and moveEnd() methods return the actual number of characters moved, which is not
  3065                         necessarily the same as the number of characters it was instructed to move. The simplest approach is
  3066                         to use this to store the characters moved when moving both the start and end of the range to the
  3067                         start of the document body and subtracting the start offset from the end offset (the
  3068                         "move-negative-gazillion" method). However, this is extremely slow when the document is large and
  3069                         the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
  3070                         the end of the document) has the same problem.
  3072                         Another approach that works is to use moveStart() to move the start boundary of the range up to the
  3073                         end boundary one character at a time and incrementing a counter with the value returned by the
  3074                         moveStart() call. However, the check for whether the start boundary has reached the end boundary is
  3075                         expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
  3076                         by the location of the range within the document).
  3078                         The approach used below is a hybrid of the two methods above. It uses the fact that a string
  3079                         containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
  3080                         be longer than the text of the TextRange, so the start of the range is moved that length initially
  3081                         and then a character at a time to make up for any trailing line breaks not contained in the 'text'
  3082                         property. This has good performance in most situations compared to the previous two methods.
  3083                         */
  3084                         var tempRange = workingRange.duplicate();
  3085                         var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
  3087                         offset = tempRange.moveStart("character", rangeLength);
  3088                         while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
  3089                             offset++;
  3090                             tempRange.moveStart("character", 1);
  3091                         }
  3092                     } else {
  3093                         offset = workingRange.text.length;
  3094                     }
  3095                     boundaryPosition = new DomPosition(boundaryNode, offset);
  3096                 } else {
  3098                     // If the boundary immediately follows a character data node and this is the end boundary, we should favour
  3099                     // a position within that, and likewise for a start boundary preceding a character data node
  3100                     previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
  3101                     nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
  3102                     if (nextNode && isCharacterDataNode(nextNode)) {
  3103                         boundaryPosition = new DomPosition(nextNode, 0);
  3104                     } else if (previousNode && isCharacterDataNode(previousNode)) {
  3105                         boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
  3106                     } else {
  3107                         boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
  3108                     }
  3109                 }
  3111                 // Clean up
  3112                 dom.removeNode(workingNode);
  3114                 return {
  3115                     boundaryPosition: boundaryPosition,
  3116                     nodeInfo: {
  3117                         nodeIndex: nodeIndex,
  3118                         containerElement: containerElement
  3119                     }
  3120                 };
  3121             };
  3123             // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
  3124             // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
  3125             // (http://code.google.com/p/ierange/)
  3126             var createBoundaryTextRange = function(boundaryPosition, isStart) {
  3127                 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
  3128                 var doc = dom.getDocument(boundaryPosition.node);
  3129                 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
  3130                 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
  3132                 if (nodeIsDataNode) {
  3133                     boundaryNode = boundaryPosition.node;
  3134                     boundaryParent = boundaryNode.parentNode;
  3135                 } else {
  3136                     childNodes = boundaryPosition.node.childNodes;
  3137                     boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
  3138                     boundaryParent = boundaryPosition.node;
  3139                 }
  3141                 // Position the range immediately before the node containing the boundary
  3142                 workingNode = doc.createElement("span");
  3144                 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
  3145                 // the element rather than immediately before or after it
  3146                 workingNode.innerHTML = "&#feff;";
  3148                 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
  3149                 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
  3150                 if (boundaryNode) {
  3151                     boundaryParent.insertBefore(workingNode, boundaryNode);
  3152                 } else {
  3153                     boundaryParent.appendChild(workingNode);
  3154                 }
  3156                 workingRange.moveToElementText(workingNode);
  3157                 workingRange.collapse(!isStart);
  3159                 // Clean up
  3160                 boundaryParent.removeChild(workingNode);
  3162                 // Move the working range to the text offset, if required
  3163                 if (nodeIsDataNode) {
  3164                     workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
  3165                 }
  3167                 return workingRange;
  3168             };
  3170             /*------------------------------------------------------------------------------------------------------------*/
  3172             // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
  3173             // prototype
  3175             WrappedTextRange = function(textRange) {
  3176                 this.textRange = textRange;
  3177                 this.refresh();
  3178             };
  3180             WrappedTextRange.prototype = new DomRange(document);
  3182             WrappedTextRange.prototype.refresh = function() {
  3183                 var start, end, startBoundary;
  3185                 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
  3186                 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
  3188                 if (textRangeIsCollapsed(this.textRange)) {
  3189                     end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
  3190                         true).boundaryPosition;
  3191                 } else {
  3192                     startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
  3193                     start = startBoundary.boundaryPosition;
  3195                     // An optimization used here is that if the start and end boundaries have the same parent element, the
  3196                     // search scope for the end boundary can be limited to exclude the portion of the element that precedes
  3197                     // the start boundary
  3198                     end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
  3199                         startBoundary.nodeInfo).boundaryPosition;
  3200                 }
  3202                 this.setStart(start.node, start.offset);
  3203                 this.setEnd(end.node, end.offset);
  3204             };
  3206             WrappedTextRange.prototype.getName = function() {
  3207                 return "WrappedTextRange";
  3208             };
  3210             DomRange.copyComparisonConstants(WrappedTextRange);
  3212             var rangeToTextRange = function(range) {
  3213                 if (range.collapsed) {
  3214                     return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
  3215                 } else {
  3216                     var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
  3217                     var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
  3218                     var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
  3219                     textRange.setEndPoint("StartToStart", startRange);
  3220                     textRange.setEndPoint("EndToEnd", endRange);
  3221                     return textRange;
  3222                 }
  3223             };
  3225             WrappedTextRange.rangeToTextRange = rangeToTextRange;
  3227             WrappedTextRange.prototype.toTextRange = function() {
  3228                 return rangeToTextRange(this);
  3229             };
  3231             api.WrappedTextRange = WrappedTextRange;
  3233             // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
  3234             // implementation to use by default.
  3235             if (!api.features.implementsDomRange || api.config.preferTextRange) {
  3236                 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
  3237                 var globalObj = (function(f) { return f("return this;")(); })(Function);
  3238                 if (typeof globalObj.Range == "undefined") {
  3239                     globalObj.Range = WrappedTextRange;
  3240                 }
  3242                 api.createNativeRange = function(doc) {
  3243                     doc = getContentDocument(doc, module, "createNativeRange");
  3244                     return getBody(doc).createTextRange();
  3245                 };
  3247                 api.WrappedRange = WrappedTextRange;
  3248             }
  3249         }
  3251         api.createRange = function(doc) {
  3252             doc = getContentDocument(doc, module, "createRange");
  3253             return new api.WrappedRange(api.createNativeRange(doc));
  3254         };
  3256         api.createRangyRange = function(doc) {
  3257             doc = getContentDocument(doc, module, "createRangyRange");
  3258             return new DomRange(doc);
  3259         };
  3261         util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
  3262         util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
  3264         api.addShimListener(function(win) {
  3265             var doc = win.document;
  3266             if (typeof doc.createRange == "undefined") {
  3267                 doc.createRange = function() {
  3268                     return api.createRange(doc);
  3269                 };
  3270             }
  3271             doc = win = null;
  3272         });
  3273     });
  3275     /*----------------------------------------------------------------------------------------------------------------*/
  3277     // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
  3278     // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
  3279     api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
  3280         api.config.checkSelectionRanges = true;
  3282         var BOOLEAN = "boolean";
  3283         var NUMBER = "number";
  3284         var dom = api.dom;
  3285         var util = api.util;
  3286         var isHostMethod = util.isHostMethod;
  3287         var DomRange = api.DomRange;
  3288         var WrappedRange = api.WrappedRange;
  3289         var DOMException = api.DOMException;
  3290         var DomPosition = dom.DomPosition;
  3291         var getNativeSelection;
  3292         var selectionIsCollapsed;
  3293         var features = api.features;
  3294         var CONTROL = "Control";
  3295         var getDocument = dom.getDocument;
  3296         var getBody = dom.getBody;
  3297         var rangesEqual = DomRange.rangesEqual;
  3300         // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
  3301         // "forward" or "forwards") or a Boolean (true for backwards).
  3302         function isDirectionBackward(dir) {
  3303             return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
  3304         }
  3306         function getWindow(win, methodName) {
  3307             if (!win) {
  3308                 return window;
  3309             } else if (dom.isWindow(win)) {
  3310                 return win;
  3311             } else if (win instanceof WrappedSelection) {
  3312                 return win.win;
  3313             } else {
  3314                 var doc = dom.getContentDocument(win, module, methodName);
  3315                 return dom.getWindow(doc);
  3316             }
  3317         }
  3319         function getWinSelection(winParam) {
  3320             return getWindow(winParam, "getWinSelection").getSelection();
  3321         }
  3323         function getDocSelection(winParam) {
  3324             return getWindow(winParam, "getDocSelection").document.selection;
  3325         }
  3327         function winSelectionIsBackward(sel) {
  3328             var backward = false;
  3329             if (sel.anchorNode) {
  3330                 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
  3331             }
  3332             return backward;
  3333         }
  3335         // Test for the Range/TextRange and Selection features required
  3336         // Test for ability to retrieve selection
  3337         var implementsWinGetSelection = isHostMethod(window, "getSelection"),
  3338             implementsDocSelection = util.isHostObject(document, "selection");
  3340         features.implementsWinGetSelection = implementsWinGetSelection;
  3341         features.implementsDocSelection = implementsDocSelection;
  3343         var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
  3345         if (useDocumentSelection) {
  3346             getNativeSelection = getDocSelection;
  3347             api.isSelectionValid = function(winParam) {
  3348                 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
  3350                 // Check whether the selection TextRange is actually contained within the correct document
  3351                 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
  3352             };
  3353         } else if (implementsWinGetSelection) {
  3354             getNativeSelection = getWinSelection;
  3355             api.isSelectionValid = function() {
  3356                 return true;
  3357             };
  3358         } else {
  3359             module.fail("Neither document.selection or window.getSelection() detected.");
  3360             return false;
  3361         }
  3363         api.getNativeSelection = getNativeSelection;
  3365         var testSelection = getNativeSelection();
  3367         // In Firefox, the selection is null in an iframe with display: none. See issue #138.
  3368         if (!testSelection) {
  3369             module.fail("Native selection was null (possibly issue 138?)");
  3370             return false;
  3371         }
  3373         var testRange = api.createNativeRange(document);
  3374         var body = getBody(document);
  3376         // Obtaining a range from a selection
  3377         var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
  3378             ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
  3380         features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
  3382         // Test for existence of native selection extend() method
  3383         var selectionHasExtend = isHostMethod(testSelection, "extend");
  3384         features.selectionHasExtend = selectionHasExtend;
  3386         // Test if rangeCount exists
  3387         var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
  3388         features.selectionHasRangeCount = selectionHasRangeCount;
  3390         var selectionSupportsMultipleRanges = false;
  3391         var collapsedNonEditableSelectionsSupported = true;
  3393         var addRangeBackwardToNative = selectionHasExtend ?
  3394             function(nativeSelection, range) {
  3395                 var doc = DomRange.getRangeDocument(range);
  3396                 var endRange = api.createRange(doc);
  3397                 endRange.collapseToPoint(range.endContainer, range.endOffset);
  3398                 nativeSelection.addRange(getNativeRange(endRange));
  3399                 nativeSelection.extend(range.startContainer, range.startOffset);
  3400             } : null;
  3402         if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
  3403                 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
  3405             (function() {
  3406                 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
  3407                 // performed on the current document's selection. See issue 109.
  3409                 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
  3410                 // will result in the selection direction begin reversed if the original selection was backwards and the
  3411                 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
  3412                 var sel = window.getSelection();
  3413                 if (sel) {
  3414                     // Store the current selection
  3415                     var originalSelectionRangeCount = sel.rangeCount;
  3416                     var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
  3417                     var originalSelectionRanges = [];
  3418                     var originalSelectionBackward = winSelectionIsBackward(sel);
  3419                     for (var i = 0; i < originalSelectionRangeCount; ++i) {
  3420                         originalSelectionRanges[i] = sel.getRangeAt(i);
  3421                     }
  3423                     // Create some test elements
  3424                     var testEl = dom.createTestElement(document, "", false);
  3425                     var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
  3427                     // Test whether the native selection will allow a collapsed selection within a non-editable element
  3428                     var r1 = document.createRange();
  3430                     r1.setStart(textNode, 1);
  3431                     r1.collapse(true);
  3432                     sel.removeAllRanges();
  3433                     sel.addRange(r1);
  3434                     collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
  3435                     sel.removeAllRanges();
  3437                     // Test whether the native selection is capable of supporting multiple ranges.
  3438                     if (!selectionHasMultipleRanges) {
  3439                         // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
  3440                         // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
  3441                         // nothing we can do about this while retaining the feature test so we have to resort to a browser
  3442                         // sniff. I'm not happy about it. See
  3443                         // https://code.google.com/p/chromium/issues/detail?id=399791
  3444                         var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
  3445                         if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
  3446                             selectionSupportsMultipleRanges = false;
  3447                         } else {
  3448                             var r2 = r1.cloneRange();
  3449                             r1.setStart(textNode, 0);
  3450                             r2.setEnd(textNode, 3);
  3451                             r2.setStart(textNode, 2);
  3452                             sel.addRange(r1);
  3453                             sel.addRange(r2);
  3454                             selectionSupportsMultipleRanges = (sel.rangeCount == 2);
  3455                         }
  3456                     }
  3458                     // Clean up
  3459                     dom.removeNode(testEl);
  3460                     sel.removeAllRanges();
  3462                     for (i = 0; i < originalSelectionRangeCount; ++i) {
  3463                         if (i == 0 && originalSelectionBackward) {
  3464                             if (addRangeBackwardToNative) {
  3465                                 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
  3466                             } else {
  3467                                 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
  3468                                 sel.addRange(originalSelectionRanges[i]);
  3469                             }
  3470                         } else {
  3471                             sel.addRange(originalSelectionRanges[i]);
  3472                         }
  3473                     }
  3474                 }
  3475             })();
  3476         }
  3478         features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
  3479         features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
  3481         // ControlRanges
  3482         var implementsControlRange = false, testControlRange;
  3484         if (body && isHostMethod(body, "createControlRange")) {
  3485             testControlRange = body.createControlRange();
  3486             if (util.areHostProperties(testControlRange, ["item", "add"])) {
  3487                 implementsControlRange = true;
  3488             }
  3489         }
  3490         features.implementsControlRange = implementsControlRange;
  3492         // Selection collapsedness
  3493         if (selectionHasAnchorAndFocus) {
  3494             selectionIsCollapsed = function(sel) {
  3495                 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
  3496             };
  3497         } else {
  3498             selectionIsCollapsed = function(sel) {
  3499                 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
  3500             };
  3501         }
  3503         function updateAnchorAndFocusFromRange(sel, range, backward) {
  3504             var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
  3505             sel.anchorNode = range[anchorPrefix + "Container"];
  3506             sel.anchorOffset = range[anchorPrefix + "Offset"];
  3507             sel.focusNode = range[focusPrefix + "Container"];
  3508             sel.focusOffset = range[focusPrefix + "Offset"];
  3509         }
  3511         function updateAnchorAndFocusFromNativeSelection(sel) {
  3512             var nativeSel = sel.nativeSelection;
  3513             sel.anchorNode = nativeSel.anchorNode;
  3514             sel.anchorOffset = nativeSel.anchorOffset;
  3515             sel.focusNode = nativeSel.focusNode;
  3516             sel.focusOffset = nativeSel.focusOffset;
  3517         }
  3519         function updateEmptySelection(sel) {
  3520             sel.anchorNode = sel.focusNode = null;
  3521             sel.anchorOffset = sel.focusOffset = 0;
  3522             sel.rangeCount = 0;
  3523             sel.isCollapsed = true;
  3524             sel._ranges.length = 0;
  3525         }
  3527         function getNativeRange(range) {
  3528             var nativeRange;
  3529             if (range instanceof DomRange) {
  3530                 nativeRange = api.createNativeRange(range.getDocument());
  3531                 nativeRange.setEnd(range.endContainer, range.endOffset);
  3532                 nativeRange.setStart(range.startContainer, range.startOffset);
  3533             } else if (range instanceof WrappedRange) {
  3534                 nativeRange = range.nativeRange;
  3535             } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
  3536                 nativeRange = range;
  3537             }
  3538             return nativeRange;
  3539         }
  3541         function rangeContainsSingleElement(rangeNodes) {
  3542             if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
  3543                 return false;
  3544             }
  3545             for (var i = 1, len = rangeNodes.length; i < len; ++i) {
  3546                 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
  3547                     return false;
  3548                 }
  3549             }
  3550             return true;
  3551         }
  3553         function getSingleElementFromRange(range) {
  3554             var nodes = range.getNodes();
  3555             if (!rangeContainsSingleElement(nodes)) {
  3556                 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
  3557             }
  3558             return nodes[0];
  3559         }
  3561         // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
  3562         function isTextRange(range) {
  3563             return !!range && typeof range.text != "undefined";
  3564         }
  3566         function updateFromTextRange(sel, range) {
  3567             // Create a Range from the selected TextRange
  3568             var wrappedRange = new WrappedRange(range);
  3569             sel._ranges = [wrappedRange];
  3571             updateAnchorAndFocusFromRange(sel, wrappedRange, false);
  3572             sel.rangeCount = 1;
  3573             sel.isCollapsed = wrappedRange.collapsed;
  3574         }
  3576         function updateControlSelection(sel) {
  3577             // Update the wrapped selection based on what's now in the native selection
  3578             sel._ranges.length = 0;
  3579             if (sel.docSelection.type == "None") {
  3580                 updateEmptySelection(sel);
  3581             } else {
  3582                 var controlRange = sel.docSelection.createRange();
  3583                 if (isTextRange(controlRange)) {
  3584                     // This case (where the selection type is "Control" and calling createRange() on the selection returns
  3585                     // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
  3586                     // ControlRange have been removed from the ControlRange and removed from the document.
  3587                     updateFromTextRange(sel, controlRange);
  3588                 } else {
  3589                     sel.rangeCount = controlRange.length;
  3590                     var range, doc = getDocument(controlRange.item(0));
  3591                     for (var i = 0; i < sel.rangeCount; ++i) {
  3592                         range = api.createRange(doc);
  3593                         range.selectNode(controlRange.item(i));
  3594                         sel._ranges.push(range);
  3595                     }
  3596                     sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
  3597                     updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
  3598                 }
  3599             }
  3600         }
  3602         function addRangeToControlSelection(sel, range) {
  3603             var controlRange = sel.docSelection.createRange();
  3604             var rangeElement = getSingleElementFromRange(range);
  3606             // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
  3607             // contained by the supplied range
  3608             var doc = getDocument(controlRange.item(0));
  3609             var newControlRange = getBody(doc).createControlRange();
  3610             for (var i = 0, len = controlRange.length; i < len; ++i) {
  3611                 newControlRange.add(controlRange.item(i));
  3612             }
  3613             try {
  3614                 newControlRange.add(rangeElement);
  3615             } catch (ex) {
  3616                 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
  3617             }
  3618             newControlRange.select();
  3620             // Update the wrapped selection based on what's now in the native selection
  3621             updateControlSelection(sel);
  3622         }
  3624         var getSelectionRangeAt;
  3626         if (isHostMethod(testSelection, "getRangeAt")) {
  3627             // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
  3628             // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
  3629             // lesson to us all, especially me.
  3630             getSelectionRangeAt = function(sel, index) {
  3631                 try {
  3632                     return sel.getRangeAt(index);
  3633                 } catch (ex) {
  3634                     return null;
  3635                 }
  3636             };
  3637         } else if (selectionHasAnchorAndFocus) {
  3638             getSelectionRangeAt = function(sel) {
  3639                 var doc = getDocument(sel.anchorNode);
  3640                 var range = api.createRange(doc);
  3641                 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
  3643                 // Handle the case when the selection was selected backwards (from the end to the start in the
  3644                 // document)
  3645                 if (range.collapsed !== this.isCollapsed) {
  3646                     range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
  3647                 }
  3649                 return range;
  3650             };
  3651         }
  3653         function WrappedSelection(selection, docSelection, win) {
  3654             this.nativeSelection = selection;
  3655             this.docSelection = docSelection;
  3656             this._ranges = [];
  3657             this.win = win;
  3658             this.refresh();
  3659         }
  3661         WrappedSelection.prototype = api.selectionPrototype;
  3663         function deleteProperties(sel) {
  3664             sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
  3665             sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
  3666             sel.detached = true;
  3667         }
  3669         var cachedRangySelections = [];
  3671         function actOnCachedSelection(win, action) {
  3672             var i = cachedRangySelections.length, cached, sel;
  3673             while (i--) {
  3674                 cached = cachedRangySelections[i];
  3675                 sel = cached.selection;
  3676                 if (action == "deleteAll") {
  3677                     deleteProperties(sel);
  3678                 } else if (cached.win == win) {
  3679                     if (action == "delete") {
  3680                         cachedRangySelections.splice(i, 1);
  3681                         return true;
  3682                     } else {
  3683                         return sel;
  3684                     }
  3685                 }
  3686             }
  3687             if (action == "deleteAll") {
  3688                 cachedRangySelections.length = 0;
  3689             }
  3690             return null;
  3691         }
  3693         var getSelection = function(win) {
  3694             // Check if the parameter is a Rangy Selection object
  3695             if (win && win instanceof WrappedSelection) {
  3696                 win.refresh();
  3697                 return win;
  3698             }
  3700             win = getWindow(win, "getNativeSelection");
  3702             var sel = actOnCachedSelection(win);
  3703             var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
  3704             if (sel) {
  3705                 sel.nativeSelection = nativeSel;
  3706                 sel.docSelection = docSel;
  3707                 sel.refresh();
  3708             } else {
  3709                 sel = new WrappedSelection(nativeSel, docSel, win);
  3710                 cachedRangySelections.push( { win: win, selection: sel } );
  3711             }
  3712             return sel;
  3713         };
  3715         api.getSelection = getSelection;
  3717         util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
  3719         var selProto = WrappedSelection.prototype;
  3721         function createControlSelection(sel, ranges) {
  3722             // Ensure that the selection becomes of type "Control"
  3723             var doc = getDocument(ranges[0].startContainer);
  3724             var controlRange = getBody(doc).createControlRange();
  3725             for (var i = 0, el, len = ranges.length; i < len; ++i) {
  3726                 el = getSingleElementFromRange(ranges[i]);
  3727                 try {
  3728                     controlRange.add(el);
  3729                 } catch (ex) {
  3730                     throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
  3731                 }
  3732             }
  3733             controlRange.select();
  3735             // Update the wrapped selection based on what's now in the native selection
  3736             updateControlSelection(sel);
  3737         }
  3739         // Selecting a range
  3740         if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
  3741             selProto.removeAllRanges = function() {
  3742                 this.nativeSelection.removeAllRanges();
  3743                 updateEmptySelection(this);
  3744             };
  3746             var addRangeBackward = function(sel, range) {
  3747                 addRangeBackwardToNative(sel.nativeSelection, range);
  3748                 sel.refresh();
  3749             };
  3751             if (selectionHasRangeCount) {
  3752                 selProto.addRange = function(range, direction) {
  3753                     if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
  3754                         addRangeToControlSelection(this, range);
  3755                     } else {
  3756                         if (isDirectionBackward(direction) && selectionHasExtend) {
  3757                             addRangeBackward(this, range);
  3758                         } else {
  3759                             var previousRangeCount;
  3760                             if (selectionSupportsMultipleRanges) {
  3761                                 previousRangeCount = this.rangeCount;
  3762                             } else {
  3763                                 this.removeAllRanges();
  3764                                 previousRangeCount = 0;
  3765                             }
  3766                             // Clone the native range so that changing the selected range does not affect the selection.
  3767                             // This is contrary to the spec but is the only way to achieve consistency between browsers. See
  3768                             // issue 80.
  3769                             var clonedNativeRange = getNativeRange(range).cloneRange();
  3770                             try {
  3771                                 this.nativeSelection.addRange(clonedNativeRange);
  3772                             } catch (ex) {
  3773                             }
  3775                             // Check whether adding the range was successful
  3776                             this.rangeCount = this.nativeSelection.rangeCount;
  3778                             if (this.rangeCount == previousRangeCount + 1) {
  3779                                 // The range was added successfully
  3781                                 // Check whether the range that we added to the selection is reflected in the last range extracted from
  3782                                 // the selection
  3783                                 if (api.config.checkSelectionRanges) {
  3784                                     var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
  3785                                     if (nativeRange && !rangesEqual(nativeRange, range)) {
  3786                                         // Happens in WebKit with, for example, a selection placed at the start of a text node
  3787                                         range = new WrappedRange(nativeRange);
  3788                                     }
  3789                                 }
  3790                                 this._ranges[this.rangeCount - 1] = range;
  3791                                 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
  3792                                 this.isCollapsed = selectionIsCollapsed(this);
  3793                             } else {
  3794                                 // The range was not added successfully. The simplest thing is to refresh
  3795                                 this.refresh();
  3796                             }
  3797                         }
  3798                     }
  3799                 };
  3800             } else {
  3801                 selProto.addRange = function(range, direction) {
  3802                     if (isDirectionBackward(direction) && selectionHasExtend) {
  3803                         addRangeBackward(this, range);
  3804                     } else {
  3805                         this.nativeSelection.addRange(getNativeRange(range));
  3806                         this.refresh();
  3807                     }
  3808                 };
  3809             }
  3811             selProto.setRanges = function(ranges) {
  3812                 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
  3813                     createControlSelection(this, ranges);
  3814                 } else {
  3815                     this.removeAllRanges();
  3816                     for (var i = 0, len = ranges.length; i < len; ++i) {
  3817                         this.addRange(ranges[i]);
  3818                     }
  3819                 }
  3820             };
  3821         } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
  3822                    implementsControlRange && useDocumentSelection) {
  3824             selProto.removeAllRanges = function() {
  3825                 // Added try/catch as fix for issue #21
  3826                 try {
  3827                     this.docSelection.empty();
  3829                     // Check for empty() not working (issue #24)
  3830                     if (this.docSelection.type != "None") {
  3831                         // Work around failure to empty a control selection by instead selecting a TextRange and then
  3832                         // calling empty()
  3833                         var doc;
  3834                         if (this.anchorNode) {
  3835                             doc = getDocument(this.anchorNode);
  3836                         } else if (this.docSelection.type == CONTROL) {
  3837                             var controlRange = this.docSelection.createRange();
  3838                             if (controlRange.length) {
  3839                                 doc = getDocument( controlRange.item(0) );
  3840                             }
  3841                         }
  3842                         if (doc) {
  3843                             var textRange = getBody(doc).createTextRange();
  3844                             textRange.select();
  3845                             this.docSelection.empty();
  3846                         }
  3847                     }
  3848                 } catch(ex) {}
  3849                 updateEmptySelection(this);
  3850             };
  3852             selProto.addRange = function(range) {
  3853                 if (this.docSelection.type == CONTROL) {
  3854                     addRangeToControlSelection(this, range);
  3855                 } else {
  3856                     api.WrappedTextRange.rangeToTextRange(range).select();
  3857                     this._ranges[0] = range;
  3858                     this.rangeCount = 1;
  3859                     this.isCollapsed = this._ranges[0].collapsed;
  3860                     updateAnchorAndFocusFromRange(this, range, false);
  3861                 }
  3862             };
  3864             selProto.setRanges = function(ranges) {
  3865                 this.removeAllRanges();
  3866                 var rangeCount = ranges.length;
  3867                 if (rangeCount > 1) {
  3868                     createControlSelection(this, ranges);
  3869                 } else if (rangeCount) {
  3870                     this.addRange(ranges[0]);
  3871                 }
  3872             };
  3873         } else {
  3874             module.fail("No means of selecting a Range or TextRange was found");
  3875             return false;
  3876         }
  3878         selProto.getRangeAt = function(index) {
  3879             if (index < 0 || index >= this.rangeCount) {
  3880                 throw new DOMException("INDEX_SIZE_ERR");
  3881             } else {
  3882                 // Clone the range to preserve selection-range independence. See issue 80.
  3883                 return this._ranges[index].cloneRange();
  3884             }
  3885         };
  3887         var refreshSelection;
  3889         if (useDocumentSelection) {
  3890             refreshSelection = function(sel) {
  3891                 var range;
  3892                 if (api.isSelectionValid(sel.win)) {
  3893                     range = sel.docSelection.createRange();
  3894                 } else {
  3895                     range = getBody(sel.win.document).createTextRange();
  3896                     range.collapse(true);
  3897                 }
  3899                 if (sel.docSelection.type == CONTROL) {
  3900                     updateControlSelection(sel);
  3901                 } else if (isTextRange(range)) {
  3902                     updateFromTextRange(sel, range);
  3903                 } else {
  3904                     updateEmptySelection(sel);
  3905                 }
  3906             };
  3907         } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
  3908             refreshSelection = function(sel) {
  3909                 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
  3910                     updateControlSelection(sel);
  3911                 } else {
  3912                     sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
  3913                     if (sel.rangeCount) {
  3914                         for (var i = 0, len = sel.rangeCount; i < len; ++i) {
  3915                             sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
  3916                         }
  3917                         updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
  3918                         sel.isCollapsed = selectionIsCollapsed(sel);
  3919                     } else {
  3920                         updateEmptySelection(sel);
  3921                     }
  3922                 }
  3923             };
  3924         } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
  3925             refreshSelection = function(sel) {
  3926                 var range, nativeSel = sel.nativeSelection;
  3927                 if (nativeSel.anchorNode) {
  3928                     range = getSelectionRangeAt(nativeSel, 0);
  3929                     sel._ranges = [range];
  3930                     sel.rangeCount = 1;
  3931                     updateAnchorAndFocusFromNativeSelection(sel);
  3932                     sel.isCollapsed = selectionIsCollapsed(sel);
  3933                 } else {
  3934                     updateEmptySelection(sel);
  3935                 }
  3936             };
  3937         } else {
  3938             module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
  3939             return false;
  3940         }
  3942         selProto.refresh = function(checkForChanges) {
  3943             var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
  3944             var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
  3946             refreshSelection(this);
  3947             if (checkForChanges) {
  3948                 // Check the range count first
  3949                 var i = oldRanges.length;
  3950                 if (i != this._ranges.length) {
  3951                     return true;
  3952                 }
  3954                 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
  3955                 // ranges after this
  3956                 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
  3957                     return true;
  3958                 }
  3960                 // Finally, compare each range in turn
  3961                 while (i--) {
  3962                     if (!rangesEqual(oldRanges[i], this._ranges[i])) {
  3963                         return true;
  3964                     }
  3965                 }
  3966                 return false;
  3967             }
  3968         };
  3970         // Removal of a single range
  3971         var removeRangeManually = function(sel, range) {
  3972             var ranges = sel.getAllRanges();
  3973             sel.removeAllRanges();
  3974             for (var i = 0, len = ranges.length; i < len; ++i) {
  3975                 if (!rangesEqual(range, ranges[i])) {
  3976                     sel.addRange(ranges[i]);
  3977                 }
  3978             }
  3979             if (!sel.rangeCount) {
  3980                 updateEmptySelection(sel);
  3981             }
  3982         };
  3984         if (implementsControlRange && implementsDocSelection) {
  3985             selProto.removeRange = function(range) {
  3986                 if (this.docSelection.type == CONTROL) {
  3987                     var controlRange = this.docSelection.createRange();
  3988                     var rangeElement = getSingleElementFromRange(range);
  3990                     // Create a new ControlRange containing all the elements in the selected ControlRange minus the
  3991                     // element contained by the supplied range
  3992                     var doc = getDocument(controlRange.item(0));
  3993                     var newControlRange = getBody(doc).createControlRange();
  3994                     var el, removed = false;
  3995                     for (var i = 0, len = controlRange.length; i < len; ++i) {
  3996                         el = controlRange.item(i);
  3997                         if (el !== rangeElement || removed) {
  3998                             newControlRange.add(controlRange.item(i));
  3999                         } else {
  4000                             removed = true;
  4001                         }
  4002                     }
  4003                     newControlRange.select();
  4005                     // Update the wrapped selection based on what's now in the native selection
  4006                     updateControlSelection(this);
  4007                 } else {
  4008                     removeRangeManually(this, range);
  4009                 }
  4010             };
  4011         } else {
  4012             selProto.removeRange = function(range) {
  4013                 removeRangeManually(this, range);
  4014             };
  4015         }
  4017         // Detecting if a selection is backward
  4018         var selectionIsBackward;
  4019         if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
  4020             selectionIsBackward = winSelectionIsBackward;
  4022             selProto.isBackward = function() {
  4023                 return selectionIsBackward(this);
  4024             };
  4025         } else {
  4026             selectionIsBackward = selProto.isBackward = function() {
  4027                 return false;
  4028             };
  4029         }
  4031         // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
  4032         selProto.isBackwards = selProto.isBackward;
  4034         // Selection stringifier
  4035         // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
  4036         // The current spec does not yet define this method.
  4037         selProto.toString = function() {
  4038             var rangeTexts = [];
  4039             for (var i = 0, len = this.rangeCount; i < len; ++i) {
  4040                 rangeTexts[i] = "" + this._ranges[i];
  4041             }
  4042             return rangeTexts.join("");
  4043         };
  4045         function assertNodeInSameDocument(sel, node) {
  4046             if (sel.win.document != getDocument(node)) {
  4047                 throw new DOMException("WRONG_DOCUMENT_ERR");
  4048             }
  4049         }
  4051         // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
  4052         selProto.collapse = function(node, offset) {
  4053             assertNodeInSameDocument(this, node);
  4054             var range = api.createRange(node);
  4055             range.collapseToPoint(node, offset);
  4056             this.setSingleRange(range);
  4057             this.isCollapsed = true;
  4058         };
  4060         selProto.collapseToStart = function() {
  4061             if (this.rangeCount) {
  4062                 var range = this._ranges[0];
  4063                 this.collapse(range.startContainer, range.startOffset);
  4064             } else {
  4065                 throw new DOMException("INVALID_STATE_ERR");
  4066             }
  4067         };
  4069         selProto.collapseToEnd = function() {
  4070             if (this.rangeCount) {
  4071                 var range = this._ranges[this.rangeCount - 1];
  4072                 this.collapse(range.endContainer, range.endOffset);
  4073             } else {
  4074                 throw new DOMException("INVALID_STATE_ERR");
  4075             }
  4076         };
  4078         // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
  4079         // specified so the native implementation is never used by Rangy.
  4080         selProto.selectAllChildren = function(node) {
  4081             assertNodeInSameDocument(this, node);
  4082             var range = api.createRange(node);
  4083             range.selectNodeContents(node);
  4084             this.setSingleRange(range);
  4085         };
  4087         selProto.deleteFromDocument = function() {
  4088             // Sepcial behaviour required for IE's control selections
  4089             if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
  4090                 var controlRange = this.docSelection.createRange();
  4091                 var element;
  4092                 while (controlRange.length) {
  4093                     element = controlRange.item(0);
  4094                     controlRange.remove(element);
  4095                     dom.removeNode(element);
  4096                 }
  4097                 this.refresh();
  4098             } else if (this.rangeCount) {
  4099                 var ranges = this.getAllRanges();
  4100                 if (ranges.length) {
  4101                     this.removeAllRanges();
  4102                     for (var i = 0, len = ranges.length; i < len; ++i) {
  4103                         ranges[i].deleteContents();
  4104                     }
  4105                     // The spec says nothing about what the selection should contain after calling deleteContents on each
  4106                     // range. Firefox moves the selection to where the final selected range was, so we emulate that
  4107                     this.addRange(ranges[len - 1]);
  4108                 }
  4109             }
  4110         };
  4112         // The following are non-standard extensions
  4113         selProto.eachRange = function(func, returnValue) {
  4114             for (var i = 0, len = this._ranges.length; i < len; ++i) {
  4115                 if ( func( this.getRangeAt(i) ) ) {
  4116                     return returnValue;
  4117                 }
  4118             }
  4119         };
  4121         selProto.getAllRanges = function() {
  4122             var ranges = [];
  4123             this.eachRange(function(range) {
  4124                 ranges.push(range);
  4125             });
  4126             return ranges;
  4127         };
  4129         selProto.setSingleRange = function(range, direction) {
  4130             this.removeAllRanges();
  4131             this.addRange(range, direction);
  4132         };
  4134         selProto.callMethodOnEachRange = function(methodName, params) {
  4135             var results = [];
  4136             this.eachRange( function(range) {
  4137                 results.push( range[methodName].apply(range, params || []) );
  4138             } );
  4139             return results;
  4140         };
  4142         function createStartOrEndSetter(isStart) {
  4143             return function(node, offset) {
  4144                 var range;
  4145                 if (this.rangeCount) {
  4146                     range = this.getRangeAt(0);
  4147                     range["set" + (isStart ? "Start" : "End")](node, offset);
  4148                 } else {
  4149                     range = api.createRange(this.win.document);
  4150                     range.setStartAndEnd(node, offset);
  4151                 }
  4152                 this.setSingleRange(range, this.isBackward());
  4153             };
  4154         }
  4156         selProto.setStart = createStartOrEndSetter(true);
  4157         selProto.setEnd = createStartOrEndSetter(false);
  4159         // Add select() method to Range prototype. Any existing selection will be removed.
  4160         api.rangePrototype.select = function(direction) {
  4161             getSelection( this.getDocument() ).setSingleRange(this, direction);
  4162         };
  4164         selProto.changeEachRange = function(func) {
  4165             var ranges = [];
  4166             var backward = this.isBackward();
  4168             this.eachRange(function(range) {
  4169                 func(range);
  4170                 ranges.push(range);
  4171             });
  4173             this.removeAllRanges();
  4174             if (backward && ranges.length == 1) {
  4175                 this.addRange(ranges[0], "backward");
  4176             } else {
  4177                 this.setRanges(ranges);
  4178             }
  4179         };
  4181         selProto.containsNode = function(node, allowPartial) {
  4182             return this.eachRange( function(range) {
  4183                 return range.containsNode(node, allowPartial);
  4184             }, true ) || false;
  4185         };
  4187         selProto.getBookmark = function(containerNode) {
  4188             return {
  4189                 backward: this.isBackward(),
  4190                 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
  4191             };
  4192         };
  4194         selProto.moveToBookmark = function(bookmark) {
  4195             var selRanges = [];
  4196             for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
  4197                 range = api.createRange(this.win);
  4198                 range.moveToBookmark(rangeBookmark);
  4199                 selRanges.push(range);
  4200             }
  4201             if (bookmark.backward) {
  4202                 this.setSingleRange(selRanges[0], "backward");
  4203             } else {
  4204                 this.setRanges(selRanges);
  4205             }
  4206         };
  4208         selProto.saveRanges = function() {
  4209             return {
  4210                 backward: this.isBackward(),
  4211                 ranges: this.callMethodOnEachRange("cloneRange")
  4212             };
  4213         };
  4215         selProto.restoreRanges = function(selRanges) {
  4216             this.removeAllRanges();
  4217             for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
  4218                 this.addRange(range, (selRanges.backward && i == 0));
  4219             }
  4220         };
  4222         selProto.toHtml = function() {
  4223             var rangeHtmls = [];
  4224             this.eachRange(function(range) {
  4225                 rangeHtmls.push( DomRange.toHtml(range) );
  4226             });
  4227             return rangeHtmls.join("");
  4228         };
  4230         if (features.implementsTextRange) {
  4231             selProto.getNativeTextRange = function() {
  4232                 var sel, textRange;
  4233                 if ( (sel = this.docSelection) ) {
  4234                     var range = sel.createRange();
  4235                     if (isTextRange(range)) {
  4236                         return range;
  4237                     } else {
  4238                         throw module.createError("getNativeTextRange: selection is a control selection");
  4239                     }
  4240                 } else if (this.rangeCount > 0) {
  4241                     return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
  4242                 } else {
  4243                     throw module.createError("getNativeTextRange: selection contains no range");
  4244                 }
  4245             };
  4246         }
  4248         function inspect(sel) {
  4249             var rangeInspects = [];
  4250             var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
  4251             var focus = new DomPosition(sel.focusNode, sel.focusOffset);
  4252             var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
  4254             if (typeof sel.rangeCount != "undefined") {
  4255                 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
  4256                     rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
  4257                 }
  4258             }
  4259             return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
  4260                     ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
  4261         }
  4263         selProto.getName = function() {
  4264             return "WrappedSelection";
  4265         };
  4267         selProto.inspect = function() {
  4268             return inspect(this);
  4269         };
  4271         selProto.detach = function() {
  4272             actOnCachedSelection(this.win, "delete");
  4273             deleteProperties(this);
  4274         };
  4276         WrappedSelection.detachAll = function() {
  4277             actOnCachedSelection(null, "deleteAll");
  4278         };
  4280         WrappedSelection.inspect = inspect;
  4281         WrappedSelection.isDirectionBackward = isDirectionBackward;
  4283         api.Selection = WrappedSelection;
  4285         api.selectionPrototype = selProto;
  4287         api.addShimListener(function(win) {
  4288             if (typeof win.getSelection == "undefined") {
  4289                 win.getSelection = function() {
  4290                     return getSelection(win);
  4291                 };
  4292             }
  4293             win = null;
  4294         });
  4295     });
  4298     /*----------------------------------------------------------------------------------------------------------------*/
  4300     // Wait for document to load before initializing
  4301     var docReady = false;
  4303     var loadHandler = function(e) {
  4304         if (!docReady) {
  4305             docReady = true;
  4306             if (!api.initialized && api.config.autoInitialize) {
  4307                 init();
  4308             }
  4309         }
  4310     };
  4312     if (isBrowser) {
  4313         // Test whether the document has already been loaded and initialize immediately if so
  4314         if (document.readyState == "complete") {
  4315             loadHandler();
  4316         } else {
  4317             if (isHostMethod(document, "addEventListener")) {
  4318                 document.addEventListener("DOMContentLoaded", loadHandler, false);
  4319             }
  4321             // Add a fallback in case the DOMContentLoaded event isn't supported
  4322             addListener(window, "load", loadHandler);
  4323         }
  4324     }
  4326     rangy = api;
  4327 })();
  4329 /**
  4330  * Selection save and restore module for Rangy.
  4331  * Saves and restores user selections using marker invisible elements in the DOM.
  4332  *
  4333  * Part of Rangy, a cross-browser JavaScript range and selection library
  4334  * https://github.com/timdown/rangy
  4335  *
  4336  * Depends on Rangy core.
  4337  *
  4338  * Copyright 2015, Tim Down
  4339  * Licensed under the MIT license.
  4340  * Version: 1.3.1-dev
  4341  * Build date: 20 May 2015
  4342  *
  4343 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
  4344 */
  4345 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
  4346     var dom = api.dom;
  4347     var removeNode = dom.removeNode;
  4348     var isDirectionBackward = api.Selection.isDirectionBackward;
  4349     var markerTextChar = "\ufeff";
  4351     function gEBI(id, doc) {
  4352         return (doc || document).getElementById(id);
  4353     }
  4355     function insertRangeBoundaryMarker(range, atStart) {
  4356         var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
  4357         var markerEl;
  4358         var doc = dom.getDocument(range.startContainer);
  4360         // Clone the Range and collapse to the appropriate boundary point
  4361         var boundaryRange = range.cloneRange();
  4362         boundaryRange.collapse(atStart);
  4364         // Create the marker element containing a single invisible character using DOM methods and insert it
  4365         markerEl = doc.createElement("span");
  4366         markerEl.id = markerId;
  4367         markerEl.style.lineHeight = "0";
  4368         markerEl.style.display = "none";
  4369         markerEl.className = "rangySelectionBoundary";
  4370         markerEl.appendChild(doc.createTextNode(markerTextChar));
  4372         boundaryRange.insertNode(markerEl);
  4373         return markerEl;
  4374     }
  4376     function setRangeBoundary(doc, range, markerId, atStart) {
  4377         var markerEl = gEBI(markerId, doc);
  4378         if (markerEl) {
  4379             range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
  4380             removeNode(markerEl);
  4381         } else {
  4382             module.warn("Marker element has been removed. Cannot restore selection.");
  4383         }
  4384     }
  4386     function compareRanges(r1, r2) {
  4387         return r2.compareBoundaryPoints(r1.START_TO_START, r1);
  4388     }
  4390     function saveRange(range, direction) {
  4391         var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
  4392         var backward = isDirectionBackward(direction);
  4394         if (range.collapsed) {
  4395             endEl = insertRangeBoundaryMarker(range, false);
  4396             return {
  4397                 document: doc,
  4398                 markerId: endEl.id,
  4399                 collapsed: true
  4400             };
  4401         } else {
  4402             endEl = insertRangeBoundaryMarker(range, false);
  4403             startEl = insertRangeBoundaryMarker(range, true);
  4405             return {
  4406                 document: doc,
  4407                 startMarkerId: startEl.id,
  4408                 endMarkerId: endEl.id,
  4409                 collapsed: false,
  4410                 backward: backward,
  4411                 toString: function() {
  4412                     return "original text: '" + text + "', new text: '" + range.toString() + "'";
  4413                 }
  4414             };
  4415         }
  4416     }
  4418     function restoreRange(rangeInfo, normalize) {
  4419         var doc = rangeInfo.document;
  4420         if (typeof normalize == "undefined") {
  4421             normalize = true;
  4422         }
  4423         var range = api.createRange(doc);
  4424         if (rangeInfo.collapsed) {
  4425             var markerEl = gEBI(rangeInfo.markerId, doc);
  4426             if (markerEl) {
  4427                 markerEl.style.display = "inline";
  4428                 var previousNode = markerEl.previousSibling;
  4430                 // Workaround for issue 17
  4431                 if (previousNode && previousNode.nodeType == 3) {
  4432                     removeNode(markerEl);
  4433                     range.collapseToPoint(previousNode, previousNode.length);
  4434                 } else {
  4435                     range.collapseBefore(markerEl);
  4436                     removeNode(markerEl);
  4437                 }
  4438             } else {
  4439                 module.warn("Marker element has been removed. Cannot restore selection.");
  4440             }
  4441         } else {
  4442             setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
  4443             setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
  4444         }
  4446         if (normalize) {
  4447             range.normalizeBoundaries();
  4448         }
  4450         return range;
  4451     }
  4453     function saveRanges(ranges, direction) {
  4454         var rangeInfos = [], range, doc;
  4455         var backward = isDirectionBackward(direction);
  4457         // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
  4458         ranges = ranges.slice(0);
  4459         ranges.sort(compareRanges);
  4461         for (var i = 0, len = ranges.length; i < len; ++i) {
  4462             rangeInfos[i] = saveRange(ranges[i], backward);
  4463         }
  4465         // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
  4466         // between its markers
  4467         for (i = len - 1; i >= 0; --i) {
  4468             range = ranges[i];
  4469             doc = api.DomRange.getRangeDocument(range);
  4470             if (range.collapsed) {
  4471                 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
  4472             } else {
  4473                 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
  4474                 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
  4475             }
  4476         }
  4478         return rangeInfos;
  4479     }
  4481     function saveSelection(win) {
  4482         if (!api.isSelectionValid(win)) {
  4483             module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
  4484             return null;
  4485         }
  4486         var sel = api.getSelection(win);
  4487         var ranges = sel.getAllRanges();
  4488         var backward = (ranges.length == 1 && sel.isBackward());
  4490         var rangeInfos = saveRanges(ranges, backward);
  4492         // Ensure current selection is unaffected
  4493         if (backward) {
  4494             sel.setSingleRange(ranges[0], backward);
  4495         } else {
  4496             sel.setRanges(ranges);
  4497         }
  4499         return {
  4500             win: win,
  4501             rangeInfos: rangeInfos,
  4502             restored: false
  4503         };
  4504     }
  4506     function restoreRanges(rangeInfos) {
  4507         var ranges = [];
  4509         // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
  4510         // normalization affecting previously restored ranges.
  4511         var rangeCount = rangeInfos.length;
  4513         for (var i = rangeCount - 1; i >= 0; i--) {
  4514             ranges[i] = restoreRange(rangeInfos[i], true);
  4515         }
  4517         return ranges;
  4518     }
  4520     function restoreSelection(savedSelection, preserveDirection) {
  4521         if (!savedSelection.restored) {
  4522             var rangeInfos = savedSelection.rangeInfos;
  4523             var sel = api.getSelection(savedSelection.win);
  4524             var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
  4526             if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
  4527                 sel.removeAllRanges();
  4528                 sel.addRange(ranges[0], true);
  4529             } else {
  4530                 sel.setRanges(ranges);
  4531             }
  4533             savedSelection.restored = true;
  4534         }
  4535     }
  4537     function removeMarkerElement(doc, markerId) {
  4538         var markerEl = gEBI(markerId, doc);
  4539         if (markerEl) {
  4540             removeNode(markerEl);
  4541         }
  4542     }
  4544     function removeMarkers(savedSelection) {
  4545         var rangeInfos = savedSelection.rangeInfos;
  4546         for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
  4547             rangeInfo = rangeInfos[i];
  4548             if (rangeInfo.collapsed) {
  4549                 removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
  4550             } else {
  4551                 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
  4552                 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
  4553             }
  4554         }
  4555     }
  4557     api.util.extend(api, {
  4558         saveRange: saveRange,
  4559         restoreRange: restoreRange,
  4560         saveRanges: saveRanges,
  4561         restoreRanges: restoreRanges,
  4562         saveSelection: saveSelection,
  4563         restoreSelection: restoreSelection,
  4564         removeMarkerElement: removeMarkerElement,
  4565         removeMarkers: removeMarkers
  4566     });
  4567 });
  4569 /**
  4570  * Text range module for Rangy.
  4571  * Text-based manipulation and searching of ranges and selections.
  4572  *
  4573  * Features
  4574  *
  4575  * - Ability to move range boundaries by character or word offsets
  4576  * - Customizable word tokenizer
  4577  * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
  4578  * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
  4579  *   sensitivity
  4580  * - Selection and range save/restore as text offsets within a node
  4581  * - Methods to return visible text within a range or selection
  4582  * - innerText method for elements
  4583  *
  4584  * References
  4585  *
  4586  * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
  4587  * http://aryeh.name/spec/innertext/innertext.html
  4588  * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
  4589  *
  4590  * Part of Rangy, a cross-browser JavaScript range and selection library
  4591  * https://github.com/timdown/rangy
  4592  *
  4593  * Depends on Rangy core.
  4594  *
  4595  * Copyright 2015, Tim Down
  4596  * Licensed under the MIT license.
  4597  * Version: 1.3.1-dev
  4598  * Build date: 20 May 2015
  4599  */
  4601 /**
  4602  * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
  4603  *
  4604  * First, a <br>: this is relatively simple. For the following HTML:
  4605  *
  4606  * 1 <br>2
  4607  *
  4608  * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
  4609  *   textarea, the space is present) and allow the caret to be placed after it.
  4610  * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
  4611  * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
  4612  *   arrow keys show this) and includes the space in the selection.
  4613  *
  4614  * The other case is the line break or breaks implied by block elements. For the following HTML:
  4615  *
  4616  * <p>1 </p><p>2<p>
  4617  *
  4618  * - WebKit does not acknowledge the space in any way
  4619  * - Firefox, IE and Opera as per <br>
  4620  *
  4621  * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
  4622  *
  4623  * <p style="white-space: pre-line">1
  4624  * 2</p>
  4625  *
  4626  * - Firefox and WebKit include the space in caret positions
  4627  * - IE does not support pre-line up to and including version 9
  4628  * - Opera ignores the space
  4629  * - Trailing space only renders if there is a non-collapsed character in the line
  4630  *
  4631  * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
  4632  * feature-tested
  4633  *
  4634  * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
  4635 */
  4636 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
  4637     var UNDEF = "undefined";
  4638     var CHARACTER = "character", WORD = "word";
  4639     var dom = api.dom, util = api.util;
  4640     var extend = util.extend;
  4641     var createOptions = util.createOptions;
  4642     var getBody = dom.getBody;
  4645     var spacesRegex = /^[ \t\f\r\n]+$/;
  4646     var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
  4647     var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
  4648     var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
  4649     var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
  4651     var defaultLanguage = "en";
  4653     var isDirectionBackward = api.Selection.isDirectionBackward;
  4655     // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
  4656     // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
  4657     var trailingSpaceInBlockCollapses = false;
  4658     var trailingSpaceBeforeBrCollapses = false;
  4659     var trailingSpaceBeforeBlockCollapses = false;
  4660     var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
  4662     (function() {
  4663         var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
  4664         var p = el.firstChild;
  4665         var sel = api.getSelection();
  4666         sel.collapse(p.lastChild, 2);
  4667         sel.setStart(p.firstChild, 0);
  4668         trailingSpaceInBlockCollapses = ("" + sel).length == 1;
  4670         el.innerHTML = "1 <br />";
  4671         sel.collapse(el, 2);
  4672         sel.setStart(el.firstChild, 0);
  4673         trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
  4675         el.innerHTML = "1 <p>1</p>";
  4676         sel.collapse(el, 2);
  4677         sel.setStart(el.firstChild, 0);
  4678         trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
  4680         dom.removeNode(el);
  4681         sel.removeAllRanges();
  4682     })();
  4684     /*----------------------------------------------------------------------------------------------------------------*/
  4686     // This function must create word and non-word tokens for the whole of the text supplied to it
  4687     function defaultTokenizer(chars, wordOptions) {
  4688         var word = chars.join(""), result, tokenRanges = [];
  4690         function createTokenRange(start, end, isWord) {
  4691             tokenRanges.push( { start: start, end: end, isWord: isWord } );
  4692         }
  4694         // Match words and mark characters
  4695         var lastWordEnd = 0, wordStart, wordEnd;
  4696         while ( (result = wordOptions.wordRegex.exec(word)) ) {
  4697             wordStart = result.index;
  4698             wordEnd = wordStart + result[0].length;
  4700             // Create token for non-word characters preceding this word
  4701             if (wordStart > lastWordEnd) {
  4702                 createTokenRange(lastWordEnd, wordStart, false);
  4703             }
  4705             // Get trailing space characters for word
  4706             if (wordOptions.includeTrailingSpace) {
  4707                 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
  4708                     ++wordEnd;
  4709                 }
  4710             }
  4711             createTokenRange(wordStart, wordEnd, true);
  4712             lastWordEnd = wordEnd;
  4713         }
  4715         // Create token for trailing non-word characters, if any exist
  4716         if (lastWordEnd < chars.length) {
  4717             createTokenRange(lastWordEnd, chars.length, false);
  4718         }
  4720         return tokenRanges;
  4721     }
  4723     function convertCharRangeToToken(chars, tokenRange) {
  4724         var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
  4725         var token = {
  4726             isWord: tokenRange.isWord,
  4727             chars: tokenChars,
  4728             toString: function() {
  4729                 return tokenChars.join("");
  4730             }
  4731         };
  4732         for (var i = 0, len = tokenChars.length; i < len; ++i) {
  4733             tokenChars[i].token = token;
  4734         }
  4735         return token;
  4736     }
  4738     function tokenize(chars, wordOptions, tokenizer) {
  4739         var tokenRanges = tokenizer(chars, wordOptions);
  4740         var tokens = [];
  4741         for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
  4742             tokens.push( convertCharRangeToToken(chars, tokenRange) );
  4743         }
  4744         return tokens;
  4745     }
  4747     var defaultCharacterOptions = {
  4748         includeBlockContentTrailingSpace: true,
  4749         includeSpaceBeforeBr: true,
  4750         includeSpaceBeforeBlock: true,
  4751         includePreLineTrailingSpace: true,
  4752         ignoreCharacters: ""
  4753     };
  4755     function normalizeIgnoredCharacters(ignoredCharacters) {
  4756         // Check if character is ignored
  4757         var ignoredChars = ignoredCharacters || "";
  4759         // Normalize ignored characters into a string consisting of characters in ascending order of character code
  4760         var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
  4761         ignoredCharsArray.sort(function(char1, char2) {
  4762             return char1.charCodeAt(0) - char2.charCodeAt(0);
  4763         });
  4765         /// Convert back to a string and remove duplicates
  4766         return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
  4767     }
  4769     var defaultCaretCharacterOptions = {
  4770         includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
  4771         includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
  4772         includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
  4773         includePreLineTrailingSpace: true
  4774     };
  4776     var defaultWordOptions = {
  4777         "en": {
  4778             wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
  4779             includeTrailingSpace: false,
  4780             tokenizer: defaultTokenizer
  4781         }
  4782     };
  4784     var defaultFindOptions = {
  4785         caseSensitive: false,
  4786         withinRange: null,
  4787         wholeWordsOnly: false,
  4788         wrap: false,
  4789         direction: "forward",
  4790         wordOptions: null,
  4791         characterOptions: null
  4792     };
  4794     var defaultMoveOptions = {
  4795         wordOptions: null,
  4796         characterOptions: null
  4797     };
  4799     var defaultExpandOptions = {
  4800         wordOptions: null,
  4801         characterOptions: null,
  4802         trim: false,
  4803         trimStart: true,
  4804         trimEnd: true
  4805     };
  4807     var defaultWordIteratorOptions = {
  4808         wordOptions: null,
  4809         characterOptions: null,
  4810         direction: "forward"
  4811     };
  4813     function createWordOptions(options) {
  4814         var lang, defaults;
  4815         if (!options) {
  4816             return defaultWordOptions[defaultLanguage];
  4817         } else {
  4818             lang = options.language || defaultLanguage;
  4819             defaults = {};
  4820             extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
  4821             extend(defaults, options);
  4822             return defaults;
  4823         }
  4824     }
  4826     function createNestedOptions(optionsParam, defaults) {
  4827         var options = createOptions(optionsParam, defaults);
  4828         if (defaults.hasOwnProperty("wordOptions")) {
  4829             options.wordOptions = createWordOptions(options.wordOptions);
  4830         }
  4831         if (defaults.hasOwnProperty("characterOptions")) {
  4832             options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
  4833         }
  4834         return options;
  4835     }
  4837     /*----------------------------------------------------------------------------------------------------------------*/
  4839     /* DOM utility functions */
  4840     var getComputedStyleProperty = dom.getComputedStyleProperty;
  4842     // Create cachable versions of DOM functions
  4844     // Test for old IE's incorrect display properties
  4845     var tableCssDisplayBlock;
  4846     (function() {
  4847         var table = document.createElement("table");
  4848         var body = getBody(document);
  4849         body.appendChild(table);
  4850         tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
  4851         body.removeChild(table);
  4852     })();
  4854     var defaultDisplayValueForTag = {
  4855         table: "table",
  4856         caption: "table-caption",
  4857         colgroup: "table-column-group",
  4858         col: "table-column",
  4859         thead: "table-header-group",
  4860         tbody: "table-row-group",
  4861         tfoot: "table-footer-group",
  4862         tr: "table-row",
  4863         td: "table-cell",
  4864         th: "table-cell"
  4865     };
  4867     // Corrects IE's "block" value for table-related elements
  4868     function getComputedDisplay(el, win) {
  4869         var display = getComputedStyleProperty(el, "display", win);
  4870         var tagName = el.tagName.toLowerCase();
  4871         return (display == "block" &&
  4872                 tableCssDisplayBlock &&
  4873                 defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
  4874             defaultDisplayValueForTag[tagName] : display;
  4875     }
  4877     function isHidden(node) {
  4878         var ancestors = getAncestorsAndSelf(node);
  4879         for (var i = 0, len = ancestors.length; i < len; ++i) {
  4880             if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
  4881                 return true;
  4882             }
  4883         }
  4885         return false;
  4886     }
  4888     function isVisibilityHiddenTextNode(textNode) {
  4889         var el;
  4890         return textNode.nodeType == 3 &&
  4891             (el = textNode.parentNode) &&
  4892             getComputedStyleProperty(el, "visibility") == "hidden";
  4893     }
  4895     /*----------------------------------------------------------------------------------------------------------------*/
  4898     // "A block node is either an Element whose "display" property does not have
  4899     // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
  4900     // Document, or a DocumentFragment."
  4901     function isBlockNode(node) {
  4902         return node &&
  4903             ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
  4904             node.nodeType == 9 || node.nodeType == 11);
  4905     }
  4907     function getLastDescendantOrSelf(node) {
  4908         var lastChild = node.lastChild;
  4909         return lastChild ? getLastDescendantOrSelf(lastChild) : node;
  4910     }
  4912     function containsPositions(node) {
  4913         return dom.isCharacterDataNode(node) ||
  4914             !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
  4915     }
  4917     function getAncestors(node) {
  4918         var ancestors = [];
  4919         while (node.parentNode) {
  4920             ancestors.unshift(node.parentNode);
  4921             node = node.parentNode;
  4922         }
  4923         return ancestors;
  4924     }
  4926     function getAncestorsAndSelf(node) {
  4927         return getAncestors(node).concat([node]);
  4928     }
  4930     function nextNodeDescendants(node) {
  4931         while (node && !node.nextSibling) {
  4932             node = node.parentNode;
  4933         }
  4934         if (!node) {
  4935             return null;
  4936         }
  4937         return node.nextSibling;
  4938     }
  4940     function nextNode(node, excludeChildren) {
  4941         if (!excludeChildren && node.hasChildNodes()) {
  4942             return node.firstChild;
  4943         }
  4944         return nextNodeDescendants(node);
  4945     }
  4947     function previousNode(node) {
  4948         var previous = node.previousSibling;
  4949         if (previous) {
  4950             node = previous;
  4951             while (node.hasChildNodes()) {
  4952                 node = node.lastChild;
  4953             }
  4954             return node;
  4955         }
  4956         var parent = node.parentNode;
  4957         if (parent && parent.nodeType == 1) {
  4958             return parent;
  4959         }
  4960         return null;
  4961     }
  4963     // Adpated from Aryeh's code.
  4964     // "A whitespace node is either a Text node whose data is the empty string; or
  4965     // a Text node whose data consists only of one or more tabs (0x0009), line
  4966     // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
  4967     // parent is an Element whose resolved value for "white-space" is "normal" or
  4968     // "nowrap"; or a Text node whose data consists only of one or more tabs
  4969     // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
  4970     // parent is an Element whose resolved value for "white-space" is "pre-line"."
  4971     function isWhitespaceNode(node) {
  4972         if (!node || node.nodeType != 3) {
  4973             return false;
  4974         }
  4975         var text = node.data;
  4976         if (text === "") {
  4977             return true;
  4978         }
  4979         var parent = node.parentNode;
  4980         if (!parent || parent.nodeType != 1) {
  4981             return false;
  4982         }
  4983         var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
  4985         return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
  4986             (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
  4987     }
  4989     // Adpated from Aryeh's code.
  4990     // "node is a collapsed whitespace node if the following algorithm returns
  4991     // true:"
  4992     function isCollapsedWhitespaceNode(node) {
  4993         // "If node's data is the empty string, return true."
  4994         if (node.data === "") {
  4995             return true;
  4996         }
  4998         // "If node is not a whitespace node, return false."
  4999         if (!isWhitespaceNode(node)) {
  5000             return false;
  5001         }
  5003         // "Let ancestor be node's parent."
  5004         var ancestor = node.parentNode;
  5006         // "If ancestor is null, return true."
  5007         if (!ancestor) {
  5008             return true;
  5009         }
  5011         // "If the "display" property of some ancestor of node has resolved value "none", return true."
  5012         if (isHidden(node)) {
  5013             return true;
  5014         }
  5016         return false;
  5017     }
  5019     function isCollapsedNode(node) {
  5020         var type = node.nodeType;
  5021         return type == 7 /* PROCESSING_INSTRUCTION */ ||
  5022             type == 8 /* COMMENT */ ||
  5023             isHidden(node) ||
  5024             /^(script|style)$/i.test(node.nodeName) ||
  5025             isVisibilityHiddenTextNode(node) ||
  5026             isCollapsedWhitespaceNode(node);
  5027     }
  5029     function isIgnoredNode(node, win) {
  5030         var type = node.nodeType;
  5031         return type == 7 /* PROCESSING_INSTRUCTION */ ||
  5032             type == 8 /* COMMENT */ ||
  5033             (type == 1 && getComputedDisplay(node, win) == "none");
  5034     }
  5036     /*----------------------------------------------------------------------------------------------------------------*/
  5038     // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
  5040     function Cache() {
  5041         this.store = {};
  5042     }
  5044     Cache.prototype = {
  5045         get: function(key) {
  5046             return this.store.hasOwnProperty(key) ? this.store[key] : null;
  5047         },
  5049         set: function(key, value) {
  5050             return this.store[key] = value;
  5051         }
  5052     };
  5054     var cachedCount = 0, uncachedCount = 0;
  5056     function createCachingGetter(methodName, func, objProperty) {
  5057         return function(args) {
  5058             var cache = this.cache;
  5059             if (cache.hasOwnProperty(methodName)) {
  5060                 cachedCount++;
  5061                 return cache[methodName];
  5062             } else {
  5063                 uncachedCount++;
  5064                 var value = func.call(this, objProperty ? this[objProperty] : this, args);
  5065                 cache[methodName] = value;
  5066                 return value;
  5067             }
  5068         };
  5069     }
  5071     /*----------------------------------------------------------------------------------------------------------------*/
  5073     function NodeWrapper(node, session) {
  5074         this.node = node;
  5075         this.session = session;
  5076         this.cache = new Cache();
  5077         this.positions = new Cache();
  5078     }
  5080     var nodeProto = {
  5081         getPosition: function(offset) {
  5082             var positions = this.positions;
  5083             return positions.get(offset) || positions.set(offset, new Position(this, offset));
  5084         },
  5086         toString: function() {
  5087             return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
  5088         }
  5089     };
  5091     NodeWrapper.prototype = nodeProto;
  5093     var EMPTY = "EMPTY",
  5094         NON_SPACE = "NON_SPACE",
  5095         UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
  5096         COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
  5097         TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
  5098         TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
  5099         TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
  5100         PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
  5101         TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
  5102         INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
  5104     extend(nodeProto, {
  5105         isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
  5106         getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
  5107         getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
  5108         containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
  5109         isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
  5110         isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
  5111         getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
  5112         isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
  5113         isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
  5114         next: createCachingGetter("nextPos", nextNode, "node"),
  5115         previous: createCachingGetter("previous", previousNode, "node"),
  5117         getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
  5118             var spaceRegex = null, collapseSpaces = false;
  5119             var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
  5120             var preLine = (cssWhitespace == "pre-line");
  5121             if (preLine) {
  5122                 spaceRegex = spacesMinusLineBreaksRegex;
  5123                 collapseSpaces = true;
  5124             } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
  5125                 spaceRegex = spacesRegex;
  5126                 collapseSpaces = true;
  5127             }
  5129             return {
  5130                 node: textNode,
  5131                 text: textNode.data,
  5132                 spaceRegex: spaceRegex,
  5133                 collapseSpaces: collapseSpaces,
  5134                 preLine: preLine
  5135             };
  5136         }, "node"),
  5138         hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
  5139             var session = this.session;
  5140             var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
  5141             var firstPosInEl = session.getPosition(el, 0);
  5143             var pos = backward ? posAfterEl : firstPosInEl;
  5144             var endPos = backward ? firstPosInEl : posAfterEl;
  5146             /*
  5147              <body><p>X  </p><p>Y</p></body>
  5149              Positions:
  5151              body:0:""
  5152              p:0:""
  5153              text:0:""
  5154              text:1:"X"
  5155              text:2:TRAILING_SPACE_IN_BLOCK
  5156              text:3:COLLAPSED_SPACE
  5157              p:1:""
  5158              body:1:"\n"
  5159              p:0:""
  5160              text:0:""
  5161              text:1:"Y"
  5163              A character is a TRAILING_SPACE_IN_BLOCK iff:
  5165              - There is no uncollapsed character after it within the visible containing block element
  5167              A character is a TRAILING_SPACE_BEFORE_BR iff:
  5169              - There is no uncollapsed character after it preceding a <br> element
  5171              An element has inner text iff
  5173              - It is not hidden
  5174              - It contains an uncollapsed character
  5176              All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
  5177              */
  5179             while (pos !== endPos) {
  5180                 pos.prepopulateChar();
  5181                 if (pos.isDefinitelyNonEmpty()) {
  5182                     return true;
  5183                 }
  5184                 pos = backward ? pos.previousVisible() : pos.nextVisible();
  5185             }
  5187             return false;
  5188         }, "node"),
  5190         isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
  5191             // Ensure that a block element containing a <br> is considered to have inner text
  5192             var brs = el.getElementsByTagName("br");
  5193             for (var i = 0, len = brs.length; i < len; ++i) {
  5194                 if (!isCollapsedNode(brs[i])) {
  5195                     return true;
  5196                 }
  5197             }
  5198             return this.hasInnerText();
  5199         }, "node"),
  5201         getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
  5202             if (el.tagName.toLowerCase() == "br") {
  5203                 return "";
  5204             } else {
  5205                 switch (this.getComputedDisplay()) {
  5206                     case "inline":
  5207                         var child = el.lastChild;
  5208                         while (child) {
  5209                             if (!isIgnoredNode(child)) {
  5210                                 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
  5211                             }
  5212                             child = child.previousSibling;
  5213                         }
  5214                         break;
  5215                     case "inline-block":
  5216                     case "inline-table":
  5217                     case "none":
  5218                     case "table-column":
  5219                     case "table-column-group":
  5220                         break;
  5221                     case "table-cell":
  5222                         return "\t";
  5223                     default:
  5224                         return this.isRenderedBlock(true) ? "\n" : "";
  5225                 }
  5226             }
  5227             return "";
  5228         }, "node"),
  5230         getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
  5231             switch (this.getComputedDisplay()) {
  5232                 case "inline":
  5233                 case "inline-block":
  5234                 case "inline-table":
  5235                 case "none":
  5236                 case "table-column":
  5237                 case "table-column-group":
  5238                 case "table-cell":
  5239                     break;
  5240                 default:
  5241                     return this.isRenderedBlock(false) ? "\n" : "";
  5242             }
  5243             return "";
  5244         }, "node")
  5245     });
  5247     /*----------------------------------------------------------------------------------------------------------------*/
  5249     function Position(nodeWrapper, offset) {
  5250         this.offset = offset;
  5251         this.nodeWrapper = nodeWrapper;
  5252         this.node = nodeWrapper.node;
  5253         this.session = nodeWrapper.session;
  5254         this.cache = new Cache();
  5255     }
  5257     function inspectPosition() {
  5258         return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
  5259     }
  5261     var positionProto = {
  5262         character: "",
  5263         characterType: EMPTY,
  5264         isBr: false,
  5266         /*
  5267         This method:
  5268         - Fully populates positions that have characters that can be determined independently of any other characters.
  5269         - Populates most types of space positions with a provisional character. The character is finalized later.
  5270          */
  5271         prepopulateChar: function() {
  5272             var pos = this;
  5273             if (!pos.prepopulatedChar) {
  5274                 var node = pos.node, offset = pos.offset;
  5275                 var visibleChar = "", charType = EMPTY;
  5276                 var finalizedChar = false;
  5277                 if (offset > 0) {
  5278                     if (node.nodeType == 3) {
  5279                         var text = node.data;
  5280                         var textChar = text.charAt(offset - 1);
  5282                         var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
  5283                         var spaceRegex = nodeInfo.spaceRegex;
  5284                         if (nodeInfo.collapseSpaces) {
  5285                             if (spaceRegex.test(textChar)) {
  5286                                 // "If the character at position is from set, append a single space (U+0020) to newdata and advance
  5287                                 // position until the character at position is not from set."
  5289                                 // We also need to check for the case where we're in a pre-line and we have a space preceding a
  5290                                 // line break, because such spaces are collapsed in some browsers
  5291                                 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
  5292                                 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
  5293                                     visibleChar = " ";
  5294                                     charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
  5295                                 } else {
  5296                                     visibleChar = " ";
  5297                                     //pos.checkForFollowingLineBreak = true;
  5298                                     charType = COLLAPSIBLE_SPACE;
  5299                                 }
  5300                             } else {
  5301                                 visibleChar = textChar;
  5302                                 charType = NON_SPACE;
  5303                                 finalizedChar = true;
  5304                             }
  5305                         } else {
  5306                             visibleChar = textChar;
  5307                             charType = UNCOLLAPSIBLE_SPACE;
  5308                             finalizedChar = true;
  5309                         }
  5310                     } else {
  5311                         var nodePassed = node.childNodes[offset - 1];
  5312                         if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
  5313                             if (nodePassed.tagName.toLowerCase() == "br") {
  5314                                 visibleChar = "\n";
  5315                                 pos.isBr = true;
  5316                                 charType = COLLAPSIBLE_SPACE;
  5317                                 finalizedChar = false;
  5318                             } else {
  5319                                 pos.checkForTrailingSpace = true;
  5320                             }
  5321                         }
  5323                         // Check the leading space of the next node for the case when a block element follows an inline
  5324                         // element or text node. In that case, there is an implied line break between the two nodes.
  5325                         if (!visibleChar) {
  5326                             var nextNode = node.childNodes[offset];
  5327                             if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
  5328                                 pos.checkForLeadingSpace = true;
  5329                             }
  5330                         }
  5331                     }
  5332                 }
  5334                 pos.prepopulatedChar = true;
  5335                 pos.character = visibleChar;
  5336                 pos.characterType = charType;
  5337                 pos.isCharInvariant = finalizedChar;
  5338             }
  5339         },
  5341         isDefinitelyNonEmpty: function() {
  5342             var charType = this.characterType;
  5343             return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
  5344         },
  5346         // Resolve leading and trailing spaces, which may involve prepopulating other positions
  5347         resolveLeadingAndTrailingSpaces: function() {
  5348             if (!this.prepopulatedChar) {
  5349                 this.prepopulateChar();
  5350             }
  5351             if (this.checkForTrailingSpace) {
  5352                 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
  5353                 if (trailingSpace) {
  5354                     this.isTrailingSpace = true;
  5355                     this.character = trailingSpace;
  5356                     this.characterType = COLLAPSIBLE_SPACE;
  5357                 }
  5358                 this.checkForTrailingSpace = false;
  5359             }
  5360             if (this.checkForLeadingSpace) {
  5361                 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
  5362                 if (leadingSpace) {
  5363                     this.isLeadingSpace = true;
  5364                     this.character = leadingSpace;
  5365                     this.characterType = COLLAPSIBLE_SPACE;
  5366                 }
  5367                 this.checkForLeadingSpace = false;
  5368             }
  5369         },
  5371         getPrecedingUncollapsedPosition: function(characterOptions) {
  5372             var pos = this, character;
  5373             while ( (pos = pos.previousVisible()) ) {
  5374                 character = pos.getCharacter(characterOptions);
  5375                 if (character !== "") {
  5376                     return pos;
  5377                 }
  5378             }
  5380             return null;
  5381         },
  5383         getCharacter: function(characterOptions) {
  5384             this.resolveLeadingAndTrailingSpaces();
  5386             var thisChar = this.character, returnChar;
  5388             // Check if character is ignored
  5389             var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
  5390             var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
  5392             // Check if this position's  character is invariant (i.e. not dependent on character options) and return it
  5393             // if so
  5394             if (this.isCharInvariant) {
  5395                 returnChar = isIgnoredCharacter ? "" : thisChar;
  5396                 return returnChar;
  5397             }
  5399             var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
  5400             var cachedChar = this.cache.get(cacheKey);
  5401             if (cachedChar !== null) {
  5402                 return cachedChar;
  5403             }
  5405             // We need to actually get the character now
  5406             var character = "";
  5407             var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
  5409             var nextPos, previousPos;
  5410             var gotPreviousPos = false;
  5411             var pos = this;
  5413             function getPreviousPos() {
  5414                 if (!gotPreviousPos) {
  5415                     previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
  5416                     gotPreviousPos = true;
  5417                 }
  5418                 return previousPos;
  5419             }
  5421             // Disallow a collapsible space that is followed by a line break or is the last character
  5422             if (collapsible) {
  5423                 // Allow a trailing space that we've previously determined should be included
  5424                 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
  5425                     character = "\n";
  5426                 }
  5427                 // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
  5428                 // or follows a collapsible included space
  5429                 else if (thisChar == " " &&
  5430                         (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
  5431                 }
  5432                 // Allow a leading line break unless it follows a line break
  5433                 else if (thisChar == "\n" && this.isLeadingSpace) {
  5434                     if (getPreviousPos() && previousPos.character != "\n") {
  5435                         character = "\n";
  5436                     } else {
  5437                     }
  5438                 } else {
  5439                     nextPos = this.nextUncollapsed();
  5440                     if (nextPos) {
  5441                         if (nextPos.isBr) {
  5442                             this.type = TRAILING_SPACE_BEFORE_BR;
  5443                         } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
  5444                             this.type = TRAILING_SPACE_IN_BLOCK;
  5445                         } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
  5446                             this.type = TRAILING_SPACE_BEFORE_BLOCK;
  5447                         }
  5449                         if (nextPos.character == "\n") {
  5450                             if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
  5451                             } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
  5452                             } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
  5453                             } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
  5454                             } else if (thisChar == "\n") {
  5455                                 if (nextPos.isTrailingSpace) {
  5456                                     if (this.isTrailingSpace) {
  5457                                     } else if (this.isBr) {
  5458                                         nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
  5460                                         if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
  5461                                             nextPos.character = "";
  5462                                         } else {
  5463                                             nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
  5464                                         }
  5465                                     }
  5466                                 } else {
  5467                                     character = "\n";
  5468                                 }
  5469                             } else if (thisChar == " ") {
  5470                                 character = " ";
  5471                             } else {
  5472                             }
  5473                         } else {
  5474                             character = thisChar;
  5475                         }
  5476                     } else {
  5477                     }
  5478                 }
  5479             }
  5481             if (ignoredChars.indexOf(character) > -1) {
  5482                 character = "";
  5483             }
  5486             this.cache.set(cacheKey, character);
  5488             return character;
  5489         },
  5491         equals: function(pos) {
  5492             return !!pos && this.node === pos.node && this.offset === pos.offset;
  5493         },
  5495         inspect: inspectPosition,
  5497         toString: function() {
  5498             return this.character;
  5499         }
  5500     };
  5502     Position.prototype = positionProto;
  5504     extend(positionProto, {
  5505         next: createCachingGetter("nextPos", function(pos) {
  5506             var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
  5507             if (!node) {
  5508                 return null;
  5509             }
  5510             var nextNode, nextOffset, child;
  5511             if (offset == nodeWrapper.getLength()) {
  5512                 // Move onto the next node
  5513                 nextNode = node.parentNode;
  5514                 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
  5515             } else {
  5516                 if (nodeWrapper.isCharacterDataNode()) {
  5517                     nextNode = node;
  5518                     nextOffset = offset + 1;
  5519                 } else {
  5520                     child = node.childNodes[offset];
  5521                     // Go into the children next, if children there are
  5522                     if (session.getNodeWrapper(child).containsPositions()) {
  5523                         nextNode = child;
  5524                         nextOffset = 0;
  5525                     } else {
  5526                         nextNode = node;
  5527                         nextOffset = offset + 1;
  5528                     }
  5529                 }
  5530             }
  5532             return nextNode ? session.getPosition(nextNode, nextOffset) : null;
  5533         }),
  5535         previous: createCachingGetter("previous", function(pos) {
  5536             var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
  5537             var previousNode, previousOffset, child;
  5538             if (offset == 0) {
  5539                 previousNode = node.parentNode;
  5540                 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
  5541             } else {
  5542                 if (nodeWrapper.isCharacterDataNode()) {
  5543                     previousNode = node;
  5544                     previousOffset = offset - 1;
  5545                 } else {
  5546                     child = node.childNodes[offset - 1];
  5547                     // Go into the children next, if children there are
  5548                     if (session.getNodeWrapper(child).containsPositions()) {
  5549                         previousNode = child;
  5550                         previousOffset = dom.getNodeLength(child);
  5551                     } else {
  5552                         previousNode = node;
  5553                         previousOffset = offset - 1;
  5554                     }
  5555                 }
  5556             }
  5557             return previousNode ? session.getPosition(previousNode, previousOffset) : null;
  5558         }),
  5560         /*
  5561          Next and previous position moving functions that filter out
  5563          - Hidden (CSS visibility/display) elements
  5564          - Script and style elements
  5565          */
  5566         nextVisible: createCachingGetter("nextVisible", function(pos) {
  5567             var next = pos.next();
  5568             if (!next) {
  5569                 return null;
  5570             }
  5571             var nodeWrapper = next.nodeWrapper, node = next.node;
  5572             var newPos = next;
  5573             if (nodeWrapper.isCollapsed()) {
  5574                 // We're skipping this node and all its descendants
  5575                 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
  5576             }
  5577             return newPos;
  5578         }),
  5580         nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
  5581             var nextPos = pos;
  5582             while ( (nextPos = nextPos.nextVisible()) ) {
  5583                 nextPos.resolveLeadingAndTrailingSpaces();
  5584                 if (nextPos.character !== "") {
  5585                     return nextPos;
  5586                 }
  5587             }
  5588             return null;
  5589         }),
  5591         previousVisible: createCachingGetter("previousVisible", function(pos) {
  5592             var previous = pos.previous();
  5593             if (!previous) {
  5594                 return null;
  5595             }
  5596             var nodeWrapper = previous.nodeWrapper, node = previous.node;
  5597             var newPos = previous;
  5598             if (nodeWrapper.isCollapsed()) {
  5599                 // We're skipping this node and all its descendants
  5600                 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
  5601             }
  5602             return newPos;
  5603         })
  5604     });
  5606     /*----------------------------------------------------------------------------------------------------------------*/
  5608     var currentSession = null;
  5610     var Session = (function() {
  5611         function createWrapperCache(nodeProperty) {
  5612             var cache = new Cache();
  5614             return {
  5615                 get: function(node) {
  5616                     var wrappersByProperty = cache.get(node[nodeProperty]);
  5617                     if (wrappersByProperty) {
  5618                         for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
  5619                             if (wrapper.node === node) {
  5620                                 return wrapper;
  5621                             }
  5622                         }
  5623                     }
  5624                     return null;
  5625                 },
  5627                 set: function(nodeWrapper) {
  5628                     var property = nodeWrapper.node[nodeProperty];
  5629                     var wrappersByProperty = cache.get(property) || cache.set(property, []);
  5630                     wrappersByProperty.push(nodeWrapper);
  5631                 }
  5632             };
  5633         }
  5635         var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
  5637         function Session() {
  5638             this.initCaches();
  5639         }
  5641         Session.prototype = {
  5642             initCaches: function() {
  5643                 this.elementCache = uniqueIDSupported ? (function() {
  5644                     var elementsCache = new Cache();
  5646                     return {
  5647                         get: function(el) {
  5648                             return elementsCache.get(el.uniqueID);
  5649                         },
  5651                         set: function(elWrapper) {
  5652                             elementsCache.set(elWrapper.node.uniqueID, elWrapper);
  5653                         }
  5654                     };
  5655                 })() : createWrapperCache("tagName");
  5657                 // Store text nodes keyed by data, although we may need to truncate this
  5658                 this.textNodeCache = createWrapperCache("data");
  5659                 this.otherNodeCache = createWrapperCache("nodeName");
  5660             },
  5662             getNodeWrapper: function(node) {
  5663                 var wrapperCache;
  5664                 switch (node.nodeType) {
  5665                     case 1:
  5666                         wrapperCache = this.elementCache;
  5667                         break;
  5668                     case 3:
  5669                         wrapperCache = this.textNodeCache;
  5670                         break;
  5671                     default:
  5672                         wrapperCache = this.otherNodeCache;
  5673                         break;
  5674                 }
  5676                 var wrapper = wrapperCache.get(node);
  5677                 if (!wrapper) {
  5678                     wrapper = new NodeWrapper(node, this);
  5679                     wrapperCache.set(wrapper);
  5680                 }
  5681                 return wrapper;
  5682             },
  5684             getPosition: function(node, offset) {
  5685                 return this.getNodeWrapper(node).getPosition(offset);
  5686             },
  5688             getRangeBoundaryPosition: function(range, isStart) {
  5689                 var prefix = isStart ? "start" : "end";
  5690                 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
  5691             },
  5693             detach: function() {
  5694                 this.elementCache = this.textNodeCache = this.otherNodeCache = null;
  5695             }
  5696         };
  5698         return Session;
  5699     })();
  5701     /*----------------------------------------------------------------------------------------------------------------*/
  5703     function startSession() {
  5704         endSession();
  5705         return (currentSession = new Session());
  5706     }
  5708     function getSession() {
  5709         return currentSession || startSession();
  5710     }
  5712     function endSession() {
  5713         if (currentSession) {
  5714             currentSession.detach();
  5715         }
  5716         currentSession = null;
  5717     }
  5719     /*----------------------------------------------------------------------------------------------------------------*/
  5721     // Extensions to the rangy.dom utility object
  5723     extend(dom, {
  5724         nextNode: nextNode,
  5725         previousNode: previousNode
  5726     });
  5728     /*----------------------------------------------------------------------------------------------------------------*/
  5730     function createCharacterIterator(startPos, backward, endPos, characterOptions) {
  5732         // Adjust the end position to ensure that it is actually reached
  5733         if (endPos) {
  5734             if (backward) {
  5735                 if (isCollapsedNode(endPos.node)) {
  5736                     endPos = startPos.previousVisible();
  5737                 }
  5738             } else {
  5739                 if (isCollapsedNode(endPos.node)) {
  5740                     endPos = endPos.nextVisible();
  5741                 }
  5742             }
  5743         }
  5745         var pos = startPos, finished = false;
  5747         function next() {
  5748             var charPos = null;
  5749             if (backward) {
  5750                 charPos = pos;
  5751                 if (!finished) {
  5752                     pos = pos.previousVisible();
  5753                     finished = !pos || (endPos && pos.equals(endPos));
  5754                 }
  5755             } else {
  5756                 if (!finished) {
  5757                     charPos = pos = pos.nextVisible();
  5758                     finished = !pos || (endPos && pos.equals(endPos));
  5759                 }
  5760             }
  5761             if (finished) {
  5762                 pos = null;
  5763             }
  5764             return charPos;
  5765         }
  5767         var previousTextPos, returnPreviousTextPos = false;
  5769         return {
  5770             next: function() {
  5771                 if (returnPreviousTextPos) {
  5772                     returnPreviousTextPos = false;
  5773                     return previousTextPos;
  5774                 } else {
  5775                     var pos, character;
  5776                     while ( (pos = next()) ) {
  5777                         character = pos.getCharacter(characterOptions);
  5778                         if (character) {
  5779                             previousTextPos = pos;
  5780                             return pos;
  5781                         }
  5782                     }
  5783                     return null;
  5784                 }
  5785             },
  5787             rewind: function() {
  5788                 if (previousTextPos) {
  5789                     returnPreviousTextPos = true;
  5790                 } else {
  5791                     throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
  5792                 }
  5793             },
  5795             dispose: function() {
  5796                 startPos = endPos = null;
  5797             }
  5798         };
  5799     }
  5801     var arrayIndexOf = Array.prototype.indexOf ?
  5802         function(arr, val) {
  5803             return arr.indexOf(val);
  5804         } :
  5805         function(arr, val) {
  5806             for (var i = 0, len = arr.length; i < len; ++i) {
  5807                 if (arr[i] === val) {
  5808                     return i;
  5809                 }
  5810             }
  5811             return -1;
  5812         };
  5814     // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
  5815     // is called and there is no more tokenized text
  5816     function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
  5817         var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
  5818         var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
  5819         var tokenizer = wordOptions.tokenizer;
  5821         // Consumes a word and the whitespace beyond it
  5822         function consumeWord(forward) {
  5823             var pos, textChar;
  5824             var newChars = [], it = forward ? forwardIterator : backwardIterator;
  5826             var passedWordBoundary = false, insideWord = false;
  5828             while ( (pos = it.next()) ) {
  5829                 textChar = pos.character;
  5832                 if (allWhiteSpaceRegex.test(textChar)) {
  5833                     if (insideWord) {
  5834                         insideWord = false;
  5835                         passedWordBoundary = true;
  5836                     }
  5837                 } else {
  5838                     if (passedWordBoundary) {
  5839                         it.rewind();
  5840                         break;
  5841                     } else {
  5842                         insideWord = true;
  5843                     }
  5844                 }
  5845                 newChars.push(pos);
  5846             }
  5849             return newChars;
  5850         }
  5852         // Get initial word surrounding initial position and tokenize it
  5853         var forwardChars = consumeWord(true);
  5854         var backwardChars = consumeWord(false).reverse();
  5855         var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
  5857         // Create initial token buffers
  5858         var forwardTokensBuffer = forwardChars.length ?
  5859             tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
  5861         var backwardTokensBuffer = backwardChars.length ?
  5862             tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
  5864         function inspectBuffer(buffer) {
  5865             var textPositions = ["[" + buffer.length + "]"];
  5866             for (var i = 0; i < buffer.length; ++i) {
  5867                 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
  5868             }
  5869             return textPositions;
  5870         }
  5873         return {
  5874             nextEndToken: function() {
  5875                 var lastToken, forwardChars;
  5877                 // If we're down to the last token, consume character chunks until we have a word or run out of
  5878                 // characters to consume
  5879                 while ( forwardTokensBuffer.length == 1 &&
  5880                     !(lastToken = forwardTokensBuffer[0]).isWord &&
  5881                     (forwardChars = consumeWord(true)).length > 0) {
  5883                     // Merge trailing non-word into next word and tokenize
  5884                     forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
  5885                 }
  5887                 return forwardTokensBuffer.shift();
  5888             },
  5890             previousStartToken: function() {
  5891                 var lastToken, backwardChars;
  5893                 // If we're down to the last token, consume character chunks until we have a word or run out of
  5894                 // characters to consume
  5895                 while ( backwardTokensBuffer.length == 1 &&
  5896                     !(lastToken = backwardTokensBuffer[0]).isWord &&
  5897                     (backwardChars = consumeWord(false)).length > 0) {
  5899                     // Merge leading non-word into next word and tokenize
  5900                     backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
  5901                 }
  5903                 return backwardTokensBuffer.pop();
  5904             },
  5906             dispose: function() {
  5907                 forwardIterator.dispose();
  5908                 backwardIterator.dispose();
  5909                 forwardTokensBuffer = backwardTokensBuffer = null;
  5910             }
  5911         };
  5912     }
  5914     function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
  5915         var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
  5916         if (count !== 0) {
  5917             var backward = (count < 0);
  5919             switch (unit) {
  5920                 case CHARACTER:
  5921                     charIterator = createCharacterIterator(pos, backward, null, characterOptions);
  5922                     while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
  5923                         ++unitsMoved;
  5924                         newPos = currentPos;
  5925                     }
  5926                     nextPos = currentPos;
  5927                     charIterator.dispose();
  5928                     break;
  5929                 case WORD:
  5930                     var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
  5931                     var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
  5933                     while ( (token = next()) && unitsMoved < absCount ) {
  5934                         if (token.isWord) {
  5935                             ++unitsMoved;
  5936                             newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
  5937                         }
  5938                     }
  5939                     break;
  5940                 default:
  5941                     throw new Error("movePositionBy: unit '" + unit + "' not implemented");
  5942             }
  5944             // Perform any necessary position tweaks
  5945             if (backward) {
  5946                 newPos = newPos.previousVisible();
  5947                 unitsMoved = -unitsMoved;
  5948             } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
  5949                 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
  5950                 // before a block element (for example, the line break between "1" and "2" in the following HTML:
  5951                 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
  5952                 // corresponds with a different selection position in most browsers from the one we want (i.e. at the
  5953                 // start of the contents of the block element). We get round this by advancing the position returned to
  5954                 // the last possible equivalent visible position.
  5955                 if (unit == WORD) {
  5956                     charIterator = createCharacterIterator(pos, false, null, characterOptions);
  5957                     nextPos = charIterator.next();
  5958                     charIterator.dispose();
  5959                 }
  5960                 if (nextPos) {
  5961                     newPos = nextPos.previousVisible();
  5962                 }
  5963             }
  5964         }
  5967         return {
  5968             position: newPos,
  5969             unitsMoved: unitsMoved
  5970         };
  5971     }
  5973     function createRangeCharacterIterator(session, range, characterOptions, backward) {
  5974         var rangeStart = session.getRangeBoundaryPosition(range, true);
  5975         var rangeEnd = session.getRangeBoundaryPosition(range, false);
  5976         var itStart = backward ? rangeEnd : rangeStart;
  5977         var itEnd = backward ? rangeStart : rangeEnd;
  5979         return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
  5980     }
  5982     function getRangeCharacters(session, range, characterOptions) {
  5984         var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
  5985         while ( (pos = it.next()) ) {
  5986             chars.push(pos);
  5987         }
  5989         it.dispose();
  5990         return chars;
  5991     }
  5993     function isWholeWord(startPos, endPos, wordOptions) {
  5994         var range = api.createRange(startPos.node);
  5995         range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
  5996         return !range.expand("word", { wordOptions: wordOptions });
  5997     }
  5999     function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
  6000         var backward = isDirectionBackward(findOptions.direction);
  6001         var it = createCharacterIterator(
  6002             initialPos,
  6003             backward,
  6004             initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
  6005             findOptions.characterOptions
  6006         );
  6007         var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
  6008         var result, insideRegexMatch;
  6009         var returnValue = null;
  6011         function handleMatch(startIndex, endIndex) {
  6012             var startPos = chars[startIndex].previousVisible();
  6013             var endPos = chars[endIndex - 1];
  6014             var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
  6016             return {
  6017                 startPos: startPos,
  6018                 endPos: endPos,
  6019                 valid: valid
  6020             };
  6021         }
  6023         while ( (pos = it.next()) ) {
  6024             currentChar = pos.character;
  6025             if (!isRegex && !findOptions.caseSensitive) {
  6026                 currentChar = currentChar.toLowerCase();
  6027             }
  6029             if (backward) {
  6030                 chars.unshift(pos);
  6031                 text = currentChar + text;
  6032             } else {
  6033                 chars.push(pos);
  6034                 text += currentChar;
  6035             }
  6037             if (isRegex) {
  6038                 result = searchTerm.exec(text);
  6039                 if (result) {
  6040                     matchStartIndex = result.index;
  6041                     matchEndIndex = matchStartIndex + result[0].length;
  6042                     if (insideRegexMatch) {
  6043                         // Check whether the match is now over
  6044                         if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
  6045                             returnValue = handleMatch(matchStartIndex, matchEndIndex);
  6046                             break;
  6047                         }
  6048                     } else {
  6049                         insideRegexMatch = true;
  6050                     }
  6051                 }
  6052             } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
  6053                 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
  6054                 break;
  6055             }
  6056         }
  6058         // Check whether regex match extends to the end of the range
  6059         if (insideRegexMatch) {
  6060             returnValue = handleMatch(matchStartIndex, matchEndIndex);
  6061         }
  6062         it.dispose();
  6064         return returnValue;
  6065     }
  6067     function createEntryPointFunction(func) {
  6068         return function() {
  6069             var sessionRunning = !!currentSession;
  6070             var session = getSession();
  6071             var args = [session].concat( util.toArray(arguments) );
  6072             var returnValue = func.apply(this, args);
  6073             if (!sessionRunning) {
  6074                 endSession();
  6075             }
  6076             return returnValue;
  6077         };
  6078     }
  6080     /*----------------------------------------------------------------------------------------------------------------*/
  6082     // Extensions to the Rangy Range object
  6084     function createRangeBoundaryMover(isStart, collapse) {
  6085         /*
  6086          Unit can be "character" or "word"
  6087          Options:
  6089          - includeTrailingSpace
  6090          - wordRegex
  6091          - tokenizer
  6092          - collapseSpaceBeforeLineBreak
  6093          */
  6094         return createEntryPointFunction(
  6095             function(session, unit, count, moveOptions) {
  6096                 if (typeof count == UNDEF) {
  6097                     count = unit;
  6098                     unit = CHARACTER;
  6099                 }
  6100                 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
  6102                 var boundaryIsStart = isStart;
  6103                 if (collapse) {
  6104                     boundaryIsStart = (count >= 0);
  6105                     this.collapse(!boundaryIsStart);
  6106                 }
  6107                 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
  6108                 var newPos = moveResult.position;
  6109                 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
  6110                 return moveResult.unitsMoved;
  6111             }
  6112         );
  6113     }
  6115     function createRangeTrimmer(isStart) {
  6116         return createEntryPointFunction(
  6117             function(session, characterOptions) {
  6118                 characterOptions = createOptions(characterOptions, defaultCharacterOptions);
  6119                 var pos;
  6120                 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
  6121                 var trimCharCount = 0;
  6122                 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
  6123                     ++trimCharCount;
  6124                 }
  6125                 it.dispose();
  6126                 var trimmed = (trimCharCount > 0);
  6127                 if (trimmed) {
  6128                     this[isStart ? "moveStart" : "moveEnd"](
  6129                         "character",
  6130                         isStart ? trimCharCount : -trimCharCount,
  6131                         { characterOptions: characterOptions }
  6132                     );
  6133                 }
  6134                 return trimmed;
  6135             }
  6136         );
  6137     }
  6139     extend(api.rangePrototype, {
  6140         moveStart: createRangeBoundaryMover(true, false),
  6142         moveEnd: createRangeBoundaryMover(false, false),
  6144         move: createRangeBoundaryMover(true, true),
  6146         trimStart: createRangeTrimmer(true),
  6148         trimEnd: createRangeTrimmer(false),
  6150         trim: createEntryPointFunction(
  6151             function(session, characterOptions) {
  6152                 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
  6153                 return startTrimmed || endTrimmed;
  6154             }
  6155         ),
  6157         expand: createEntryPointFunction(
  6158             function(session, unit, expandOptions) {
  6159                 var moved = false;
  6160                 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
  6161                 var characterOptions = expandOptions.characterOptions;
  6162                 if (!unit) {
  6163                     unit = CHARACTER;
  6164                 }
  6165                 if (unit == WORD) {
  6166                     var wordOptions = expandOptions.wordOptions;
  6167                     var startPos = session.getRangeBoundaryPosition(this, true);
  6168                     var endPos = session.getRangeBoundaryPosition(this, false);
  6170                     var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
  6171                     var startToken = startTokenizedTextProvider.nextEndToken();
  6172                     var newStartPos = startToken.chars[0].previousVisible();
  6173                     var endToken, newEndPos;
  6175                     if (this.collapsed) {
  6176                         endToken = startToken;
  6177                     } else {
  6178                         var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
  6179                         endToken = endTokenizedTextProvider.previousStartToken();
  6180                     }
  6181                     newEndPos = endToken.chars[endToken.chars.length - 1];
  6183                     if (!newStartPos.equals(startPos)) {
  6184                         this.setStart(newStartPos.node, newStartPos.offset);
  6185                         moved = true;
  6186                     }
  6187                     if (newEndPos && !newEndPos.equals(endPos)) {
  6188                         this.setEnd(newEndPos.node, newEndPos.offset);
  6189                         moved = true;
  6190                     }
  6192                     if (expandOptions.trim) {
  6193                         if (expandOptions.trimStart) {
  6194                             moved = this.trimStart(characterOptions) || moved;
  6195                         }
  6196                         if (expandOptions.trimEnd) {
  6197                             moved = this.trimEnd(characterOptions) || moved;
  6198                         }
  6199                     }
  6201                     return moved;
  6202                 } else {
  6203                     return this.moveEnd(CHARACTER, 1, expandOptions);
  6204                 }
  6205             }
  6206         ),
  6208         text: createEntryPointFunction(
  6209             function(session, characterOptions) {
  6210                 return this.collapsed ?
  6211                     "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
  6212             }
  6213         ),
  6215         selectCharacters: createEntryPointFunction(
  6216             function(session, containerNode, startIndex, endIndex, characterOptions) {
  6217                 var moveOptions = { characterOptions: characterOptions };
  6218                 if (!containerNode) {
  6219                     containerNode = getBody( this.getDocument() );
  6220                 }
  6221                 this.selectNodeContents(containerNode);
  6222                 this.collapse(true);
  6223                 this.moveStart("character", startIndex, moveOptions);
  6224                 this.collapse(true);
  6225                 this.moveEnd("character", endIndex - startIndex, moveOptions);
  6226             }
  6227         ),
  6229         // Character indexes are relative to the start of node
  6230         toCharacterRange: createEntryPointFunction(
  6231             function(session, containerNode, characterOptions) {
  6232                 if (!containerNode) {
  6233                     containerNode = getBody( this.getDocument() );
  6234                 }
  6235                 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
  6236                 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
  6237                 var rangeBetween = this.cloneRange();
  6238                 var startIndex, endIndex;
  6239                 if (rangeStartsBeforeNode) {
  6240                     rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
  6241                     startIndex = -rangeBetween.text(characterOptions).length;
  6242                 } else {
  6243                     rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
  6244                     startIndex = rangeBetween.text(characterOptions).length;
  6245                 }
  6246                 endIndex = startIndex + this.text(characterOptions).length;
  6248                 return {
  6249                     start: startIndex,
  6250                     end: endIndex
  6251                 };
  6252             }
  6253         ),
  6255         findText: createEntryPointFunction(
  6256             function(session, searchTermParam, findOptions) {
  6257                 // Set up options
  6258                 findOptions = createNestedOptions(findOptions, defaultFindOptions);
  6260                 // Create word options if we're matching whole words only
  6261                 if (findOptions.wholeWordsOnly) {
  6262                     // We don't ever want trailing spaces for search results
  6263                     findOptions.wordOptions.includeTrailingSpace = false;
  6264                 }
  6266                 var backward = isDirectionBackward(findOptions.direction);
  6268                 // Create a range representing the search scope if none was provided
  6269                 var searchScopeRange = findOptions.withinRange;
  6270                 if (!searchScopeRange) {
  6271                     searchScopeRange = api.createRange();
  6272                     searchScopeRange.selectNodeContents(this.getDocument());
  6273                 }
  6275                 // Examine and prepare the search term
  6276                 var searchTerm = searchTermParam, isRegex = false;
  6277                 if (typeof searchTerm == "string") {
  6278                     if (!findOptions.caseSensitive) {
  6279                         searchTerm = searchTerm.toLowerCase();
  6280                     }
  6281                 } else {
  6282                     isRegex = true;
  6283                 }
  6285                 var initialPos = session.getRangeBoundaryPosition(this, !backward);
  6287                 // Adjust initial position if it lies outside the search scope
  6288                 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
  6290                 if (comparison === -1) {
  6291                     initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
  6292                 } else if (comparison === 1) {
  6293                     initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
  6294                 }
  6296                 var pos = initialPos;
  6297                 var wrappedAround = false;
  6299                 // Try to find a match and ignore invalid ones
  6300                 var findResult;
  6301                 while (true) {
  6302                     findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
  6304                     if (findResult) {
  6305                         if (findResult.valid) {
  6306                             this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
  6307                             return true;
  6308                         } else {
  6309                             // We've found a match that is not a whole word, so we carry on searching from the point immediately
  6310                             // after the match
  6311                             pos = backward ? findResult.startPos : findResult.endPos;
  6312                         }
  6313                     } else if (findOptions.wrap && !wrappedAround) {
  6314                         // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
  6315                         searchScopeRange = searchScopeRange.cloneRange();
  6316                         pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
  6317                         searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
  6318                         wrappedAround = true;
  6319                     } else {
  6320                         // Nothing found and we can't wrap around, so we're done
  6321                         return false;
  6322                     }
  6323                 }
  6324             }
  6325         ),
  6327         pasteHtml: function(html) {
  6328             this.deleteContents();
  6329             if (html) {
  6330                 var frag = this.createContextualFragment(html);
  6331                 var lastChild = frag.lastChild;
  6332                 this.insertNode(frag);
  6333                 this.collapseAfter(lastChild);
  6334             }
  6335         }
  6336     });
  6338     /*----------------------------------------------------------------------------------------------------------------*/
  6340     // Extensions to the Rangy Selection object
  6342     function createSelectionTrimmer(methodName) {
  6343         return createEntryPointFunction(
  6344             function(session, characterOptions) {
  6345                 var trimmed = false;
  6346                 this.changeEachRange(function(range) {
  6347                     trimmed = range[methodName](characterOptions) || trimmed;
  6348                 });
  6349                 return trimmed;
  6350             }
  6351         );
  6352     }
  6354     extend(api.selectionPrototype, {
  6355         expand: createEntryPointFunction(
  6356             function(session, unit, expandOptions) {
  6357                 this.changeEachRange(function(range) {
  6358                     range.expand(unit, expandOptions);
  6359                 });
  6360             }
  6361         ),
  6363         move: createEntryPointFunction(
  6364             function(session, unit, count, options) {
  6365                 var unitsMoved = 0;
  6366                 if (this.focusNode) {
  6367                     this.collapse(this.focusNode, this.focusOffset);
  6368                     var range = this.getRangeAt(0);
  6369                     if (!options) {
  6370                         options = {};
  6371                     }
  6372                     options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
  6373                     unitsMoved = range.move(unit, count, options);
  6374                     this.setSingleRange(range);
  6375                 }
  6376                 return unitsMoved;
  6377             }
  6378         ),
  6380         trimStart: createSelectionTrimmer("trimStart"),
  6381         trimEnd: createSelectionTrimmer("trimEnd"),
  6382         trim: createSelectionTrimmer("trim"),
  6384         selectCharacters: createEntryPointFunction(
  6385             function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
  6386                 var range = api.createRange(containerNode);
  6387                 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
  6388                 this.setSingleRange(range, direction);
  6389             }
  6390         ),
  6392         saveCharacterRanges: createEntryPointFunction(
  6393             function(session, containerNode, characterOptions) {
  6394                 var ranges = this.getAllRanges(), rangeCount = ranges.length;
  6395                 var rangeInfos = [];
  6397                 var backward = rangeCount == 1 && this.isBackward();
  6399                 for (var i = 0, len = ranges.length; i < len; ++i) {
  6400                     rangeInfos[i] = {
  6401                         characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
  6402                         backward: backward,
  6403                         characterOptions: characterOptions
  6404                     };
  6405                 }
  6407                 return rangeInfos;
  6408             }
  6409         ),
  6411         restoreCharacterRanges: createEntryPointFunction(
  6412             function(session, containerNode, saved) {
  6413                 this.removeAllRanges();
  6414                 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
  6415                     rangeInfo = saved[i];
  6416                     characterRange = rangeInfo.characterRange;
  6417                     range = api.createRange(containerNode);
  6418                     range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
  6419                     this.addRange(range, rangeInfo.backward);
  6420                 }
  6421             }
  6422         ),
  6424         text: createEntryPointFunction(
  6425             function(session, characterOptions) {
  6426                 var rangeTexts = [];
  6427                 for (var i = 0, len = this.rangeCount; i < len; ++i) {
  6428                     rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
  6429                 }
  6430                 return rangeTexts.join("");
  6431             }
  6432         )
  6433     });
  6435     /*----------------------------------------------------------------------------------------------------------------*/
  6437     // Extensions to the core rangy object
  6439     api.innerText = function(el, characterOptions) {
  6440         var range = api.createRange(el);
  6441         range.selectNodeContents(el);
  6442         var text = range.text(characterOptions);
  6443         return text;
  6444     };
  6446     api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
  6447         var session = getSession();
  6448         iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
  6449         var startPos = session.getPosition(startNode, startOffset);
  6450         var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
  6451         var backward = isDirectionBackward(iteratorOptions.direction);
  6453         return {
  6454             next: function() {
  6455                 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
  6456             },
  6458             dispose: function() {
  6459                 tokenizedTextProvider.dispose();
  6460                 this.next = function() {};
  6461             }
  6462         };
  6463     };
  6465     /*----------------------------------------------------------------------------------------------------------------*/
  6467     api.noMutation = function(func) {
  6468         var session = getSession();
  6469         func(session);
  6470         endSession();
  6471     };
  6473     api.noMutation.createEntryPointFunction = createEntryPointFunction;
  6475     api.textRange = {
  6476         isBlockNode: isBlockNode,
  6477         isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
  6479         createPosition: createEntryPointFunction(
  6480             function(session, node, offset) {
  6481                 return session.getPosition(node, offset);
  6482             }
  6483         )
  6484     };
  6485 });
  6487 /**
  6488  * Detect browser support for specific features
  6489  */
  6490 wysihtml.browser = (function() {
  6491   var userAgent   = navigator.userAgent,
  6492       testElement = document.createElement("div"),
  6493       // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
  6494       // We need to be extra careful about Microsoft as it shows increasing tendency of tainting its userAgent strings with false feathers
  6495       isGecko     = userAgent.indexOf("Gecko")        !== -1 && userAgent.indexOf("KHTML") === -1 && !isIE(),
  6496       isWebKit    = userAgent.indexOf("AppleWebKit/") !== -1 && !isIE(),
  6497       isChrome    = userAgent.indexOf("Chrome/")      !== -1 && !isIE(),
  6498       isOpera     = userAgent.indexOf("Opera/")       !== -1 && !isIE();
  6500   function iosVersion(userAgent) {
  6501     return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
  6502   }
  6504   function androidVersion(userAgent) {
  6505     return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
  6506   }
  6508   function isIE(version, equation) {
  6509     var rv = -1,
  6510         re;
  6512     if (navigator.appName == 'Microsoft Internet Explorer') {
  6513       re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
  6514     } else if (navigator.appName == 'Netscape') {
  6515       if (navigator.userAgent.indexOf("Trident") > -1) {
  6516         re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
  6517       } else if ((/Edge\/(\d+)./i).test(navigator.userAgent)) {
  6518         re = /Edge\/(\d+)./i;
  6519       }
  6520     }
  6522     if (re && re.exec(navigator.userAgent) != null) {
  6523       rv = parseFloat(RegExp.$1);
  6524     }
  6526     if (rv === -1) { return false; }
  6527     if (!version) { return true; }
  6528     if (!equation) { return version === rv; }
  6529     if (equation === "<") { return version < rv; }
  6530     if (equation === ">") { return version > rv; }
  6531     if (equation === "<=") { return version <= rv; }
  6532     if (equation === ">=") { return version >= rv; }
  6533   }
  6535   return {
  6536     // Static variable needed, publicly accessible, to be able override it in unit tests
  6537     USER_AGENT: userAgent,
  6539     /**
  6540      * Exclude browsers that are not capable of displaying and handling
  6541      * contentEditable as desired:
  6542      *    - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
  6543      *    - IE < 8 create invalid markup and crash randomly from time to time
  6544      *
  6545      * @return {Boolean}
  6546      */
  6547     supported: function() {
  6548       var userAgent                   = this.USER_AGENT.toLowerCase(),
  6549           // Essential for making html elements editable
  6550           hasContentEditableSupport   = "contentEditable" in testElement,
  6551           // Following methods are needed in order to interact with the contentEditable area
  6552           hasEditingApiSupport        = document.execCommand && document.queryCommandSupported && document.queryCommandState,
  6553           // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
  6554           hasQuerySelectorSupport     = document.querySelector && document.querySelectorAll,
  6555           // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
  6556           isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
  6557       return hasContentEditableSupport
  6558         && hasEditingApiSupport
  6559         && hasQuerySelectorSupport
  6560         && !isIncompatibleMobileBrowser;
  6561     },
  6563     isTouchDevice: function() {
  6564       return this.supportsEvent("touchmove");
  6565     },
  6567     isIos: function() {
  6568       return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
  6569     },
  6571     isAndroid: function() {
  6572       return this.USER_AGENT.indexOf("Android") !== -1;
  6573     },
  6575     /**
  6576      * Whether the browser supports sandboxed iframes
  6577      * Currently only IE 6+ offers such feature <iframe security="restricted">
  6578      *
  6579      * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
  6580      * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
  6581      *
  6582      * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
  6583      */
  6584     supportsSandboxedIframes: function() {
  6585       return isIE();
  6586     },
  6588     /**
  6589      * IE6+7 throw a mixed content warning when the src of an iframe
  6590      * is empty/unset or about:blank
  6591      * window.querySelector is implemented as of IE8
  6592      */
  6593     throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
  6594       return !("querySelector" in document);
  6595     },
  6597     /**
  6598      * Whether the caret is correctly displayed in contentEditable elements
  6599      * Firefox sometimes shows a huge caret in the beginning after focusing
  6600      */
  6601     displaysCaretInEmptyContentEditableCorrectly: function() {
  6602       return isIE(12, ">");
  6603     },
  6605     /**
  6606      * Opera and IE are the only browsers who offer the css value
  6607      * in the original unit, thx to the currentStyle object
  6608      * All other browsers provide the computed style in px via window.getComputedStyle
  6609      */
  6610     hasCurrentStyleProperty: function() {
  6611       return "currentStyle" in testElement;
  6612     },
  6614     /**
  6615      * Whether the browser inserts a <br> when pressing enter in a contentEditable element
  6616      */
  6617     insertsLineBreaksOnReturn: function() {
  6618       return isGecko;
  6619     },
  6621     supportsPlaceholderAttributeOn: function(element) {
  6622       return "placeholder" in element;
  6623     },
  6625     supportsEvent: function(eventName) {
  6626       return "on" + eventName in testElement || (function() {
  6627         testElement.setAttribute("on" + eventName, "return;");
  6628         return typeof(testElement["on" + eventName]) === "function";
  6629       })();
  6630     },
  6632     /**
  6633      * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
  6634      */
  6635     supportsEventsInIframeCorrectly: function() {
  6636       return !isOpera;
  6637     },
  6639     /**
  6640      * Everything below IE9 doesn't know how to treat HTML5 tags
  6641      *
  6642      * @param {Object} context The document object on which to check HTML5 support
  6643      *
  6644      * @example
  6645      *    wysihtml.browser.supportsHTML5Tags(document);
  6646      */
  6647     supportsHTML5Tags: function(context) {
  6648       var element = context.createElement("div"),
  6649           html5   = "<article>foo</article>";
  6650       element.innerHTML = html5;
  6651       return element.innerHTML.toLowerCase() === html5;
  6652     },
  6654     /**
  6655      * Checks whether a document supports a certain queryCommand
  6656      * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
  6657      * in oder to report correct results
  6658      *
  6659      * @param {Object} doc Document object on which to check for a query command
  6660      * @param {String} command The query command to check for
  6661      * @return {Boolean}
  6662      *
  6663      * @example
  6664      *    wysihtml.browser.supportsCommand(document, "bold");
  6665      */
  6666     supportsCommand: (function() {
  6667       // Following commands are supported but contain bugs in some browsers
  6668       // TODO: investigate if some of these bugs can be tested without altering selection on page, instead of targeting browsers and versions directly
  6669       var buggyCommands = {
  6670         // formatBlock fails with some tags (eg. <blockquote>)
  6671         "formatBlock":          isIE(10, "<="),
  6672          // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
  6673          // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
  6674          // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
  6675         "insertUnorderedList":  isIE(),
  6676         "insertOrderedList":    isIE()
  6677       };
  6679       // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
  6680       var supported = {
  6681         "insertHTML": isGecko
  6682       };
  6684       return function(doc, command) {
  6685         var isBuggy = buggyCommands[command];
  6686         if (!isBuggy) {
  6687           // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
  6688           try {
  6689             return doc.queryCommandSupported(command);
  6690           } catch(e1) {}
  6692           try {
  6693             return doc.queryCommandEnabled(command);
  6694           } catch(e2) {
  6695             return !!supported[command];
  6696           }
  6697         }
  6698         return false;
  6699       };
  6700     })(),
  6702     /**
  6703      * IE: URLs starting with:
  6704      *    www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
  6705      *    nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
  6706      * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
  6707      * space bar when the caret is directly after such an url.
  6708      * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
  6709      * (related blog post on msdn
  6710      * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
  6711      */
  6712     doesAutoLinkingInContentEditable: function() {
  6713       return isIE();
  6714     },
  6716     /**
  6717      * As stated above, IE auto links urls typed into contentEditable elements
  6718      * Since IE9 it's possible to prevent this behavior
  6719      */
  6720     canDisableAutoLinking: function() {
  6721       return this.supportsCommand(document, "AutoUrlDetect");
  6722     },
  6724     /**
  6725      * IE leaves an empty paragraph in the contentEditable element after clearing it
  6726      * Chrome/Safari sometimes an empty <div>
  6727      */
  6728     clearsContentEditableCorrectly: function() {
  6729       return isGecko || isOpera || isWebKit;
  6730     },
  6732     /**
  6733      * IE gives wrong results for getAttribute
  6734      */
  6735     supportsGetAttributeCorrectly: function() {
  6736       var td = document.createElement("td");
  6737       return td.getAttribute("rowspan") != "1";
  6738     },
  6740     /**
  6741      * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
  6742      * Chrome and Safari both don't support this
  6743      */
  6744     canSelectImagesInContentEditable: function() {
  6745       return isGecko || isIE() || isOpera;
  6746     },
  6748     /**
  6749      * All browsers except Safari and Chrome automatically scroll the range/caret position into view
  6750      */
  6751     autoScrollsToCaret: function() {
  6752       return !isWebKit;
  6753     },
  6755     /**
  6756      * Check whether the browser automatically closes tags that don't need to be opened
  6757      */
  6758     autoClosesUnclosedTags: function() {
  6759       var clonedTestElement = testElement.cloneNode(false),
  6760           returnValue,
  6761           innerHTML;
  6763       clonedTestElement.innerHTML = "<p><div></div>";
  6764       innerHTML                   = clonedTestElement.innerHTML.toLowerCase();
  6765       returnValue                 = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
  6767       // Cache result by overwriting current function
  6768       this.autoClosesUnclosedTags = function() { return returnValue; };
  6770       return returnValue;
  6771     },
  6773     /**
  6774      * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
  6775      */
  6776     supportsNativeGetElementsByClassName: function() {
  6777       return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
  6778     },
  6780     /**
  6781      * As of now (19.04.2011) only supported by Firefox 4 and Chrome
  6782      * See https://developer.mozilla.org/en/DOM/Selection/modify
  6783      */
  6784     supportsSelectionModify: function() {
  6785       return "getSelection" in window && "modify" in window.getSelection();
  6786     },
  6788     /**
  6789      * Opera needs a white space after a <br> in order to position the caret correctly
  6790      */
  6791     needsSpaceAfterLineBreak: function() {
  6792       return isOpera;
  6793     },
  6795     /**
  6796      * Whether the browser supports the speech api on the given element
  6797      * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
  6798      *
  6799      * @example
  6800      *    var input = document.createElement("input");
  6801      *    if (wysihtml.browser.supportsSpeechApiOn(input)) {
  6802      *      // ...
  6803      *    }
  6804      */
  6805     supportsSpeechApiOn: function(input) {
  6806       var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
  6807       return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
  6808     },
  6810     /**
  6811      * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
  6812      * See https://connect.microsoft.com/ie/feedback/details/650112
  6813      * or try the POC http://tifftiff.de/ie9_crash/
  6814      */
  6815     crashesWhenDefineProperty: function(property) {
  6816       return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
  6817     },
  6819     /**
  6820      * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
  6821      */
  6822     doesAsyncFocus: function() {
  6823       return isIE(12, ">");
  6824     },
  6826     /**
  6827      * 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
  6828      */
  6829     hasProblemsSettingCaretAfterImg: function() {
  6830       return isIE();
  6831     },
  6833     /* In IE when deleting with caret at the begining of LI, List get broken into half instead of merging the LI with previous */
  6834     hasLiDeletingProblem: function() {
  6835       return isIE();
  6836     },
  6838     hasUndoInContextMenu: function() {
  6839       return isGecko || isChrome || isOpera;
  6840     },
  6842     /**
  6843      * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode)
  6844      * is used (regardless if rangy or native)
  6845      * This especially happens when the caret is positioned right after a <br> because then
  6846      * insertNode() will insert the node right before the <br>
  6847      */
  6848     hasInsertNodeIssue: function() {
  6849       return isOpera;
  6850     },
  6852     /**
  6853      * 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>)
  6854      */
  6855     hasIframeFocusIssue: function() {
  6856       return isIE();
  6857     },
  6859     /**
  6860      * Chrome + Safari create invalid nested markup after paste
  6861      *
  6862      *  <p>
  6863      *    foo
  6864      *    <p>bar</p> <!-- BOO! -->
  6865      *  </p>
  6866      */
  6867     createsNestedInvalidMarkupAfterPaste: function() {
  6868       return isWebKit;
  6869     },
  6871     // In all webkit browsers there are some places where caret can not be placed at the end of blocks and directly before block level element
  6872     //   when startContainer is element.
  6873     hasCaretBlockElementIssue: function() {
  6874       return isWebKit;
  6875     },
  6877     supportsMutationEvents: function() {
  6878       return ("MutationEvent" in window);
  6879     },
  6881     /**
  6882       IE (at least up to 11) does not support clipboardData on event.
  6883       It is on window but cannot return text/html
  6884       Should actually check for clipboardData on paste event, but cannot in firefox
  6885     */
  6886     supportsModernPaste: function () {
  6887       return !isIE();
  6888     },
  6890     // Unifies the property names of element.style by returning the suitable property name for current browser
  6891     // Input property key must be the standard
  6892     fixStyleKey: function(key) {
  6893       if (key === "cssFloat") {
  6894         return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat";
  6895       }
  6896       return key;
  6897     },
  6899     usesControlRanges: function() {
  6900       return document.body && "createControlRange" in document.body;
  6901     },
  6903     // Webkit browsers have an issue that when caret is at the end of link it is moved outside of link while inserting new characters,
  6904     // so all inserted content will be after link. Selection before inserion is reported to be in link though.
  6905     // This makes changing link texts from problematic to impossible (if link is just 1 characer long) for the user.
  6906     // TODO: needs to be tested better than just browser as it some day might get fixed
  6907     hasCaretAtLinkEndInsertionProblems: function() {
  6908       return isWebKit;
  6909     }
  6910   };
  6911 })();
  6913 wysihtml.lang.array = function(arr) {
  6914   return {
  6915     /**
  6916      * Check whether a given object exists in an array
  6917      *
  6918      * @example
  6919      *    wysihtml.lang.array([1, 2]).contains(1);
  6920      *    // => true
  6921      *
  6922      * Can be used to match array with array. If intersection is found true is returned
  6923      */
  6924     contains: function(needle) {
  6925       if (Array.isArray(needle)) {
  6926         for (var i = needle.length; i--;) {
  6927           if (wysihtml.lang.array(arr).indexOf(needle[i]) !== -1) {
  6928             return true;
  6929           }
  6930         }
  6931         return false;
  6932       } else {
  6933         return wysihtml.lang.array(arr).indexOf(needle) !== -1;
  6934       }
  6935     },
  6937     /**
  6938      * Check whether a given object exists in an array and return index
  6939      * If no elelemt found returns -1
  6940      *
  6941      * @example
  6942      *    wysihtml.lang.array([1, 2]).indexOf(2);
  6943      *    // => 1
  6944      */
  6945     indexOf: function(needle) {
  6946         if (arr.indexOf) {
  6947           return arr.indexOf(needle);
  6948         } else {
  6949           for (var i=0, length=arr.length; i<length; i++) {
  6950             if (arr[i] === needle) { return i; }
  6951           }
  6952           return -1;
  6953         }
  6954     },
  6956     /**
  6957      * Substract one array from another
  6958      *
  6959      * @example
  6960      *    wysihtml.lang.array([1, 2, 3, 4]).without([3, 4]);
  6961      *    // => [1, 2]
  6962      */
  6963     without: function(arrayToSubstract) {
  6964       arrayToSubstract = wysihtml.lang.array(arrayToSubstract);
  6965       var newArr  = [],
  6966           i       = 0,
  6967           length  = arr.length;
  6968       for (; i<length; i++) {
  6969         if (!arrayToSubstract.contains(arr[i])) {
  6970           newArr.push(arr[i]);
  6971         }
  6972       }
  6973       return newArr;
  6974     },
  6976     /**
  6977      * Return a clean native array
  6978      *
  6979      * Following will convert a Live NodeList to a proper Array
  6980      * @example
  6981      *    var childNodes = wysihtml.lang.array(document.body.childNodes).get();
  6982      */
  6983     get: function() {
  6984       var i        = 0,
  6985           length   = arr.length,
  6986           newArray = [];
  6987       for (; i<length; i++) {
  6988         newArray.push(arr[i]);
  6989       }
  6990       return newArray;
  6991     },
  6993     /**
  6994      * Creates a new array with the results of calling a provided function on every element in this array.
  6995      * optionally this can be provided as second argument
  6996      *
  6997      * @example
  6998      *    var childNodes = wysihtml.lang.array([1,2,3,4]).map(function (value, index, array) {
  6999             return value * 2;
  7000      *    });
  7001      *    // => [2,4,6,8]
  7002      */
  7003     map: function(callback, thisArg) {
  7004       if (Array.prototype.map) {
  7005         return arr.map(callback, thisArg);
  7006       } else {
  7007         var len = arr.length >>> 0,
  7008             A = new Array(len),
  7009             i = 0;
  7010         for (; i < len; i++) {
  7011            A[i] = callback.call(thisArg, arr[i], i, arr);
  7012         }
  7013         return A;
  7014       }
  7015     },
  7017     /* ReturnS new array without duplicate entries
  7018      *
  7019      * @example
  7020      *    var uniq = wysihtml.lang.array([1,2,3,2,1,4]).unique();
  7021      *    // => [1,2,3,4]
  7022      */
  7023     unique: function() {
  7024       var vals = [],
  7025           max = arr.length,
  7026           idx = 0;
  7028       while (idx < max) {
  7029         if (!wysihtml.lang.array(vals).contains(arr[idx])) {
  7030           vals.push(arr[idx]);
  7031         }
  7032         idx++;
  7033       }
  7034       return vals;
  7035     }
  7037   };
  7038 };
  7040 wysihtml.lang.Dispatcher = Base.extend(
  7041   /** @scope wysihtml.lang.Dialog.prototype */ {
  7042   on: function(eventName, handler) {
  7043     this.events = this.events || {};
  7044     this.events[eventName] = this.events[eventName] || [];
  7045     this.events[eventName].push(handler);
  7046     return this;
  7047   },
  7049   off: function(eventName, handler) {
  7050     this.events = this.events || {};
  7051     var i = 0,
  7052         handlers,
  7053         newHandlers;
  7054     if (eventName) {
  7055       handlers    = this.events[eventName] || [],
  7056       newHandlers = [];
  7057       for (; i<handlers.length; i++) {
  7058         if (handlers[i] !== handler && handler) {
  7059           newHandlers.push(handlers[i]);
  7060         }
  7061       }
  7062       this.events[eventName] = newHandlers;
  7063     } else {
  7064       // Clean up all events
  7065       this.events = {};
  7066     }
  7067     return this;
  7068   },
  7070   fire: function(eventName, payload) {
  7071     this.events = this.events || {};
  7072     var handlers = this.events[eventName] || [],
  7073         i        = 0;
  7074     for (; i<handlers.length; i++) {
  7075       handlers[i].call(this, payload);
  7076     }
  7077     return this;
  7078   },
  7080   // deprecated, use .on()
  7081   observe: function() {
  7082     return this.on.apply(this, arguments);
  7083   },
  7085   // deprecated, use .off()
  7086   stopObserving: function() {
  7087     return this.off.apply(this, arguments);
  7088   }
  7089 });
  7091 wysihtml.lang.object = function(obj) {
  7092   return {
  7093     /**
  7094      * @example
  7095      *    wysihtml.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
  7096      *    // => { foo: 1, bar: 2, baz: 3 }
  7097      */
  7098     merge: function(otherObj, deep) {
  7099       for (var i in otherObj) {
  7100         if (deep && wysihtml.lang.object(otherObj[i]).isPlainObject() && (typeof obj[i] === "undefined" || wysihtml.lang.object(obj[i]).isPlainObject())) {
  7101           if (typeof obj[i] === "undefined") {
  7102             obj[i] = wysihtml.lang.object(otherObj[i]).clone(true);
  7103           } else {
  7104             wysihtml.lang.object(obj[i]).merge(wysihtml.lang.object(otherObj[i]).clone(true));
  7105           }
  7106         } else {
  7107           obj[i] = wysihtml.lang.object(otherObj[i]).isPlainObject() ? wysihtml.lang.object(otherObj[i]).clone(true) : otherObj[i];
  7108         }
  7109       }
  7110       return this;
  7111     },
  7113     difference: function (otherObj) {
  7114       var diffObj = {};
  7116       // Get old values not in comparing object
  7117       for (var i in obj) {
  7118         if (obj.hasOwnProperty(i)) {
  7119           if (!otherObj.hasOwnProperty(i)) {
  7120             diffObj[i] = obj[i];
  7121           }
  7122         }
  7123       }
  7125       // Get new and different values in comparing object
  7126       for (var o in otherObj) {
  7127         if (otherObj.hasOwnProperty(o)) {
  7128           if (!obj.hasOwnProperty(o) || obj[o] !== otherObj[o]) {
  7129             diffObj[0] = obj[0];
  7130           }
  7131         }
  7132       }
  7133       return diffObj;
  7134     },
  7136     get: function() {
  7137       return obj;
  7138     },
  7140     /**
  7141      * @example
  7142      *    wysihtml.lang.object({ foo: 1 }).clone();
  7143      *    // => { foo: 1 }
  7144      *
  7145      *    v0.4.14 adds options for deep clone : wysihtml.lang.object({ foo: 1 }).clone(true);
  7146      */
  7147     clone: function(deep) {
  7148       var newObj = {},
  7149           i;
  7151       if (obj === null || !wysihtml.lang.object(obj).isPlainObject()) {
  7152         return obj;
  7153       }
  7155       for (i in obj) {
  7156         if(obj.hasOwnProperty(i)) {
  7157           if (deep) {
  7158             newObj[i] = wysihtml.lang.object(obj[i]).clone(deep);
  7159           } else {
  7160             newObj[i] = obj[i];
  7161           }
  7162         }
  7163       }
  7164       return newObj;
  7165     },
  7167     /**
  7168      * @example
  7169      *    wysihtml.lang.object([]).isArray();
  7170      *    // => true
  7171      */
  7172     isArray: function() {
  7173       return Object.prototype.toString.call(obj) === "[object Array]";
  7174     },
  7176     /**
  7177      * @example
  7178      *    wysihtml.lang.object(function() {}).isFunction();
  7179      *    // => true
  7180      */
  7181     isFunction: function() {
  7182       return Object.prototype.toString.call(obj) === '[object Function]';
  7183     },
  7185     isPlainObject: function () {
  7186       return obj && Object.prototype.toString.call(obj) === '[object Object]' && !(("Node" in window) ? obj instanceof Node : obj instanceof Element || obj instanceof Text);
  7187     },
  7189     /**
  7190      * @example
  7191      *    wysihtml.lang.object({}).isEmpty();
  7192      *    // => true
  7193      */
  7194     isEmpty: function() {
  7195       for (var i in obj) {
  7196         if (obj.hasOwnProperty(i)) {
  7197           return false;
  7198         }
  7199       }
  7200       return true;
  7201     }
  7202   };
  7203 };
  7205 (function() {
  7206   var WHITE_SPACE_START = /^\s+/,
  7207       WHITE_SPACE_END   = /\s+$/,
  7208       ENTITY_REG_EXP    = /[&<>\t"]/g,
  7209       ENTITY_MAP = {
  7210         '&': '&',
  7211         '<': '<',
  7212         '>': '>',
  7213         '"': """,
  7214         '\t':"  "
  7215       };
  7216   wysihtml.lang.string = function(str) {
  7217     str = String(str);
  7218     return {
  7219       /**
  7220        * @example
  7221        *    wysihtml.lang.string("   foo   ").trim();
  7222        *    // => "foo"
  7223        */
  7224       trim: function() {
  7225         return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
  7226       },
  7228       /**
  7229        * @example
  7230        *    wysihtml.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
  7231        *    // => "Hello Christopher"
  7232        */
  7233       interpolate: function(vars) {
  7234         for (var i in vars) {
  7235           str = this.replace("#{" + i + "}").by(vars[i]);
  7236         }
  7237         return str;
  7238       },
  7240       /**
  7241        * @example
  7242        *    wysihtml.lang.string("Hello Tom").replace("Tom").with("Hans");
  7243        *    // => "Hello Hans"
  7244        */
  7245       replace: function(search) {
  7246         return {
  7247           by: function(replace) {
  7248             return str.split(search).join(replace);
  7249           }
  7250         };
  7251       },
  7253       /**
  7254        * @example
  7255        *    wysihtml.lang.string("hello<br>").escapeHTML();
  7256        *    // => "hello<br>"
  7257        */
  7258       escapeHTML: function(linebreaks, convertSpaces) {
  7259         var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
  7260         if (linebreaks) {
  7261           html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
  7262         }
  7263         if (convertSpaces) {
  7264           html = html.replace(/  /gi, "  ");
  7265         }
  7266         return html;
  7267       }
  7268     };
  7269   };
  7270 })();
  7272 /**
  7273  * Find urls in descendant text nodes of an element and auto-links them
  7274  * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
  7275  *
  7276  * @param {Element} element Container element in which to search for urls
  7277  *
  7278  * @example
  7279  *    <div id="text-container">Please click here: www.google.com</div>
  7280  *    <script>wysihtml.dom.autoLink(document.getElementById("text-container"));</script>
  7281  */
  7282 (function(wysihtml) {
  7283   var /**
  7284        * Don't auto-link urls that are contained in the following elements:
  7285        */
  7286       IGNORE_URLS_IN        = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
  7287       /**
  7288        * revision 1:
  7289        *    /(\S+\.{1}[^\s\,\.\!]+)/g
  7290        *
  7291        * revision 2:
  7292        *    /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
  7293        *
  7294        * put this in the beginning if you don't wan't to match within a word
  7295        *    (^|[\>\(\{\[\s\>])
  7296        */
  7297       URL_REG_EXP           = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
  7298       TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
  7299       MAX_DISPLAY_LENGTH    = 100,
  7300       BRACKETS              = { ")": "(", "]": "[", "}": "{" };
  7302   function autoLink(element, ignoreInClasses) {
  7303     if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
  7304       return element;
  7305     }
  7307     if (element === element.ownerDocument.documentElement) {
  7308       element = element.ownerDocument.body;
  7309     }
  7311     return _parseNode(element, ignoreInClasses);
  7312   }
  7314   /**
  7315    * This is basically a rebuild of
  7316    * the rails auto_link_urls text helper
  7317    */
  7318   function _convertUrlsToLinks(str) {
  7319     return str.replace(URL_REG_EXP, function(match, url) {
  7320       var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
  7321           opening     = BRACKETS[punctuation];
  7322       url = url.replace(TRAILING_CHAR_REG_EXP, "");
  7324       if (url.split(opening).length > url.split(punctuation).length) {
  7325         url = url + punctuation;
  7326         punctuation = "";
  7327       }
  7328       var realUrl    = url,
  7329           displayUrl = url;
  7330       if (url.length > MAX_DISPLAY_LENGTH) {
  7331         displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
  7332       }
  7333       // Add http prefix if necessary
  7334       if (realUrl.substr(0, 4) === "www.") {
  7335         realUrl = "http://" + realUrl;
  7336       }
  7338       return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
  7339     });
  7340   }
  7342   /**
  7343    * Creates or (if already cached) returns a temp element
  7344    * for the given document object
  7345    */
  7346   function _getTempElement(context) {
  7347     var tempElement = context._wysihtml_tempElement;
  7348     if (!tempElement) {
  7349       tempElement = context._wysihtml_tempElement = context.createElement("div");
  7350     }
  7351     return tempElement;
  7352   }
  7354   /**
  7355    * Replaces the original text nodes with the newly auto-linked dom tree
  7356    */
  7357   function _wrapMatchesInNode(textNode) {
  7358     var parentNode  = textNode.parentNode,
  7359         nodeValue   = wysihtml.lang.string(textNode.data).escapeHTML(),
  7360         tempElement = _getTempElement(parentNode.ownerDocument);
  7362     // We need to insert an empty/temporary <span /> to fix IE quirks
  7363     // Elsewise IE would strip white space in the beginning
  7364     tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue);
  7365     tempElement.removeChild(tempElement.firstChild);
  7367     while (tempElement.firstChild) {
  7368       // inserts tempElement.firstChild before textNode
  7369       parentNode.insertBefore(tempElement.firstChild, textNode);
  7370     }
  7371     parentNode.removeChild(textNode);
  7372   }
  7374   function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
  7375     var nodeName;
  7376     while (node.parentNode) {
  7377       node = node.parentNode;
  7378       nodeName = node.nodeName;
  7379       if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
  7380         return true;
  7381       }
  7382       if (IGNORE_URLS_IN.contains(nodeName)) {
  7383         return true;
  7384       } else if (nodeName === "body") {
  7385         return false;
  7386       }
  7387     }
  7388     return false;
  7389   }
  7391   function _parseNode(element, ignoreInClasses) {
  7392     if (IGNORE_URLS_IN.contains(element.nodeName)) {
  7393       return;
  7394     }
  7396     if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
  7397       return;
  7398     }
  7400     if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) {
  7401       _wrapMatchesInNode(element);
  7402       return;
  7403     }
  7405     var childNodes        = wysihtml.lang.array(element.childNodes).get(),
  7406         childNodesLength  = childNodes.length,
  7407         i                 = 0;
  7409     for (; i<childNodesLength; i++) {
  7410       _parseNode(childNodes[i], ignoreInClasses);
  7411     }
  7413     return element;
  7414   }
  7416   wysihtml.dom.autoLink = autoLink;
  7418   // Reveal url reg exp to the outside
  7419   wysihtml.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
  7420 })(wysihtml);
  7422 (function(wysihtml) {
  7423   var api = wysihtml.dom;
  7425   api.addClass = function(element, className) {
  7426     var classList = element.classList;
  7427     if (classList) {
  7428       return classList.add(className);
  7429     }
  7430     if (api.hasClass(element, className)) {
  7431       return;
  7432     }
  7433     element.className += " " + className;
  7434   };
  7436   api.removeClass = function(element, className) {
  7437     var classList = element.classList;
  7438     if (classList) {
  7439       return classList.remove(className);
  7440     }
  7442     element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
  7443   };
  7445   api.hasClass = function(element, className) {
  7446     var classList = element.classList;
  7447     if (classList) {
  7448       return classList.contains(className);
  7449     }
  7451     var elementClassName = element.className;
  7452     return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  7453   };
  7454 })(wysihtml);
  7456 wysihtml.dom.compareDocumentPosition = (function() {
  7457   var documentElement = document.documentElement;
  7458   if (documentElement.compareDocumentPosition) {
  7459     return function(container, element) {
  7460       return container.compareDocumentPosition(element);
  7461     };
  7462   } else {
  7463     return function( container, element ) {
  7464       // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
  7465       var thisOwner, otherOwner;
  7467       if( container.nodeType === 9) // Node.DOCUMENT_NODE
  7468         thisOwner = container;
  7469       else
  7470         thisOwner = container.ownerDocument;
  7472       if( element.nodeType === 9) // Node.DOCUMENT_NODE
  7473         otherOwner = element;
  7474       else
  7475         otherOwner = element.ownerDocument;
  7477       if( container === element ) return 0;
  7478       if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
  7479       if( container.ownerDocument === element ) return 2 + 8;  //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
  7480       if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
  7482       // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
  7483       if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml.lang.array(container.childNodes).indexOf( element ) !== -1)
  7484         return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
  7486       if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml.lang.array(element.childNodes).indexOf( container ) !== -1)
  7487         return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
  7489       var point = container;
  7490       var parents = [ ];
  7491       var previous = null;
  7492       while( point ) {
  7493         if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
  7494         parents.push( point );
  7495         point = point.parentNode;
  7496       }
  7497       point = element;
  7498       previous = null;
  7499       while( point ) {
  7500         if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
  7501         var location_index = wysihtml.lang.array(parents).indexOf( point );
  7502         if( location_index !== -1) {
  7503          var smallest_common_ancestor = parents[ location_index ];
  7504          var this_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] );
  7505          var other_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
  7506          if( this_index > other_index ) {
  7507                return 2; //Node.DOCUMENT_POSITION_PRECEDING;
  7508          }
  7509          else {
  7510            return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
  7511          }
  7512         }
  7513         previous = point;
  7514         point = point.parentNode;
  7515       }
  7516       return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
  7517     };
  7518   }
  7519 })();
  7521 wysihtml.dom.contains = (function() {
  7522   var documentElement = document.documentElement;
  7523   if (documentElement.contains) {
  7524     return function(container, element) {
  7525       if (element.nodeType !== wysihtml.ELEMENT_NODE) {
  7526         if (element.parentNode === container) {
  7527           return true;
  7528         }
  7529         element = element.parentNode;
  7530       }
  7531       return container !== element && container.contains(element);
  7532     };
  7533   } else if (documentElement.compareDocumentPosition) {
  7534     return function(container, element) {
  7535       // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
  7536       return !!(container.compareDocumentPosition(element) & 16);
  7537     };
  7538   }
  7539 })();
  7541 (function(wysihtml) {
  7542   var doc = document;
  7543   wysihtml.dom.ContentEditableArea = Base.extend({
  7544       getContentEditable: function() {
  7545         return this.element;
  7546       },
  7548       getWindow: function() {
  7549         return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow;
  7550       },
  7552       getDocument: function() {
  7553         return this.element.ownerDocument;
  7554       },
  7556       constructor: function(readyCallback, config, contentEditable) {
  7557         this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
  7558         this.config   = wysihtml.lang.object({}).merge(config).get();
  7559         if (!this.config.className) {
  7560           this.config.className = "wysihtml-sandbox";
  7561         }
  7562         if (contentEditable) {
  7563             this.element = this._bindElement(contentEditable);
  7564         } else {
  7565             this.element = this._createElement();
  7566         }
  7567       },
  7569       destroy: function() {
  7571       },
  7573       // creates a new contenteditable and initiates it
  7574       _createElement: function() {
  7575         var element = doc.createElement("div");
  7576         element.className = this.config.className;
  7577         this._loadElement(element);
  7578         return element;
  7579       },
  7581       // initiates an allready existent contenteditable
  7582       _bindElement: function(contentEditable) {
  7583         contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml-sandbox" : "wysihtml-sandbox";
  7584         this._loadElement(contentEditable, true);
  7585         return contentEditable;
  7586       },
  7588       _loadElement: function(element, contentExists) {
  7589         var that = this;
  7591         if (!contentExists) {
  7592             var innerHtml = this._getHtml();
  7593             element.innerHTML = innerHtml;
  7594         }
  7596         this.loaded = true;
  7597         // Trigger the callback
  7598         setTimeout(function() { that.callback(that); }, 0);
  7599       },
  7601       _getHtml: function(templateVars) {
  7602         return '';
  7603       }
  7605   });
  7606 })(wysihtml);
  7608 /**
  7609  * Converts an HTML fragment/element into a unordered/ordered list
  7610  *
  7611  * @param {Element} element The element which should be turned into a list
  7612  * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
  7613  * @return {Element} The created list
  7614  *
  7615  * @example
  7616  *    <!-- Assume the following dom: -->
  7617  *    <span id="pseudo-list">
  7618  *      eminem<br>
  7619  *      dr. dre
  7620  *      <div>50 Cent</div>
  7621  *    </span>
  7622  *
  7623  *    <script>
  7624  *      wysihtml.dom.convertToList(document.getElementById("pseudo-list"), "ul");
  7625  *    </script>
  7626  *
  7627  *    <!-- Will result in: -->
  7628  *    <ul>
  7629  *      <li>eminem</li>
  7630  *      <li>dr. dre</li>
  7631  *      <li>50 Cent</li>
  7632  *    </ul>
  7633  */
  7634 wysihtml.dom.convertToList = (function() {
  7635   function _createListItem(doc, list) {
  7636     var listItem = doc.createElement("li");
  7637     list.appendChild(listItem);
  7638     return listItem;
  7639   }
  7641   function _createList(doc, type) {
  7642     return doc.createElement(type);
  7643   }
  7645   function convertToList(element, listType, uneditableClass) {
  7646     if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
  7647       // Already a list
  7648       return element;
  7649     }
  7651     var doc               = element.ownerDocument,
  7652         list              = _createList(doc, listType),
  7653         lineBreaks        = element.querySelectorAll("br"),
  7654         lineBreaksLength  = lineBreaks.length,
  7655         childNodes,
  7656         childNodesLength,
  7657         childNode,
  7658         lineBreak,
  7659         parentNode,
  7660         isBlockElement,
  7661         isLineBreak,
  7662         currentListItem,
  7663         i;
  7665     // First find <br> at the end of inline elements and move them behind them
  7666     for (i=0; i<lineBreaksLength; i++) {
  7667       lineBreak = lineBreaks[i];
  7668       while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
  7669         if (wysihtml.dom.getStyle("display").from(parentNode) === "block") {
  7670           parentNode.removeChild(lineBreak);
  7671           break;
  7672         }
  7673         wysihtml.dom.insert(lineBreak).after(lineBreak.parentNode);
  7674       }
  7675     }
  7677     childNodes        = wysihtml.lang.array(element.childNodes).get();
  7678     childNodesLength  = childNodes.length;
  7680     for (i=0; i<childNodesLength; i++) {
  7681       currentListItem   = currentListItem || _createListItem(doc, list);
  7682       childNode         = childNodes[i];
  7683       isBlockElement    = wysihtml.dom.getStyle("display").from(childNode) === "block";
  7684       isLineBreak       = childNode.nodeName === "BR";
  7686       // consider uneditable as an inline element
  7687       if (isBlockElement && (!uneditableClass || !wysihtml.dom.hasClass(childNode, uneditableClass))) {
  7688         // Append blockElement to current <li> if empty, otherwise create a new one
  7689         currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
  7690         currentListItem.appendChild(childNode);
  7691         currentListItem = null;
  7692         continue;
  7693       }
  7695       if (isLineBreak) {
  7696         // Only create a new list item in the next iteration when the current one has already content
  7697         currentListItem = currentListItem.firstChild ? null : currentListItem;
  7698         continue;
  7699       }
  7701       currentListItem.appendChild(childNode);
  7702     }
  7704     if (childNodes.length === 0) {
  7705       _createListItem(doc, list);
  7706     }
  7708     element.parentNode.replaceChild(list, element);
  7709     return list;
  7710   }
  7712   return convertToList;
  7713 })();
  7715 /**
  7716  * Copy a set of attributes from one element to another
  7717  *
  7718  * @param {Array} attributesToCopy List of attributes which should be copied
  7719  * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
  7720  *    copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
  7721  *    with the element where to copy the attributes to (see example)
  7722  *
  7723  * @example
  7724  *    var textarea    = document.querySelector("textarea"),
  7725  *        div         = document.querySelector("div[contenteditable=true]"),
  7726  *        anotherDiv  = document.querySelector("div.preview");
  7727  *    wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
  7728  *
  7729  */
  7730 wysihtml.dom.copyAttributes = function(attributesToCopy) {
  7731   return {
  7732     from: function(elementToCopyFrom) {
  7733       return {
  7734         to: function pasteElementAttributesTo(elementToCopyTo) {
  7735           var attribute,
  7736               i         = 0,
  7737               length    = attributesToCopy.length;
  7738           for (; i<length; i++) {
  7739             attribute = attributesToCopy[i];
  7740             if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
  7741               elementToCopyTo[attribute] = elementToCopyFrom[attribute];
  7742             }
  7743           }
  7744           return { andTo: pasteElementAttributesTo };
  7745         }
  7746       };
  7747     }
  7748   };
  7749 };
  7751 /**
  7752  * Copy a set of styles from one element to another
  7753  * Please note that this only works properly across browsers when the element from which to copy the styles
  7754  * is in the dom
  7755  *
  7756  * Interesting article on how to copy styles
  7757  *
  7758  * @param {Array} stylesToCopy List of styles which should be copied
  7759  * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
  7760  *    copy the styles from., this again returns an object which provides a method named "to" which can be invoked
  7761  *    with the element where to copy the styles to (see example)
  7762  *
  7763  * @example
  7764  *    var textarea    = document.querySelector("textarea"),
  7765  *        div         = document.querySelector("div[contenteditable=true]"),
  7766  *        anotherDiv  = document.querySelector("div.preview");
  7767  *    wysihtml.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
  7768  *
  7769  */
  7770 (function(dom) {
  7772   /**
  7773    * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
  7774    * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
  7775    * its computed css width will be 198px
  7776    *
  7777    * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
  7778    */
  7779   var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
  7781   var shouldIgnoreBoxSizingBorderBox = function(element) {
  7782     if (hasBoxSizingBorderBox(element)) {
  7783        return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
  7784     }
  7785     return false;
  7786   };
  7788   var hasBoxSizingBorderBox = function(element) {
  7789     var i       = 0,
  7790         length  = BOX_SIZING_PROPERTIES.length;
  7791     for (; i<length; i++) {
  7792       if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
  7793         return BOX_SIZING_PROPERTIES[i];
  7794       }
  7795     }
  7796   };
  7798   dom.copyStyles = function(stylesToCopy) {
  7799     return {
  7800       from: function(element) {
  7801         if (shouldIgnoreBoxSizingBorderBox(element)) {
  7802           stylesToCopy = wysihtml.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
  7803         }
  7805         var cssText = "",
  7806             length  = stylesToCopy.length,
  7807             i       = 0,
  7808             property;
  7809         for (; i<length; i++) {
  7810           property = stylesToCopy[i];
  7811           cssText += property + ":" + dom.getStyle(property).from(element) + ";";
  7812         }
  7814         return {
  7815           to: function pasteStylesTo(element) {
  7816             dom.setStyles(cssText).on(element);
  7817             return { andTo: pasteStylesTo };
  7818           }
  7819         };
  7820       }
  7821     };
  7822   };
  7823 })(wysihtml.dom);
  7825 /**
  7826  * Event Delegation
  7827  *
  7828  * @example
  7829  *    wysihtml.dom.delegate(document.body, "a", "click", function() {
  7830  *      // foo
  7831  *    });
  7832  */
  7833 (function(wysihtml) {
  7834   wysihtml.dom.delegate = function(container, selector, eventName, handler) {
  7835     var callback = function(event) {
  7836       var target = event.target,
  7837           element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes
  7838           matches  = container.querySelectorAll(selector);
  7840       for (var i = 0, max = matches.length; i < max; i++) {
  7841         if (matches[i].contains(element)) {
  7842           handler.call(matches[i], event);
  7843         }
  7844       }
  7845     };
  7847     container.addEventListener(eventName, callback, false);
  7848     return {
  7849       stop: function() {
  7850         container.removeEventListener(eventName, callback, false);
  7851       }
  7852     };
  7853   };
  7854 })(wysihtml);
  7856 // TODO: Refactor dom tree traversing here
  7857 (function(wysihtml) {
  7859   // Finds parents of a node, returning the outermost node first in Array
  7860   // if contain node is given parents search is stopped at the container
  7861   function parents(node, container) {
  7862     var nodes = [node], n = node;
  7864     // iterate parents while parent exists and it is not container element
  7865     while((container && n && n !== container) || (!container && n)) {
  7866       nodes.unshift(n);
  7867       n = n.parentNode;
  7868     }
  7869     return nodes;
  7870   }
  7872   wysihtml.dom.domNode = function(node) {
  7873     var defaultNodeTypes = [wysihtml.ELEMENT_NODE, wysihtml.TEXT_NODE];
  7875     return {
  7877       is: {
  7878         emptyTextNode: function(ignoreWhitespace) {
  7879           var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g);
  7880           return node && node.nodeType === wysihtml.TEXT_NODE && (regx).test(node.data);
  7881         },
  7883         // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
  7884         rangyBookmark: function() {
  7885           return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary');
  7886         },
  7888         visible: function() {
  7889           var isVisible = !(/^\s*$/g).test(wysihtml.dom.getTextContent(node));
  7891           if (!isVisible) {
  7892             if (node.nodeType === 1 && node.querySelector('img, br, hr, object, embed, canvas, input, textarea')) {
  7893               isVisible = true;
  7894             }
  7895           }
  7896           return isVisible;
  7897         },
  7898         lineBreak: function() {
  7899           return node && node.nodeType === 1 && node.nodeName === "BR";
  7900         },
  7901         block: function() {
  7902           return node && node.nodeType === 1 && node.ownerDocument.defaultView.getComputedStyle(node).display === "block";
  7903         },
  7904         // Void elements are elemens that can not have content
  7905         // In most cases browsers should solve the cases for you when you try to insert content into those,
  7906         //    but IE does not and it is not nice to do so anyway.
  7907         voidElement: function() {
  7908           return wysihtml.dom.domNode(node).test({
  7909             query: wysihtml.VOID_ELEMENTS
  7910           });
  7911         }
  7912       },
  7914       // var node = wysihtml.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
  7915       prev: function(options) {
  7916         var prevNode = node.previousSibling,
  7917             types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
  7919         if (!prevNode) {
  7920           return null;
  7921         }
  7923         if (
  7924           wysihtml.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
  7925           (!wysihtml.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
  7926           (options && options.ignoreBlankTexts && wysihtml.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
  7927         ) {
  7928           return wysihtml.dom.domNode(prevNode).prev(options);
  7929         }
  7931         return prevNode;
  7932       },
  7934       // var node = wysihtml.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true});
  7935       next: function(options) {
  7936         var nextNode = node.nextSibling,
  7937             types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
  7939         if (!nextNode) {
  7940           return null;
  7941         }
  7943         if (
  7944           wysihtml.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
  7945           (!wysihtml.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
  7946           (options && options.ignoreBlankTexts && wysihtml.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
  7947         ) {
  7948           return wysihtml.dom.domNode(nextNode).next(options);
  7949         }
  7951         return nextNode;
  7952       },
  7954       // Finds the common acnestor container of two nodes
  7955       // If container given stops search at the container
  7956       // If no common ancestor found returns null
  7957       // var node = wysihtml.dom.domNode(element).commonAncestor(node2, container);
  7958       commonAncestor: function(node2, container) {
  7959         var parents1 = parents(node, container),
  7960             parents2 = parents(node2, container);
  7962         // Ensure we have found a common ancestor, which will be the first one if anything
  7963         if (parents1[0] != parents2[0]) {
  7964           return null;
  7965         }
  7967         // Traverse up the hierarchy of parents until we reach where they're no longer
  7968         // the same. Then return previous which was the common ancestor.
  7969         for (var i = 0; i < parents1.length; i++) {
  7970           if (parents1[i] != parents2[i]) {
  7971             return parents1[i - 1];
  7972           }
  7973         }
  7975         return null;
  7976       },
  7978       // Traverses a node for last children and their chidren (including itself), and finds the last node that has no children.
  7979       // Array of classes for forced last-leaves (ex: uneditable-container) can be defined (options = {leafClasses: [...]})
  7980       // Useful for finding the actually visible element before cursor
  7981       lastLeafNode: function(options) {
  7982         var lastChild;
  7984         // Returns non-element nodes
  7985         if (node.nodeType !== 1) {
  7986           return node;
  7987         }
  7989         // Returns if element is leaf
  7990         lastChild = node.lastChild;
  7991         if (!lastChild) {
  7992           return node;
  7993         }
  7995         // Returns if element is of of options.leafClasses leaf
  7996         if (options && options.leafClasses) {
  7997           for (var i = options.leafClasses.length; i--;) {
  7998             if (wysihtml.dom.hasClass(node, options.leafClasses[i])) {
  7999               return node;
  8000             }
  8001           }
  8002         }
  8004         return wysihtml.dom.domNode(lastChild).lastLeafNode(options);
  8005       },
  8007       // Splits element at childnode and extracts the childNode out of the element context
  8008       // Example:
  8009       //   var node = wysihtml.dom.domNode(node).escapeParent(parentNode);
  8010       escapeParent: function(element, newWrapper) {
  8011         var parent, split2, nodeWrap,
  8012             curNode = node;
  8014         // Stop if node is not a descendant of element
  8015         if (!wysihtml.dom.contains(element, node)) {
  8016           throw new Error("Child is not a descendant of node.");
  8017         }
  8019         // Climb up the node tree untill node is reached
  8020         do {
  8021           // Get current parent of node
  8022           parent = curNode.parentNode;
  8024           // Move after nodes to new clone wrapper
  8025           split2 = parent.cloneNode(false);
  8026           while (parent.lastChild && parent.lastChild !== curNode) {
  8027             split2.insertBefore(parent.lastChild, split2.firstChild);
  8028           }
  8030           // Move node up a level. If parent is not yet the container to escape, clone the parent around node, so inner nodes are escaped out too
  8031           if (parent !== element) {
  8032             nodeWrap = parent.cloneNode(false);
  8033             nodeWrap.appendChild(curNode);
  8034             curNode = nodeWrap;
  8035           }
  8036           parent.parentNode.insertBefore(curNode, parent.nextSibling);
  8038           // Add after nodes (unless empty)
  8039           if (split2.innerHTML !== '') {
  8040             // if contents are empty insert without wrap
  8041             if ((/^\s+$/).test(split2.innerHTML)) {
  8042               while (split2.lastChild) {
  8043                 parent.parentNode.insertBefore(split2.lastChild, curNode.nextSibling);
  8044               }
  8045             } else {
  8046               parent.parentNode.insertBefore(split2, curNode.nextSibling);
  8047             }
  8048           }
  8050           // If the node left behind before the split (parent) is now empty then remove
  8051           if (parent.innerHTML === '') {
  8052             parent.parentNode.removeChild(parent);
  8053           } else if ((/^\s+$/).test(parent.innerHTML)) {
  8054             while (parent.firstChild) {
  8055               parent.parentNode.insertBefore(parent.firstChild, parent);
  8056             }
  8057             parent.parentNode.removeChild(parent);
  8058           }
  8060         } while (parent && parent !== element);
  8062         if (newWrapper && curNode) {
  8063           curNode.parentNode.insertBefore(newWrapper, curNode);
  8064           newWrapper.appendChild(curNode);
  8065         }
  8066       },
  8068       transferContentTo: function(targetNode, removeOldWrapper) {
  8069         if (node.nodeType === 1) {
  8070           if (wysihtml.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) {
  8071             while (node.lastChild) {
  8072               targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling);
  8073             }
  8074           } else {
  8075             while (node.firstChild) {
  8076               targetNode.appendChild(node.firstChild);
  8077             }
  8078           }
  8079           if (removeOldWrapper) {
  8080             node.parentNode.removeChild(node);
  8081           }
  8082         } else if (node.nodeType === 3 || node.nodeType === 8){
  8083           if (wysihtml.dom.domNode(targetNode).is.voidElement()) {
  8084             targetNode.parentNode.insertBefore(node, targetNode.nextSibling);
  8085           } else {
  8086             targetNode.appendChild(node);
  8087           }
  8088         }
  8089       },
  8091       /*
  8092         Tests a node against properties, and returns true if matches.
  8093         Tests on principle that all properties defined must have at least one match.
  8094         styleValue parameter works in context of styleProperty and has no effect otherwise.
  8095         Returns true if element matches and false if it does not.
  8097         Properties for filtering element:
  8098         {
  8099           query: selector string,
  8100           nodeName: string (uppercase),
  8101           className: string,
  8102           classRegExp: regex,
  8103           styleProperty: string or [],
  8104           styleValue: string, [] or regex
  8105         }
  8107         Example:
  8108         var node = wysihtml.dom.domNode(element).test({})
  8109       */
  8110       test: function(properties) {
  8111         var prop;
  8113         // return false if properties object is not defined
  8114         if (!properties) {
  8115           return false;
  8116         }
  8118         // Only element nodes can be tested for these properties
  8119         if (node.nodeType !== 1) {
  8120           return false;
  8121         }
  8123         if (properties.query) {
  8124           if (!node.matches(properties.query)) {
  8125             return false;
  8126           }
  8127         }
  8129         if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
  8130           return false;
  8131         }
  8133         if (properties.className && !node.classList.contains(properties.className)) {
  8134           return false;
  8135         }
  8137         // classRegExp check (useful for classname begins with logic)
  8138         if (properties.classRegExp) {
  8139           var matches = (node.className || "").match(properties.classRegExp) || [];
  8140           if (matches.length === 0) {
  8141             return false;
  8142           }
  8143         }
  8145         // styleProperty check
  8146         if (properties.styleProperty && properties.styleProperty.length > 0) {
  8147           var hasOneStyle = false,
  8148               styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty];
  8149           for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) {
  8150             // Some old IE-s have different property name for cssFloat
  8151             prop = wysihtml.browser.fixStyleKey(styles[j]);
  8152             if (node.style[prop]) {
  8153               if (properties.styleValue) {
  8154                 // Style value as additional parameter
  8155                 if (properties.styleValue instanceof RegExp) {
  8156                   // style value as Regexp
  8157                   if (node.style[prop].trim().match(properties.styleValue).length > 0) {
  8158                     hasOneStyle = true;
  8159                     break;
  8160                   }
  8161                 } else if (Array.isArray(properties.styleValue)) {
  8162                   // style value as array
  8163                   if (properties.styleValue.indexOf(node.style[prop].trim())) {
  8164                     hasOneStyle = true;
  8165                     break;
  8166                   }
  8167                 } else {
  8168                   // style value as string
  8169                   if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) {
  8170                     hasOneStyle = true;
  8171                     break;
  8172                   }
  8173                 }
  8174               } else {
  8175                 hasOneStyle = true;
  8176                 break;
  8177               }
  8178             }
  8179             if (!hasOneStyle) {
  8180               return false;
  8181             }
  8182           }
  8183         }
  8185         if (properties.attribute) {
  8186           var attr = wysihtml.dom.getAttributes(node),
  8187               attrList = [],
  8188               hasOneAttribute = false;
  8190           if (Array.isArray(properties.attribute)) {
  8191             attrList = properties.attribute;
  8192           } else {
  8193             attrList[properties.attribute] = properties.attributeValue;
  8194           }
  8196           for (var a in attrList) {
  8197             if (attrList.hasOwnProperty(a)) {
  8198               if (typeof attrList[a] === "undefined") {
  8199                 if (typeof attr[a] !== "undefined") {
  8200                   hasOneAttribute = true;
  8201                   break;
  8202                 }
  8203               } else if (attr[a] === attrList[a]) {
  8204                 hasOneAttribute = true;
  8205                 break;
  8206               }
  8207             }
  8208           }
  8210           if (!hasOneAttribute) {
  8211             return false;
  8212           }
  8214         }
  8216         return true;
  8217       }
  8219     };
  8220   };
  8221 })(wysihtml);
  8223 /**
  8224  * Returns the given html wrapped in a div element
  8225  *
  8226  * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
  8227  * when inserted via innerHTML
  8228  *
  8229  * @param {String} html The html which should be wrapped in a dom element
  8230  * @param {Obejct} [context] Document object of the context the html belongs to
  8231  *
  8232  * @example
  8233  *    wysihtml.dom.getAsDom("<article>foo</article>");
  8234  */
  8235 wysihtml.dom.getAsDom = (function() {
  8237   var _innerHTMLShiv = function(html, context) {
  8238     var tempElement = context.createElement("div");
  8239     tempElement.style.display = "none";
  8240     context.body.appendChild(tempElement);
  8241     // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
  8242     try { tempElement.innerHTML = html; } catch(e) {}
  8243     context.body.removeChild(tempElement);
  8244     return tempElement;
  8245   };
  8247   /**
  8248    * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
  8249    */
  8250   var _ensureHTML5Compatibility = function(context) {
  8251     if (context._wysihtml_supportsHTML5Tags) {
  8252       return;
  8253     }
  8254     for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
  8255       context.createElement(HTML5_ELEMENTS[i]);
  8256     }
  8257     context._wysihtml_supportsHTML5Tags = true;
  8258   };
  8261   /**
  8262    * List of html5 tags
  8263    * taken from http://simon.html5.org/html5-elements
  8264    */
  8265   var HTML5_ELEMENTS = [
  8266     "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
  8267     "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
  8268     "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
  8269   ];
  8271   return function(html, context) {
  8272     context = context || document;
  8273     var tempElement;
  8274     if (typeof(html) === "object" && html.nodeType) {
  8275       tempElement = context.createElement("div");
  8276       tempElement.appendChild(html);
  8277     } else if (wysihtml.browser.supportsHTML5Tags(context)) {
  8278       tempElement = context.createElement("div");
  8279       tempElement.innerHTML = html;
  8280     } else {
  8281       _ensureHTML5Compatibility(context);
  8282       tempElement = _innerHTMLShiv(html, context);
  8283     }
  8284     return tempElement;
  8285   };
  8286 })();
  8288 /**
  8289  * Get a set of attribute from one element
  8290  *
  8291  * IE gives wrong results for hasAttribute/getAttribute, for example:
  8292  *    var td = document.createElement("td");
  8293  *    td.getAttribute("rowspan"); // => "1" in IE
  8294  *
  8295  * Therefore we have to check the element's outerHTML for the attribute
  8296 */
  8298 wysihtml.dom.getAttribute = function(node, attributeName) {
  8299   var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly();
  8300   attributeName = attributeName.toLowerCase();
  8301   var nodeName = node.nodeName;
  8302   if (nodeName == "IMG" && attributeName == "src" && wysihtml.dom.isLoadedImage(node) === true) {
  8303     // Get 'src' attribute value via object property since this will always contain the
  8304     // full absolute url (http://...)
  8305     // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
  8306     // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
  8307     return node.src;
  8308   } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
  8309     // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
  8310     var outerHTML      = node.outerHTML.toLowerCase(),
  8311         // TODO: This might not work for attributes without value: <input disabled>
  8312         hasAttribute   = outerHTML.indexOf(" " + attributeName +  "=") != -1;
  8314     return hasAttribute ? node.getAttribute(attributeName) : null;
  8315   } else{
  8316     return node.getAttribute(attributeName);
  8317   }
  8318 };
  8320 /**
  8321  * Get all attributes of an element
  8322  *
  8323  * IE gives wrong results for hasAttribute/getAttribute, for example:
  8324  *    var td = document.createElement("td");
  8325  *    td.getAttribute("rowspan"); // => "1" in IE
  8326  *
  8327  * Therefore we have to check the element's outerHTML for the attribute
  8328 */
  8330 wysihtml.dom.getAttributes = function(node) {
  8331   var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(),
  8332       nodeName = node.nodeName,
  8333       attributes = [],
  8334       attr;
  8336   for (attr in node.attributes) {
  8337     if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr)))  {
  8338       if (node.attributes[attr].specified) {
  8339         if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml.dom.isLoadedImage(node) === true) {
  8340           attributes['src'] = node.src;
  8341         } else if (wysihtml.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
  8342           if (node.attributes[attr].value !== 1) {
  8343             attributes[node.attributes[attr].name] = node.attributes[attr].value;
  8344           }
  8345         } else {
  8346           attributes[node.attributes[attr].name] = node.attributes[attr].value;
  8347         }
  8348       }
  8349     }
  8350   }
  8351   return attributes;
  8352 };
  8354 /**
  8355  * Walks the dom tree from the given node up until it finds a match
  8356  *
  8357  * @param {Element} node The from which to check the parent nodes
  8358  * @param {Object} matchingSet Object to match against, Properties for filtering element:
  8359  *   {
  8360  *     query: selector string,
  8361  *     classRegExp: regex,
  8362  *     styleProperty: string or [],
  8363  *     styleValue: string, [] or regex
  8364  *   }
  8365  * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
  8366  * @param {Element} Optional, defines the container that limits the search
  8367  *
  8368  * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
  8369 */
  8371 wysihtml.dom.getParentElement = (function() {
  8373   return function(node, properties, levels, container) {
  8374     levels = levels || 50;
  8375     while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
  8376       if (wysihtml.dom.domNode(node).test(properties)) {
  8377         return node;
  8378       }
  8379       node = node.parentNode;
  8380     }
  8381     return null;
  8382   };
  8384 })();
  8386 /* 
  8387  * Methods for fetching pasted html before it gets inserted into content
  8388 **/
  8390 /* Modern event.clipboardData driven approach.
  8391  * Advantage is that it does not have to loose selection or modify dom to catch the data. 
  8392  * IE does not support though.
  8393 **/
  8394 wysihtml.dom.getPastedHtml = function(event) {
  8395   var html;
  8396   if (wysihtml.browser.supportsModernPaste() && event.clipboardData) {
  8397     if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) {
  8398       html = event.clipboardData.getData('text/html');
  8399     } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) {
  8400       html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
  8401     }
  8402   }
  8403   return html;
  8404 };
  8406 /* Older temprorary contenteditable as paste source catcher method for fallbacks */
  8407 wysihtml.dom.getPastedHtmlWithDiv = function (composer, f) {
  8408   var selBookmark = composer.selection.getBookmark(),
  8409       doc = composer.element.ownerDocument,
  8410       cleanerDiv = doc.createElement('DIV'),
  8411       scrollPos = composer.getScrollPos();
  8413   doc.body.appendChild(cleanerDiv);
  8415   cleanerDiv.style.width = "1px";
  8416   cleanerDiv.style.height = "1px";
  8417   cleanerDiv.style.overflow = "hidden";
  8418   cleanerDiv.style.position = "absolute";
  8419   cleanerDiv.style.top = scrollPos.y + "px";
  8420   cleanerDiv.style.left = scrollPos.x + "px";
  8422   cleanerDiv.setAttribute('contenteditable', 'true');
  8423   cleanerDiv.focus();
  8425   setTimeout(function () {
  8426     var html;
  8428     composer.selection.setBookmark(selBookmark);
  8429     html = cleanerDiv.innerHTML;
  8430     if (html && (/^<br\/?>$/i).test(html.trim())) {
  8431       html = false;
  8432     }
  8433     f(html);
  8434     cleanerDiv.parentNode.removeChild(cleanerDiv);
  8435   }, 0);
  8436 };
  8438 /**
  8439  * Get element's style for a specific css property
  8440  *
  8441  * @param {Element} element The element on which to retrieve the style
  8442  * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
  8443  *
  8444  * @example
  8445  *    wysihtml.dom.getStyle("display").from(document.body);
  8446  *    // => "block"
  8447  */
  8448 wysihtml.dom.getStyle = (function() {
  8449   var stylePropertyMapping = {
  8450         "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
  8451       },
  8452       REG_EXP_CAMELIZE = /\-[a-z]/g;
  8454   function camelize(str) {
  8455     return str.replace(REG_EXP_CAMELIZE, function(match) {
  8456       return match.charAt(1).toUpperCase();
  8457     });
  8458   }
  8460   return function(property) {
  8461     return {
  8462       from: function(element) {
  8463         if (element.nodeType !== wysihtml.ELEMENT_NODE) {
  8464           return;
  8465         }
  8467         var doc               = element.ownerDocument,
  8468             camelizedProperty = stylePropertyMapping[property] || camelize(property),
  8469             style             = element.style,
  8470             currentStyle      = element.currentStyle,
  8471             styleValue        = style[camelizedProperty];
  8472         if (styleValue) {
  8473           return styleValue;
  8474         }
  8476         // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
  8477         // window.getComputedStyle, since it returns css property values in their original unit:
  8478         // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
  8479         // gives you the original "50%".
  8480         // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
  8481         if (currentStyle) {
  8482           try {
  8483             return currentStyle[camelizedProperty];
  8484           } catch(e) {
  8485             //ie will occasionally fail for unknown reasons. swallowing exception
  8486           }
  8487         }
  8489         var win                 = doc.defaultView || doc.parentWindow,
  8490             needsOverflowReset  = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
  8491             originalOverflow,
  8492             returnValue;
  8494         if (win.getComputedStyle) {
  8495           // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
  8496           // therfore we remove and restore the scrollbar and calculate the value in between
  8497           if (needsOverflowReset) {
  8498             originalOverflow = style.overflow;
  8499             style.overflow = "hidden";
  8500           }
  8501           returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
  8502           if (needsOverflowReset) {
  8503             style.overflow = originalOverflow || "";
  8504           }
  8505           return returnValue;
  8506         }
  8507       }
  8508     };
  8509   };
  8510 })();
  8512 wysihtml.dom.getTextNodes = function(node, ingoreEmpty){
  8513   var all = [];
  8514   for (node=node.firstChild;node;node=node.nextSibling){
  8515     if (node.nodeType == 3) {
  8516       if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
  8517         all.push(node);
  8518       }
  8519     } else {
  8520       all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty));
  8521     }
  8522   }
  8523   return all;
  8524 };
  8526 /**
  8527  * High performant way to check whether an element with a specific class name is in the given document
  8528  * Optimized for being heavily executed
  8529  * Unleashes the power of live node lists
  8530  *
  8531  * @param {Object} doc The document object of the context where to check
  8532  * @param {String} tagName Upper cased tag name
  8533  * @example
  8534  *    wysihtml.dom.hasElementWithClassName(document, "foobar");
  8535  */
  8536 (function(wysihtml) {
  8537   var LIVE_CACHE          = {},
  8538       DOCUMENT_IDENTIFIER = 1;
  8540   function _getDocumentIdentifier(doc) {
  8541     return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
  8542   }
  8544   wysihtml.dom.hasElementWithClassName = function(doc, className) {
  8545     // getElementsByClassName is not supported by IE<9
  8546     // but is sometimes mocked via library code (which then doesn't return live node lists)
  8547     if (!wysihtml.browser.supportsNativeGetElementsByClassName()) {
  8548       return !!doc.querySelector("." + className);
  8549     }
  8551     var key         = _getDocumentIdentifier(doc) + ":" + className,
  8552         cacheEntry  = LIVE_CACHE[key];
  8553     if (!cacheEntry) {
  8554       cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
  8555     }
  8557     return cacheEntry.length > 0;
  8558   };
  8559 })(wysihtml);
  8561 /**
  8562  * High performant way to check whether an element with a specific tag name is in the given document
  8563  * Optimized for being heavily executed
  8564  * Unleashes the power of live node lists
  8565  *
  8566  * @param {Object} doc The document object of the context where to check
  8567  * @param {String} tagName Upper cased tag name
  8568  * @example
  8569  *    wysihtml.dom.hasElementWithTagName(document, "IMG");
  8570  */
  8571 wysihtml.dom.hasElementWithTagName = (function() {
  8572   var LIVE_CACHE          = {},
  8573       DOCUMENT_IDENTIFIER = 1;
  8575   function _getDocumentIdentifier(doc) {
  8576     return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
  8577   }
  8579   return function(doc, tagName) {
  8580     var key         = _getDocumentIdentifier(doc) + ":" + tagName,
  8581         cacheEntry  = LIVE_CACHE[key];
  8582     if (!cacheEntry) {
  8583       cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
  8584     }
  8586     return cacheEntry.length > 0;
  8587   };
  8588 })();
  8590 wysihtml.dom.insert = function(elementToInsert) {
  8591   return {
  8592     after: function(element) {
  8593       element.parentNode.insertBefore(elementToInsert, element.nextSibling);
  8594     },
  8596     before: function(element) {
  8597       element.parentNode.insertBefore(elementToInsert, element);
  8598     },
  8600     into: function(element) {
  8601       element.appendChild(elementToInsert);
  8602     }
  8603   };
  8604 };
  8606 wysihtml.dom.insertCSS = function(rules) {
  8607   rules = rules.join("\n");
  8609   return {
  8610     into: function(doc) {
  8611       var styleElement = doc.createElement("style");
  8612       styleElement.type = "text/css";
  8614       if (styleElement.styleSheet) {
  8615         styleElement.styleSheet.cssText = rules;
  8616       } else {
  8617         styleElement.appendChild(doc.createTextNode(rules));
  8618       }
  8620       var link = doc.querySelector("head link");
  8621       if (link) {
  8622         link.parentNode.insertBefore(styleElement, link);
  8623         return;
  8624       } else {
  8625         var head = doc.querySelector("head");
  8626         if (head) {
  8627           head.appendChild(styleElement);
  8628         }
  8629       }
  8630     }
  8631   };
  8632 };
  8634 /**
  8635    * Check whether the given node is a proper loaded image
  8636    * FIXME: Returns undefined when unknown (Chrome, Safari)
  8637 */
  8639 wysihtml.dom.isLoadedImage = function (node) {
  8640   try {
  8641     return node.complete && !node.mozMatchesSelector(":-moz-broken");
  8642   } catch(e) {
  8643     if (node.complete && node.readyState === "complete") {
  8644       return true;
  8645     }
  8646   }
  8647 };
  8649 // TODO: Refactor dom tree traversing here
  8650 (function(wysihtml) {
  8651   wysihtml.dom.lineBreaks = function(node) {
  8653     function _isLineBreak(n) {
  8654       return n.nodeName === "BR";
  8655     }
  8657     /**
  8658      * Checks whether the elment causes a visual line break
  8659      * (<br> or block elements)
  8660      */
  8661     function _isLineBreakOrBlockElement(element) {
  8662       if (_isLineBreak(element)) {
  8663         return true;
  8664       }
  8666       if (wysihtml.dom.getStyle("display").from(element) === "block") {
  8667         return true;
  8668       }
  8670       return false;
  8671     }
  8673     return {
  8675       /* wysihtml.dom.lineBreaks(element).add();
  8676        *
  8677        * Adds line breaks before and after the given node if the previous and next siblings
  8678        * aren't already causing a visual line break (block element or <br>)
  8679        */
  8680       add: function(options) {
  8681         var doc             = node.ownerDocument,
  8682           nextSibling     = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
  8683           previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
  8685         if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
  8686           wysihtml.dom.insert(doc.createElement("br")).after(node);
  8687         }
  8688         if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
  8689           wysihtml.dom.insert(doc.createElement("br")).before(node);
  8690         }
  8691       },
  8693       /* wysihtml.dom.lineBreaks(element).remove();
  8694        *
  8695        * Removes line breaks before and after the given node
  8696        */
  8697       remove: function(options) {
  8698         var nextSibling     = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
  8699             previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
  8701         if (nextSibling && _isLineBreak(nextSibling)) {
  8702           nextSibling.parentNode.removeChild(nextSibling);
  8703         }
  8704         if (previousSibling && _isLineBreak(previousSibling)) {
  8705           previousSibling.parentNode.removeChild(previousSibling);
  8706         }
  8707       }
  8708     };
  8709   };
  8710 })(wysihtml);
  8711 /**
  8712  * Method to set dom events
  8713  *
  8714  * @example
  8715  *    wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
  8716  */
  8717 wysihtml.dom.observe = function(element, eventNames, handler) {
  8718   eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
  8720   var handlerWrapper,
  8721       eventName,
  8722       i       = 0,
  8723       length  = eventNames.length;
  8725   for (; i<length; i++) {
  8726     eventName = eventNames[i];
  8727     if (element.addEventListener) {
  8728       element.addEventListener(eventName, handler, false);
  8729     } else {
  8730       handlerWrapper = function(event) {
  8731         if (!("target" in event)) {
  8732           event.target = event.srcElement;
  8733         }
  8734         event.preventDefault = event.preventDefault || function() {
  8735           this.returnValue = false;
  8736         };
  8737         event.stopPropagation = event.stopPropagation || function() {
  8738           this.cancelBubble = true;
  8739         };
  8740         handler.call(element, event);
  8741       };
  8742       element.attachEvent("on" + eventName, handlerWrapper);
  8743     }
  8744   }
  8746   return {
  8747     stop: function() {
  8748       var eventName,
  8749           i       = 0,
  8750           length  = eventNames.length;
  8751       for (; i<length; i++) {
  8752         eventName = eventNames[i];
  8753         if (element.removeEventListener) {
  8754           element.removeEventListener(eventName, handler, false);
  8755         } else {
  8756           element.detachEvent("on" + eventName, handlerWrapper);
  8757         }
  8758       }
  8759     }
  8760   };
  8761 };
  8763 /**
  8764  * HTML Sanitizer
  8765  * Rewrites the HTML based on given rules
  8766  *
  8767  * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
  8768  * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
  8769  *    be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
  8770  *    desired substitution.
  8771  * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
  8772  *
  8773  * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
  8774  *
  8775  * @example
  8776  *    var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
  8777  *    wysihtml.dom.parse(userHTML, {
  8778  *      tags {
  8779  *        p:      "div",      // Rename p tags to div tags
  8780  *        font:   "span"      // Rename font tags to span tags
  8781  *        div:    true,       // Keep them, also possible (same result when passing: "div" or true)
  8782  *        script: undefined   // Remove script elements
  8783  *      }
  8784  *    });
  8785  *    // => <div><div><span>foo bar</span></div></div>
  8786  *
  8787  *    var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
  8788  *    wysihtml.dom.parse(userHTML);
  8789  *    // => '<span><span><span><span>I'm a table!</span></span></span></span>'
  8790  *
  8791  *    var userHTML = '<div>foobar<br>foobar</div>';
  8792  *    wysihtml.dom.parse(userHTML, {
  8793  *      tags: {
  8794  *        div: undefined,
  8795  *        br:  true
  8796  *      }
  8797  *    });
  8798  *    // => ''
  8799  *
  8800  *    var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
  8801  *    wysihtml.dom.parse(userHTML, {
  8802  *      classes: {
  8803  *        red:    1,
  8804  *        green:  1
  8805  *      },
  8806  *      tags: {
  8807  *        div: {
  8808  *          rename_tag:     "p"
  8809  *        }
  8810  *      }
  8811  *    });
  8812  *    // => '<p class="red">foo</p><p>bar</p>'
  8813  */
  8815 wysihtml.dom.parse = function(elementOrHtml_current, config_current) {
  8816   /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
  8817    * Refactor whole code as this method while workind is kind of awkward too */
  8819   /**
  8820    * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
  8821    * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
  8822    * node isn't closed
  8823    *
  8824    * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
  8825    */
  8826   var NODE_TYPE_MAPPING = {
  8827         "1": _handleElement,
  8828         "3": _handleText,
  8829         "8": _handleComment
  8830       },
  8831       // Rename unknown tags to this
  8832       DEFAULT_NODE_NAME   = "span",
  8833       WHITE_SPACE_REG_EXP = /\s+/,
  8834       defaultRules        = { tags: {}, classes: {} },
  8835       currentRules        = {},
  8836       blockElements       = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" ,
  8837                              "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU",
  8838                              "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"];
  8840   /**
  8841    * Iterates over all childs of the element, recreates them, appends them into a document fragment
  8842    * which later replaces the entire body content
  8843    */
  8844    function parse(elementOrHtml, config) {
  8845     wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
  8847     var context       = config.context || elementOrHtml.ownerDocument || document,
  8848         fragment      = context.createDocumentFragment(),
  8849         isString      = typeof(elementOrHtml) === "string",
  8850         clearInternals = false,
  8851         element,
  8852         newNode,
  8853         firstChild;
  8855     if (config.clearInternals === true) {
  8856       clearInternals = true;
  8857     }
  8859     if (isString) {
  8860       element = wysihtml.dom.getAsDom(elementOrHtml, context);
  8861     } else {
  8862       element = elementOrHtml;
  8863     }
  8865     if (currentRules.selectors) {
  8866       _applySelectorRules(element, currentRules.selectors);
  8867     }
  8869     while (element.firstChild) {
  8870       firstChild = element.firstChild;
  8871       newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
  8872       if (newNode) {
  8873         fragment.appendChild(newNode);
  8874       }
  8875       if (firstChild !== newNode) {
  8876         element.removeChild(firstChild);
  8877       }
  8878     }
  8880     if (config.unjoinNbsps) {
  8881       // replace joined non-breakable spaces with unjoined
  8882       var txtnodes = wysihtml.dom.getTextNodes(fragment);
  8883       for (var n = txtnodes.length; n--;) {
  8884         txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
  8885       }
  8886     }
  8888     // Clear element contents
  8889     element.innerHTML = "";
  8891     // Insert new DOM tree
  8892     element.appendChild(fragment);
  8894     return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element;
  8895   }
  8897   function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
  8898     var oldNodeType     = oldNode.nodeType,
  8899         oldChilds       = oldNode.childNodes,
  8900         oldChildsLength = oldChilds.length,
  8901         method          = NODE_TYPE_MAPPING[oldNodeType],
  8902         i               = 0,
  8903         fragment,
  8904         newNode,
  8905         newChild,
  8906         nodeDisplay;
  8908     // Passes directly elemets with uneditable class
  8909     if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) {
  8910         return oldNode;
  8911     }
  8913     newNode = method && method(oldNode, clearInternals);
  8915     // Remove or unwrap node in case of return value null or false
  8916     if (!newNode) {
  8917         if (newNode === false) {
  8918             // false defines that tag should be removed but contents should remain (unwrap)
  8919             fragment = oldNode.ownerDocument.createDocumentFragment();
  8921             for (i = oldChildsLength; i--;) {
  8922               if (oldChilds[i]) {
  8923                 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
  8924                 if (newChild) {
  8925                   if (oldChilds[i] === newChild) {
  8926                     i--;
  8927                   }
  8928                   fragment.insertBefore(newChild, fragment.firstChild);
  8929                 }
  8930               }
  8931             }
  8933             nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode);
  8935             if (nodeDisplay === '') {
  8936               // Handle display style when element not in dom
  8937               nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : "";
  8938             }
  8939             if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) {
  8940               fragment.appendChild(oldNode.ownerDocument.createElement("br"));
  8941             }
  8943             // TODO: try to minimize surplus spaces
  8944             if (wysihtml.lang.array([
  8945                 "div", "pre", "p",
  8946                 "table", "td", "th",
  8947                 "ul", "ol", "li",
  8948                 "dd", "dl",
  8949                 "footer", "header", "section",
  8950                 "h1", "h2", "h3", "h4", "h5", "h6"
  8951             ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
  8952                 // add space at first when unwraping non-textflow elements
  8953                 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
  8954                   fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
  8955                 }
  8956             }
  8958             if (fragment.normalize) {
  8959               fragment.normalize();
  8960             }
  8961             return fragment;
  8962         } else {
  8963           // Remove
  8964           return null;
  8965         }
  8966     }
  8968     // Converts all childnodes
  8969     for (i=0; i<oldChildsLength; i++) {
  8970       if (oldChilds[i]) {
  8971         newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
  8972         if (newChild) {
  8973           if (oldChilds[i] === newChild) {
  8974             i--;
  8975           }
  8976           newNode.appendChild(newChild);
  8977         }
  8978       }
  8979     }
  8981     // Cleanup senseless <span> elements
  8982     if (cleanUp &&
  8983         newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
  8984         (!newNode.childNodes.length ||
  8985          ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
  8986          !newNode.attributes.length)
  8987         ) {
  8988       fragment = newNode.ownerDocument.createDocumentFragment();
  8989       while (newNode.firstChild) {
  8990         fragment.appendChild(newNode.firstChild);
  8991       }
  8992       if (fragment.normalize) {
  8993         fragment.normalize();
  8994       }
  8995       return fragment;
  8996     }
  8998     if (newNode.normalize) {
  8999       newNode.normalize();
  9000     }
  9001     return newNode;
  9002   }
  9004   function _applySelectorRules (element, selectorRules) {
  9005     var sel, method, els;
  9007     for (sel in selectorRules) {
  9008       if (selectorRules.hasOwnProperty(sel)) {
  9009         if (wysihtml.lang.object(selectorRules[sel]).isFunction()) {
  9010           method = selectorRules[sel];
  9011         } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
  9012           method = elementHandlingMethods[selectorRules[sel]];
  9013         }
  9014         els = element.querySelectorAll(sel);
  9015         for (var i = els.length; i--;) {
  9016           method(els[i]);
  9017         }
  9018       }
  9019     }
  9020   }
  9022   function _handleElement(oldNode, clearInternals) {
  9023     var rule,
  9024         newNode,
  9025         tagRules    = currentRules.tags,
  9026         nodeName    = oldNode.nodeName.toLowerCase(),
  9027         scopeName   = oldNode.scopeName,
  9028         renameTag;
  9030     /**
  9031      * We already parsed that element
  9032      * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
  9033      */
  9034     if (oldNode._wysihtml) {
  9035       return null;
  9036     }
  9037     oldNode._wysihtml = 1;
  9039     if (oldNode.className === "wysihtml-temp") {
  9040       return null;
  9041     }
  9043     /**
  9044      * IE is the only browser who doesn't include the namespace in the
  9045      * nodeName, that's why we have to prepend it by ourselves
  9046      * scopeName is a proprietary IE feature
  9047      * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
  9048      */
  9049     if (scopeName && scopeName != "HTML") {
  9050       nodeName = scopeName + ":" + nodeName;
  9051     }
  9052     /**
  9053      * Repair node
  9054      * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
  9055      * 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
  9056      */
  9057     if ("outerHTML" in oldNode) {
  9058       if (!wysihtml.browser.autoClosesUnclosedTags() &&
  9059           oldNode.nodeName === "P" &&
  9060           oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
  9061         nodeName = "div";
  9062       }
  9063     }
  9065     if (nodeName in tagRules) {
  9066       rule = tagRules[nodeName];
  9067       if (!rule || rule.remove) {
  9068         return null;
  9069       } else if (rule.unwrap) {
  9070         return false;
  9071       }
  9072       rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
  9073     } else if (oldNode.firstChild) {
  9074       rule = { rename_tag: DEFAULT_NODE_NAME };
  9075     } else {
  9076       // Remove empty unknown elements
  9077       return null;
  9078     }
  9080     // tests if type condition is met or node should be removed/unwrapped/renamed
  9081     if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
  9082       if (rule.remove_action) {
  9083         if (rule.remove_action === "unwrap") {
  9084           return false;
  9085         } else if (rule.remove_action === "rename") {
  9086           renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
  9087         } else {
  9088           return null;
  9089         }
  9090       } else {
  9091         return null;
  9092       }
  9093     }
  9095     newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
  9096     _handleAttributes(oldNode, newNode, rule, clearInternals);
  9097     _handleStyles(oldNode, newNode, rule);
  9099     oldNode = null;
  9101     if (newNode.normalize) { newNode.normalize(); }
  9102     return newNode;
  9103   }
  9105   function _testTypes(oldNode, rules, types, clearInternals) {
  9106     var definition, type;
  9108     // do not interfere with placeholder span or pasting caret position is not maintained
  9109     if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
  9110       return true;
  9111     }
  9113     for (type in types) {
  9114       if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
  9115         definition = rules.type_definitions[type];
  9116         if (_testType(oldNode, definition)) {
  9117           return true;
  9118         }
  9119       }
  9120     }
  9121     return false;
  9122   }
  9124   function array_contains(a, obj) {
  9125       var i = a.length;
  9126       while (i--) {
  9127          if (a[i] === obj) {
  9128              return true;
  9129          }
  9130       }
  9131       return false;
  9132   }
  9134   function _testType(oldNode, definition) {
  9136     var nodeClasses = oldNode.getAttribute("class"),
  9137         nodeStyles =  oldNode.getAttribute("style"),
  9138         classesLength, s, s_corrected, a, attr, currentClass, styleProp;
  9140     // test for methods
  9141     if (definition.methods) {
  9142       for (var m in definition.methods) {
  9143         if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
  9145           if (typeCeckMethods[m](oldNode)) {
  9146             return true;
  9147           }
  9148         }
  9149       }
  9150     }
  9152     // test for classes, if one found return true
  9153     if (nodeClasses && definition.classes) {
  9154       nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
  9155       classesLength = nodeClasses.length;
  9156       for (var i = 0; i < classesLength; i++) {
  9157         if (definition.classes[nodeClasses[i]]) {
  9158           return true;
  9159         }
  9160       }
  9161     }
  9163     // test for styles, if one found return true
  9164     if (nodeStyles && definition.styles) {
  9166       nodeStyles = nodeStyles.split(';');
  9167       for (s in definition.styles) {
  9168         if (definition.styles.hasOwnProperty(s)) {
  9169           for (var sp = nodeStyles.length; sp--;) {
  9170             styleProp = nodeStyles[sp].split(':');
  9172             if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
  9173               if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
  9174                 return true;
  9175               }
  9176             }
  9177           }
  9178         }
  9179       }
  9180     }
  9182     // test for attributes in general against regex match
  9183     if (definition.attrs) {
  9184         for (a in definition.attrs) {
  9185             if (definition.attrs.hasOwnProperty(a)) {
  9186                 attr = wysihtml.dom.getAttribute(oldNode, a);
  9187                 if (typeof(attr) === "string") {
  9188                     if (attr.search(definition.attrs[a]) > -1) {
  9189                         return true;
  9190                     }
  9191                 }
  9192             }
  9193         }
  9194     }
  9195     return false;
  9196   }
  9198   function _handleStyles(oldNode, newNode, rule) {
  9199     var s, v;
  9200     if(rule && rule.keep_styles) {
  9201       for (s in rule.keep_styles) {
  9202         if (rule.keep_styles.hasOwnProperty(s)) {
  9203           v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
  9204           // value can be regex and if so should match or style skipped
  9205           if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
  9206             continue;
  9207           }
  9208           if (s === "float") {
  9209             // IE compability
  9210             newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
  9211            } else if (oldNode.style[s]) {
  9212              newNode.style[s] = v;
  9213            }
  9214         }
  9215       }
  9216     }
  9217   };
  9219   function _getAttributesBeginningWith(beginning, attributes) {
  9220     var returnAttributes = [];
  9221     for (var attr in attributes) {
  9222       if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
  9223         returnAttributes.push(attr);
  9224       }
  9225     }
  9226     return returnAttributes;
  9227   }
  9229   function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
  9230     var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName],
  9231         newAttributeValue;
  9233     if (method) {
  9234       newAttributeValue = method(attributeValue, nodeName);
  9235       if (typeof(newAttributeValue) === "string") {
  9236         return newAttributeValue;
  9237       }
  9238     }
  9240     return false;
  9241   }
  9243   function _checkAttributes(oldNode, local_attributes) {
  9244     var globalAttributes  = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
  9245         checkAttributes   = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(),
  9246         attributes        = {},
  9247         oldAttributes     = wysihtml.dom.getAttributes(oldNode),
  9248         attributeName, newValue, matchingAttributes;
  9250     for (attributeName in checkAttributes) {
  9251       if ((/\*$/).test(attributeName)) {
  9253         matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
  9254         for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
  9256           newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
  9257           if (newValue !== false) {
  9258             attributes[matchingAttributes[i]] = newValue;
  9259           }
  9260         }
  9261       } else {
  9262         newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
  9263         if (newValue !== false) {
  9264           attributes[attributeName] = newValue;
  9265         }
  9266       }
  9267     }
  9269     return attributes;
  9270   }
  9272   // TODO: refactor. Too long to read
  9273   function _handleAttributes(oldNode, newNode, rule, clearInternals) {
  9274     var attributes          = {},                         // fresh new set of attributes to set on newNode
  9275         setClass            = rule.set_class,             // classes to set
  9276         addClass            = rule.add_class,             // add classes based on existing attributes
  9277         addStyle            = rule.add_style,             // add styles based on existing attributes
  9278         setAttributes       = rule.set_attributes,        // attributes to set on the current node
  9279         allowedClasses      = currentRules.classes,
  9280         i                   = 0,
  9281         classes             = [],
  9282         styles              = [],
  9283         newClasses          = [],
  9284         oldClasses          = [],
  9285         classesLength,
  9286         newClassesLength,
  9287         currentClass,
  9288         newClass,
  9289         attributeName,
  9290         method;
  9292     if (setAttributes) {
  9293       attributes = wysihtml.lang.object(setAttributes).clone();
  9294     }
  9296     // check/convert values of attributes
  9297     attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode,  rule.check_attributes)).get();
  9299     if (setClass) {
  9300       classes.push(setClass);
  9301     }
  9303     if (addClass) {
  9304       for (attributeName in addClass) {
  9305         method = addClassMethods[addClass[attributeName]];
  9306         if (!method) {
  9307           continue;
  9308         }
  9309         newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName));
  9310         if (typeof(newClass) === "string") {
  9311           classes.push(newClass);
  9312         }
  9313       }
  9314     }
  9316     if (addStyle) {
  9317       for (attributeName in addStyle) {
  9318         method = addStyleMethods[addStyle[attributeName]];
  9319         if (!method) {
  9320           continue;
  9321         }
  9323         newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName));
  9324         if (typeof(newStyle) === "string") {
  9325           styles.push(newStyle);
  9326         }
  9327       }
  9328     }
  9331     if (typeof(allowedClasses) === "string" && allowedClasses === "any") {
  9332       if (oldNode.getAttribute("class")) {
  9333         if (currentRules.classes_blacklist) {
  9334           oldClasses = oldNode.getAttribute("class");
  9335           if (oldClasses) {
  9336             classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
  9337           }
  9339           classesLength = classes.length;
  9340           for (; i<classesLength; i++) {
  9341             currentClass = classes[i];
  9342             if (!currentRules.classes_blacklist[currentClass]) {
  9343               newClasses.push(currentClass);
  9344             }
  9345           }
  9347           if (newClasses.length) {
  9348             attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
  9349           }
  9351         } else {
  9352           attributes["class"] = oldNode.getAttribute("class");
  9353         }
  9354       } else {
  9355         if(classes && classes.length > 0) {
  9356           attributes["class"] = wysihtml.lang.array(classes).unique().join(" ");
  9357         }
  9358       }
  9359     } else {
  9360       // make sure that wysihtml temp class doesn't get stripped out
  9361       if (!clearInternals) {
  9362         allowedClasses["_wysihtml-temp-placeholder"] = 1;
  9363         allowedClasses["_rangySelectionBoundary"] = 1;
  9364         allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
  9365       }
  9367       // add old classes last
  9368       oldClasses = oldNode.getAttribute("class");
  9369       if (oldClasses) {
  9370         classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
  9371       }
  9372       classesLength = classes.length;
  9373       for (; i<classesLength; i++) {
  9374         currentClass = classes[i];
  9375         if (allowedClasses[currentClass]) {
  9376           newClasses.push(currentClass);
  9377         }
  9378       }
  9380       if (newClasses.length) {
  9381         attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
  9382       }
  9383     }
  9385     // remove table selection class if present
  9386     if (attributes["class"] && clearInternals) {
  9387       attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", "");
  9388       if ((/^\s*$/g).test(attributes["class"])) {
  9389         delete attributes["class"];
  9390       }
  9391     }
  9393     if (styles.length) {
  9394       attributes["style"] = wysihtml.lang.array(styles).unique().join(" ");
  9395     }
  9397     // set attributes on newNode
  9398     for (attributeName in attributes) {
  9399       // Setting attributes can cause a js error in IE under certain circumstances
  9400       // eg. on a <img> under https when it's new attribute value is non-https
  9401       // TODO: Investigate this further and check for smarter handling
  9402       try {
  9403         newNode.setAttribute(attributeName, attributes[attributeName]);
  9404       } catch(e) {}
  9405     }
  9407     // IE8 sometimes loses the width/height attributes when those are set before the "src"
  9408     // so we make sure to set them again
  9409     if (attributes.src) {
  9410       if (typeof(attributes.width) !== "undefined") {
  9411         newNode.setAttribute("width", attributes.width);
  9412       }
  9413       if (typeof(attributes.height) !== "undefined") {
  9414         newNode.setAttribute("height", attributes.height);
  9415       }
  9416     }
  9417   }
  9419   function _handleText(oldNode) {
  9420     var nextSibling = oldNode.nextSibling;
  9421     if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) {
  9422       // Concatenate text nodes
  9423       nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
  9424     } else {
  9425       // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
  9426       var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
  9427       return oldNode.ownerDocument.createTextNode(data);
  9428     }
  9429   }
  9431   function _handleComment(oldNode) {
  9432     if (currentRules.comments) {
  9433       return oldNode.ownerDocument.createComment(oldNode.nodeValue);
  9434     }
  9435   }
  9437   // ------------ attribute checks ------------ \\
  9438   var attributeCheckMethods = {
  9439     url: (function() {
  9440       var REG_EXP = /^https?:\/\//i;
  9441       return function(attributeValue) {
  9442         if (!attributeValue || !attributeValue.match(REG_EXP)) {
  9443           return null;
  9444         }
  9445         return attributeValue.replace(REG_EXP, function(match) {
  9446           return match.toLowerCase();
  9447         });
  9448       };
  9449     })(),
  9451     src: (function() {
  9452       var REG_EXP = /^(\/|https?:\/\/)/i;
  9453       return function(attributeValue) {
  9454         if (!attributeValue || !attributeValue.match(REG_EXP)) {
  9455           return null;
  9456         }
  9457         return attributeValue.replace(REG_EXP, function(match) {
  9458           return match.toLowerCase();
  9459         });
  9460       };
  9461     })(),
  9463     href: (function() {
  9464       var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i;
  9465       return function(attributeValue) {
  9466         if (!attributeValue || !attributeValue.match(REG_EXP)) {
  9467           return null;
  9468         }
  9469         return attributeValue.replace(REG_EXP, function(match) {
  9470           return match.toLowerCase();
  9471         });
  9472       };
  9473     })(),
  9475     alt: (function() {
  9476       var REG_EXP = /[^ a-z0-9_\-]/gi;
  9477       return function(attributeValue, nodeName) {
  9478         if (!attributeValue) {
  9479           if (nodeName === "IMG") {
  9480             return "";
  9481           } else {
  9482             return null;
  9483           }
  9484         }
  9485         return attributeValue.replace(REG_EXP, "");
  9486       };
  9487     })(),
  9489     // Integers. Does not work with floating point numbers and units
  9490     numbers: (function() {
  9491       var REG_EXP = /\D/g;
  9492       return function(attributeValue) {
  9493         attributeValue = (attributeValue || "").replace(REG_EXP, "");
  9494         return attributeValue || null;
  9495       };
  9496     })(),
  9498     // Useful for with/height attributes where floating points and percentages are allowed
  9499     dimension: (function() {
  9500       var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/;
  9501       return function(attributeValue) {
  9502         attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3");
  9503         return attributeValue || null;
  9504       };
  9505     })(),
  9507     any: (function() {
  9508       return function(attributeValue) {
  9509         if (!attributeValue) {
  9510           return null;
  9511         }
  9512         return attributeValue;
  9513       };
  9514     })()
  9515   };
  9517   // ------------ style converter (converts an html attribute to a style) ------------ \\
  9518   var addStyleMethods = {
  9519     align_text: (function() {
  9520       var mapping = {
  9521         left:     "text-align: left;",
  9522         right:    "text-align: right;",
  9523         center:   "text-align: center;"
  9524       };
  9525       return function(attributeValue) {
  9526         return mapping[String(attributeValue).toLowerCase()];
  9527       };
  9528     })(),
  9529   };
  9531   // ------------ class converter (converts an html attribute to a class name) ------------ \\
  9532   var addClassMethods = {
  9533     align_img: (function() {
  9534       var mapping = {
  9535         left:   "wysiwyg-float-left",
  9536         right:  "wysiwyg-float-right"
  9537       };
  9538       return function(attributeValue) {
  9539         return mapping[String(attributeValue).toLowerCase()];
  9540       };
  9541     })(),
  9543     align_text: (function() {
  9544       var mapping = {
  9545         left:     "wysiwyg-text-align-left",
  9546         right:    "wysiwyg-text-align-right",
  9547         center:   "wysiwyg-text-align-center",
  9548         justify:  "wysiwyg-text-align-justify"
  9549       };
  9550       return function(attributeValue) {
  9551         return mapping[String(attributeValue).toLowerCase()];
  9552       };
  9553     })(),
  9555     clear_br: (function() {
  9556       var mapping = {
  9557         left:   "wysiwyg-clear-left",
  9558         right:  "wysiwyg-clear-right",
  9559         both:   "wysiwyg-clear-both",
  9560         all:    "wysiwyg-clear-both"
  9561       };
  9562       return function(attributeValue) {
  9563         return mapping[String(attributeValue).toLowerCase()];
  9564       };
  9565     })(),
  9567     size_font: (function() {
  9568       var mapping = {
  9569         "1": "wysiwyg-font-size-xx-small",
  9570         "2": "wysiwyg-font-size-small",
  9571         "3": "wysiwyg-font-size-medium",
  9572         "4": "wysiwyg-font-size-large",
  9573         "5": "wysiwyg-font-size-x-large",
  9574         "6": "wysiwyg-font-size-xx-large",
  9575         "7": "wysiwyg-font-size-xx-large",
  9576         "-": "wysiwyg-font-size-smaller",
  9577         "+": "wysiwyg-font-size-larger"
  9578       };
  9579       return function(attributeValue) {
  9580         return mapping[String(attributeValue).charAt(0)];
  9581       };
  9582     })()
  9583   };
  9585   // checks if element is possibly visible
  9586   var typeCeckMethods = {
  9587     has_visible_contet: (function() {
  9588       var txt,
  9589           isVisible = false,
  9590           visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
  9591                              'style', 'table', 'iframe', 'object', 'embed', 'audio',
  9592                              'svg', 'input', 'button', 'select','textarea', 'canvas'];
  9594       return function(el) {
  9596         // has visible innertext. so is visible
  9597         txt = (el.innerText || el.textContent).replace(/\s/g, '');
  9598         if (txt && txt.length > 0) {
  9599           return true;
  9600         }
  9602         // matches list of visible dimensioned elements
  9603         for (var i = visibleElements.length; i--;) {
  9604           if (el.querySelector(visibleElements[i])) {
  9605             return true;
  9606           }
  9607         }
  9609         // try to measure dimesions in last resort. (can find only of elements in dom)
  9610         if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
  9611           return true;
  9612         }
  9614         return false;
  9615       };
  9616     })()
  9617   };
  9619   var elementHandlingMethods = {
  9620     unwrap: function (element) {
  9621       wysihtml.dom.unwrap(element);
  9622     },
  9624     remove: function (element) {
  9625       element.parentNode.removeChild(element);
  9626     }
  9627   };
  9629   return parse(elementOrHtml_current, config_current);
  9630 };
  9632 // does a selector query on element or array of elements
  9633 wysihtml.dom.query = function(elements, query) {
  9634     var ret = [],
  9635         q;
  9637     if (elements.nodeType) {
  9638         elements = [elements];
  9639     }
  9641     for (var e = 0, len = elements.length; e < len; e++) {
  9642         q = elements[e].querySelectorAll(query);
  9643         if (q) {
  9644             for(var i = q.length; i--; ret.unshift(q[i]));
  9645         }
  9646     }
  9647     return ret;
  9648 };
  9650 /**
  9651  * Checks for empty text node childs and removes them
  9652  *
  9653  * @param {Element} node The element in which to cleanup
  9654  * @example
  9655  *    wysihtml.dom.removeEmptyTextNodes(element);
  9656  */
  9657 wysihtml.dom.removeEmptyTextNodes = function(node) {
  9658   var childNode,
  9659       childNodes        = wysihtml.lang.array(node.childNodes).get(),
  9660       childNodesLength  = childNodes.length,
  9661       i                 = 0;
  9663   for (; i<childNodesLength; i++) {
  9664     childNode = childNodes[i];
  9665     if (childNode.nodeType === wysihtml.TEXT_NODE && (/^[\n\r]*$/).test(childNode.data)) {
  9666       childNode.parentNode.removeChild(childNode);
  9667     }
  9668   }
  9669 };
  9671 wysihtml.dom.removeInvisibleSpaces = function(node) {
  9672   var textNodes = wysihtml.dom.getTextNodes(node);
  9673   for (var n = textNodes.length; n--;) {
  9674     textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
  9675   }
  9676 };
  9678 /**
  9679  * Renames an element (eg. a <div> to a <p>) and keeps its childs
  9680  *
  9681  * @param {Element} element The list element which should be renamed
  9682  * @param {Element} newNodeName The desired tag name
  9683  *
  9684  * @example
  9685  *    <!-- Assume the following dom: -->
  9686  *    <ul id="list">
  9687  *      <li>eminem</li>
  9688  *      <li>dr. dre</li>
  9689  *      <li>50 Cent</li>
  9690  *    </ul>
  9691  *
  9692  *    <script>
  9693  *      wysihtml.dom.renameElement(document.getElementById("list"), "ol");
  9694  *    </script>
  9695  *
  9696  *    <!-- Will result in: -->
  9697  *    <ol>
  9698  *      <li>eminem</li>
  9699  *      <li>dr. dre</li>
  9700  *      <li>50 Cent</li>
  9701  *    </ol>
  9702  */
  9703 wysihtml.dom.renameElement = function(element, newNodeName) {
  9704   var newElement = element.ownerDocument.createElement(newNodeName),
  9705       firstChild;
  9706   while (firstChild = element.firstChild) {
  9707     newElement.appendChild(firstChild);
  9708   }
  9709   wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
  9711   if (element.parentNode) {
  9712     element.parentNode.replaceChild(newElement, element);
  9713   }
  9715   return newElement;
  9716 };
  9718 /**
  9719  * Takes an element, removes it and replaces it with it's childs
  9720  *
  9721  * @param {Object} node The node which to replace with it's child nodes
  9722  * @example
  9723  *    <div id="foo">
  9724  *      <span>hello</span>
  9725  *    </div>
  9726  *    <script>
  9727  *      // Remove #foo and replace with it's children
  9728  *      wysihtml.dom.replaceWithChildNodes(document.getElementById("foo"));
  9729  *    </script>
  9730  */
  9731 wysihtml.dom.replaceWithChildNodes = function(node) {
  9732   if (!node.parentNode) {
  9733     return;
  9734   }
  9736   while (node.firstChild) {
  9737     node.parentNode.insertBefore(node.firstChild, node);
  9738   }
  9739   node.parentNode.removeChild(node);
  9740 };
  9742 /**
  9743  * Unwraps an unordered/ordered list
  9744  *
  9745  * @param {Element} element The list element which should be unwrapped
  9746  *
  9747  * @example
  9748  *    <!-- Assume the following dom: -->
  9749  *    <ul id="list">
  9750  *      <li>eminem</li>
  9751  *      <li>dr. dre</li>
  9752  *      <li>50 Cent</li>
  9753  *    </ul>
  9754  *
  9755  *    <script>
  9756  *      wysihtml.dom.resolveList(document.getElementById("list"));
  9757  *    </script>
  9758  *
  9759  *    <!-- Will result in: -->
  9760  *    eminem<br>
  9761  *    dr. dre<br>
  9762  *    50 Cent<br>
  9763  */
  9764 (function(dom) {
  9765   function _isBlockElement(node) {
  9766     return dom.getStyle("display").from(node) === "block";
  9767   }
  9769   function _isLineBreak(node) {
  9770     return node.nodeName === "BR";
  9771   }
  9773   function _appendLineBreak(element) {
  9774     var lineBreak = element.ownerDocument.createElement("br");
  9775     element.appendChild(lineBreak);
  9776   }
  9778   function resolveList(list, useLineBreaks) {
  9779     if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
  9780       return;
  9781     }
  9783     var doc             = list.ownerDocument,
  9784         fragment        = doc.createDocumentFragment(),
  9785         previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}),
  9786         nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}),
  9787         firstChild,
  9788         lastChild,
  9789         isLastChild,
  9790         shouldAppendLineBreak,
  9791         paragraph,
  9792         listItem,
  9793         lastListItem = list.lastElementChild || list.lastChild,
  9794         isLastItem;
  9796     if (useLineBreaks) {
  9797       // Insert line break if list is after a non-block element
  9798       if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
  9799         _appendLineBreak(fragment);
  9800       }
  9802       while (listItem = (list.firstElementChild || list.firstChild)) {
  9803         lastChild = listItem.lastChild;
  9804         isLastItem = listItem === lastListItem;
  9805         while (firstChild = listItem.firstChild) {
  9806           isLastChild           = firstChild === lastChild;
  9807           // This needs to be done before appending it to the fragment, as it otherwise will lose style information
  9808           shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
  9809           fragment.appendChild(firstChild);
  9810           if (shouldAppendLineBreak) {
  9811             _appendLineBreak(fragment);
  9812           }
  9813         }
  9815         listItem.parentNode.removeChild(listItem);
  9816       }
  9817     } else {
  9818       while (listItem = (list.firstElementChild || list.firstChild)) {
  9819         if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
  9820           while (firstChild = listItem.firstChild) {
  9821             fragment.appendChild(firstChild);
  9822           }
  9823         } else {
  9824           paragraph = doc.createElement("p");
  9825           while (firstChild = listItem.firstChild) {
  9826             paragraph.appendChild(firstChild);
  9827           }
  9828           fragment.appendChild(paragraph);
  9829         }
  9830         listItem.parentNode.removeChild(listItem);
  9831       }
  9832     }
  9834     list.parentNode.replaceChild(fragment, list);
  9835   }
  9837   dom.resolveList = resolveList;
  9838 })(wysihtml.dom);
  9840 /**
  9841  * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
  9842  *
  9843  * Browser Compatibility:
  9844  *  - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
  9845  *  - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
  9846  *
  9847  * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
  9848  *    - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
  9849  *    - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
  9850  *    - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
  9851  *    - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
  9852  *      can do anything as if the sandbox attribute wasn't set
  9853  *
  9854  * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
  9855  * @param {Object} [config] Optional parameters
  9856  *
  9857  * @example
  9858  *    new wysihtml.dom.Sandbox(function(sandbox) {
  9859  *      sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
  9860  *    });
  9861  */
  9862 (function(wysihtml) {
  9863   var /**
  9864        * Default configuration
  9865        */
  9866       doc                 = document,
  9867       /**
  9868        * Properties to unset/protect on the window object
  9869        */
  9870       windowProperties    = [
  9871         "parent", "top", "opener", "frameElement", "frames",
  9872         "localStorage", "globalStorage", "sessionStorage", "indexedDB"
  9873       ],
  9874       /**
  9875        * Properties on the window object which are set to an empty function
  9876        */
  9877       windowProperties2   = [
  9878         "open", "close", "openDialog", "showModalDialog",
  9879         "alert", "confirm", "prompt",
  9880         "openDatabase", "postMessage",
  9881         "XMLHttpRequest", "XDomainRequest"
  9882       ],
  9883       /**
  9884        * Properties to unset/protect on the document object
  9885        */
  9886       documentProperties  = [
  9887         "referrer",
  9888         "write", "open", "close"
  9889       ];
  9891   wysihtml.dom.Sandbox = Base.extend(
  9892     /** @scope wysihtml.dom.Sandbox.prototype */ {
  9894     constructor: function(readyCallback, config) {
  9895       this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
  9896       this.config   = wysihtml.lang.object({}).merge(config).get();
  9897       if (!this.config.className) {
  9898         this.config.className = "wysihtml-sandbox";
  9899       }
  9900       this.editableArea   = this._createIframe();
  9901     },
  9903     insertInto: function(element) {
  9904       if (typeof(element) === "string") {
  9905         element = doc.getElementById(element);
  9906       }
  9908       element.appendChild(this.editableArea);
  9909     },
  9911     getIframe: function() {
  9912       return this.editableArea;
  9913     },
  9915     getWindow: function() {
  9916       this._readyError();
  9917     },
  9919     getDocument: function() {
  9920       this._readyError();
  9921     },
  9923     destroy: function() {
  9924       var iframe = this.getIframe();
  9925       iframe.parentNode.removeChild(iframe);
  9926     },
  9928     _readyError: function() {
  9929       throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet");
  9930     },
  9932     /**
  9933      * Creates the sandbox iframe
  9934      *
  9935      * Some important notes:
  9936      *  - We can't use HTML5 sandbox for now:
  9937      *    setting it causes that the iframe's dom can't be accessed from the outside
  9938      *    Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
  9939      *    But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
  9940      *    In order to make this happen we need to set the "allow-scripts" flag.
  9941      *    A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
  9942      *  - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
  9943      *  - IE needs to have the security="restricted" attribute set before the iframe is
  9944      *    inserted into the dom tree
  9945      *  - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
  9946      *    though it supports it
  9947      *  - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
  9948      *  - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
  9949      *    on the onreadystatechange event
  9950      */
  9951     _createIframe: function() {
  9952       var that   = this,
  9953           iframe = doc.createElement("iframe");
  9954       iframe.className = this.config.className;
  9955       wysihtml.dom.setAttributes({
  9956         "security":           "restricted",
  9957         "allowtransparency":  "true",
  9958         "frameborder":        0,
  9959         "width":              0,
  9960         "height":             0,
  9961         "marginwidth":        0,
  9962         "marginheight":       0
  9963       }).on(iframe);
  9965       // Setting the src like this prevents ssl warnings in IE6
  9966       if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
  9967         iframe.src = "javascript:'<html></html>'";
  9968       }
  9970       iframe.onload = function() {
  9971         iframe.onreadystatechange = iframe.onload = null;
  9972         that._onLoadIframe(iframe);
  9973       };
  9975       iframe.onreadystatechange = function() {
  9976         if (/loaded|complete/.test(iframe.readyState)) {
  9977           iframe.onreadystatechange = iframe.onload = null;
  9978           that._onLoadIframe(iframe);
  9979         }
  9980       };
  9982       return iframe;
  9983     },
  9985     /**
  9986      * Callback for when the iframe has finished loading
  9987      */
  9988     _onLoadIframe: function(iframe) {
  9989       // don't resume when the iframe got unloaded (eg. by removing it from the dom)
  9990       if (!wysihtml.dom.contains(doc.documentElement, iframe)) {
  9991         return;
  9992       }
  9994       var that           = this,
  9995           iframeWindow   = iframe.contentWindow,
  9996           iframeDocument = iframe.contentWindow.document,
  9997           charset        = doc.characterSet || doc.charset || "utf-8",
  9998           sandboxHtml    = this._getHtml({
  9999             charset:      charset,
 10000             stylesheets:  this.config.stylesheets
 10001           });
 10003       // Create the basic dom tree including proper DOCTYPE and charset
 10004       iframeDocument.open("text/html", "replace");
 10005       iframeDocument.write(sandboxHtml);
 10006       iframeDocument.close();
 10008       this.getWindow = function() { return iframe.contentWindow; };
 10009       this.getDocument = function() { return iframe.contentWindow.document; };
 10011       // Catch js errors and pass them to the parent's onerror event
 10012       // addEventListener("error") doesn't work properly in some browsers
 10013       // TODO: apparently this doesn't work in IE9!
 10014       iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
 10015         throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber);
 10016       };
 10018       if (!wysihtml.browser.supportsSandboxedIframes()) {
 10019         // Unset a bunch of sensitive variables
 10020         // Please note: This isn't hack safe!
 10021         // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
 10022         // IE is secure though, which is the most important thing, since IE is the only browser, who
 10023         // takes over scripts & styles into contentEditable elements when copied from external websites
 10024         // or applications (Microsoft Word, ...)
 10025         var i, length;
 10026         for (i=0, length=windowProperties.length; i<length; i++) {
 10027           this._unset(iframeWindow, windowProperties[i]);
 10028         }
 10029         for (i=0, length=windowProperties2.length; i<length; i++) {
 10030           this._unset(iframeWindow, windowProperties2[i], wysihtml.EMPTY_FUNCTION);
 10031         }
 10032         for (i=0, length=documentProperties.length; i<length; i++) {
 10033           this._unset(iframeDocument, documentProperties[i]);
 10034         }
 10035         // This doesn't work in Safari 5
 10036         // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
 10037         this._unset(iframeDocument, "cookie", "", true);
 10038       }
 10040       if (wysihtml.polyfills) {
 10041         wysihtml.polyfills(iframeWindow, iframeDocument).apply();
 10042       }
 10044       this.loaded = true;
 10046       // Trigger the callback
 10047       setTimeout(function() { that.callback(that); }, 0);
 10048     },
 10050     _getHtml: function(templateVars) {
 10051       var stylesheets = templateVars.stylesheets,
 10052           html        = "",
 10053           i           = 0,
 10054           length;
 10055       stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
 10056       if (stylesheets) {
 10057         length = stylesheets.length;
 10058         for (; i<length; i++) {
 10059           html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
 10060         }
 10061       }
 10062       templateVars.stylesheets = html;
 10064       return wysihtml.lang.string(
 10065         '<!DOCTYPE html><html><head>'
 10066         + '<meta charset="#{charset}">#{stylesheets}</head>'
 10067         + '<body></body></html>'
 10068       ).interpolate(templateVars);
 10069     },
 10071     /**
 10072      * Method to unset/override existing variables
 10073      * @example
 10074      *    // Make cookie unreadable and unwritable
 10075      *    this._unset(document, "cookie", "", true);
 10076      */
 10077     _unset: function(object, property, value, setter) {
 10078       try { object[property] = value; } catch(e) {}
 10080       try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
 10081       if (setter) {
 10082         try { object.__defineSetter__(property, function() {}); } catch(e) {}
 10083       }
 10085       if (!wysihtml.browser.crashesWhenDefineProperty(property)) {
 10086         try {
 10087           var config = {
 10088             get: function() { return value; }
 10089           };
 10090           if (setter) {
 10091             config.set = function() {};
 10092           }
 10093           Object.defineProperty(object, property, config);
 10094         } catch(e) {}
 10095       }
 10096     }
 10097   });
 10098 })(wysihtml);
 10100 (function() {
 10101   var mapping = {
 10102     "className": "class"
 10103   };
 10104   wysihtml.dom.setAttributes = function(attributes) {
 10105     return {
 10106       on: function(element) {
 10107         for (var i in attributes) {
 10108           element.setAttribute(mapping[i] || i, attributes[i]);
 10109         }
 10110       }
 10111     };
 10112   };
 10113 })();
 10115 wysihtml.dom.setStyles = function(styles) {
 10116   return {
 10117     on: function(element) {
 10118       var style = element.style;
 10119       if (typeof(styles) === "string") {
 10120         style.cssText += ";" + styles;
 10121         return;
 10122       }
 10123       for (var i in styles) {
 10124         if (i === "float") {
 10125           style.cssFloat = styles[i];
 10126           style.styleFloat = styles[i];
 10127         } else {
 10128           style[i] = styles[i];
 10129         }
 10130       }
 10131     }
 10132   };
 10133 };
 10135 /**
 10136  * Simulate HTML5 placeholder attribute
 10137  *
 10138  * Needed since
 10139  *    - div[contentEditable] elements don't support it
 10140  *    - older browsers (such as IE8 and Firefox 3.6) don't support it at all
 10141  *
 10142  * @param {Object} parent Instance of main wysihtml.Editor class
 10143  * @param {Element} view Instance of wysihtml.views.* class
 10144  * @param {String} placeholderText
 10145  *
 10146  * @example
 10147  *    wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
 10148  */
 10149 (function(dom) {
 10150   dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) {
 10151     var CLASS_NAME = placeholderClassName || "wysihtml-placeholder",
 10152         unset = function() {
 10153           var composerIsVisible   = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
 10154           if (view.hasPlaceholderSet()) {
 10155             view.clear();
 10156             view.element.focus();
 10157             if (composerIsVisible ) {
 10158               setTimeout(function() {
 10159                 var sel = view.selection.getSelection();
 10160                 if (!sel.focusNode || !sel.anchorNode) {
 10161                   view.selection.selectNode(view.element.firstChild || view.element);
 10162                 }
 10163               }, 0);
 10164             }
 10165           }
 10166           view.placeholderSet = false;
 10167           dom.removeClass(view.element, CLASS_NAME);
 10168         },
 10169         set = function() {
 10170           if (view.isEmpty() && !view.placeholderSet) {
 10171             view.placeholderSet = true;
 10172             view.setValue(placeholderText, false);
 10173             dom.addClass(view.element, CLASS_NAME);
 10174           }
 10175         };
 10177     editor
 10178       .on("set_placeholder", set)
 10179       .on("unset_placeholder", unset)
 10180       .on("focus:composer", unset)
 10181       .on("paste:composer", unset)
 10182       .on("blur:composer", set);
 10184     set();
 10185   };
 10186 })(wysihtml.dom);
 10188 (function(dom) {
 10189   var documentElement = document.documentElement;
 10190   if ("textContent" in documentElement) {
 10191     dom.setTextContent = function(element, text) {
 10192       element.textContent = text;
 10193     };
 10195     dom.getTextContent = function(element) {
 10196       return element.textContent;
 10197     };
 10198   } else if ("innerText" in documentElement) {
 10199     dom.setTextContent = function(element, text) {
 10200       element.innerText = text;
 10201     };
 10203     dom.getTextContent = function(element) {
 10204       return element.innerText;
 10205     };
 10206   } else {
 10207     dom.setTextContent = function(element, text) {
 10208       element.nodeValue = text;
 10209     };
 10211     dom.getTextContent = function(element) {
 10212       return element.nodeValue;
 10213     };
 10214   }
 10215 })(wysihtml.dom);
 10217 /* Unwraps element and returns list of childNodes that the node contained.
 10218  *
 10219  * Example:
 10220  *    var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me'));
 10221 */
 10223 wysihtml.dom.unwrap = function(node) {
 10224   var children = [];
 10225   if (node.parentNode) {
 10226     while (node.lastChild) {
 10227       children.unshift(node.lastChild);
 10228       wysihtml.dom.insert(node.lastChild).after(node);
 10229     }
 10230     node.parentNode.removeChild(node);
 10231   }
 10232   return children;
 10233 };
 10235 /**
 10236  * Fix most common html formatting misbehaviors of browsers implementation when inserting
 10237  * content via copy & paste contentEditable
 10238  *
 10239  * @author Christopher Blum
 10240  */
 10241 wysihtml.quirks.cleanPastedHTML = (function() {
 10243   var styleToRegex = function (styleStr) {
 10244     var trimmedStr = wysihtml.lang.string(styleStr).trim(),
 10245         escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
 10247     return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
 10248   };
 10250   var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
 10251     var newRules = wysihtml.lang.object(rules).clone(true),
 10252         tag, style;
 10254     for (tag in newRules.tags) {
 10256       if (newRules.tags.hasOwnProperty(tag)) {
 10257         if (newRules.tags[tag].keep_styles) {
 10258           for (style in newRules.tags[tag].keep_styles) {
 10259             if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
 10260               if (exceptStyles[style]) {
 10261                 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
 10262               }
 10263             }
 10264           }
 10265         }
 10266       }
 10267     }
 10269     return newRules;
 10270   };
 10272   var pickRuleset = function(ruleset, html) {
 10273     var pickedSet, defaultSet;
 10275     if (!ruleset) {
 10276       return null;
 10277     }
 10279     for (var i = 0, max = ruleset.length; i < max; i++) {
 10280       if (!ruleset[i].condition) {
 10281         defaultSet = ruleset[i].set;
 10282       }
 10283       if (ruleset[i].condition && ruleset[i].condition.test(html)) {
 10284         return ruleset[i].set;
 10285       }
 10286     }
 10288     return defaultSet;
 10289   };
 10291   return function(html, options) {
 10292     var exceptStyles = {
 10293           'color': wysihtml.dom.getStyle("color").from(options.referenceNode),
 10294           'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode)
 10295         },
 10296         rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
 10297         newHtml;
 10299     newHtml = wysihtml.dom.parse(html, {
 10300       "rules": rules,
 10301       "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content
 10302       "context": options.referenceNode.ownerDocument,
 10303       "uneditableClass": options.uneditableClass,
 10304       "clearInternals" : true, // don't paste temprorary selection and other markings
 10305       "unjoinNbsps" : true
 10306     });
 10308     return newHtml;
 10309   };
 10311 })();
 10313 /**
 10314  * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
 10315  *
 10316  * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
 10317  * @exaple
 10318  *    wysihtml.quirks.ensureProperClearing(myContentEditableElement);
 10319  */
 10320 wysihtml.quirks.ensureProperClearing = (function() {
 10321   var clearIfNecessary = function() {
 10322     var element = this;
 10323     setTimeout(function() {
 10324       var innerHTML = element.innerHTML.toLowerCase();
 10325       if (innerHTML == "<p> </p>" ||
 10326           innerHTML == "<p> </p><p> </p>") {
 10327         element.innerHTML = "";
 10328       }
 10329     }, 0);
 10330   };
 10332   return function(composer) {
 10333     wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
 10334   };
 10335 })();
 10337 // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
 10338 //
 10339 // In Firefox this:
 10340 //      var d = document.createElement("div");
 10341 //      d.innerHTML ='<a href="~"></a>';
 10342 //      d.innerHTML;
 10343 // will result in:
 10344 //      <a href="%7E"></a>
 10345 // which is wrong
 10346 (function(wysihtml) {
 10347   var TILDE_ESCAPED = "%7E";
 10348   wysihtml.quirks.getCorrectInnerHTML = function(element) {
 10349     var innerHTML = element.innerHTML;
 10350     if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
 10351       return innerHTML;
 10352     }
 10354     var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
 10355         url,
 10356         urlToSearch,
 10357         length,
 10358         i;
 10359     for (i=0, length=elementsWithTilde.length; i<length; i++) {
 10360       url         = elementsWithTilde[i].href || elementsWithTilde[i].src;
 10361       urlToSearch = wysihtml.lang.string(url).replace("~").by(TILDE_ESCAPED);
 10362       innerHTML   = wysihtml.lang.string(innerHTML).replace(urlToSearch).by(url);
 10363     }
 10364     return innerHTML;
 10365   };
 10366 })(wysihtml);
 10368 /**
 10369  * Force rerendering of a given element
 10370  * Needed to fix display misbehaviors of IE
 10371  *
 10372  * @param {Element} element The element object which needs to be rerendered
 10373  * @example
 10374  *    wysihtml.quirks.redraw(document.body);
 10375  */
 10376 (function(wysihtml) {
 10377   var CLASS_NAME = "wysihtml-quirks-redraw";
 10379   wysihtml.quirks.redraw = function(element) {
 10380     wysihtml.dom.addClass(element, CLASS_NAME);
 10381     wysihtml.dom.removeClass(element, CLASS_NAME);
 10383     // Following hack is needed for firefox to make sure that image resize handles are properly removed
 10384     try {
 10385       var doc = element.ownerDocument;
 10386       doc.execCommand("italic", false, null);
 10387       doc.execCommand("italic", false, null);
 10388     } catch(e) {}
 10389   };
 10390 })(wysihtml);
 10392 (function(wysihtml) {
 10394   // List of supported color format parsing methods
 10395   // If radix is not defined 10 is expected as default
 10396   var colorParseMethods = {
 10397         rgba : {
 10398           regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
 10399           name: "rgba"
 10400         },
 10401         rgb : {
 10402           regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
 10403           name: "rgb"
 10404         },
 10405         hex6 : {
 10406           regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
 10407           name: "hex",
 10408           radix: 16
 10409         },
 10410         hex3 : {
 10411           regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i,
 10412           name: "hex",
 10413           radix: 16
 10414         }
 10415       },
 10416       // Takes a style key name as an argument and makes a regex that can be used to the match key:value pair from style string
 10417       makeParamRegExp = function (p) {
 10418         return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi");
 10419       };
 10421   // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it
 10422   function getColorParseMethod (colorStr) {
 10423     var prop, colorTypeConf;
 10425     for (prop in colorParseMethods) {
 10426       if (!colorParseMethods.hasOwnProperty(prop)) { continue; }
 10428       colorTypeConf = colorParseMethods[prop];
 10430       if (colorTypeConf.regex.test(colorStr)) {
 10431         return colorTypeConf;
 10432       }
 10433     }
 10434   }
 10436   // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba". 
 10437   function getColorFormat (colorStr) {
 10438     var type = getColorParseMethod(colorStr);
 10440     return type ? type.name : undefined;
 10441   }
 10443   // Public API functions for styleParser
 10444   wysihtml.quirks.styleParser = {
 10446     // Takes color string value as an argument and returns suitable parsing method for it
 10447     getColorParseMethod : getColorParseMethod,
 10449     // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba". 
 10450     getColorFormat : getColorFormat,
 10452     /* Parses a color string to and array of [red, green, blue, alpha].
 10453      * paramName: optional argument to parse color value directly from style string parameter
 10454      *
 10455      * Examples:
 10456      *    var colorArray = wysihtml.quirks.styleParser.parseColor("#ABC");            // [170, 187, 204, 1]
 10457      *    var colorArray = wysihtml.quirks.styleParser.parseColor("#AABBCC");         // [170, 187, 204, 1]
 10458      *    var colorArray = wysihtml.quirks.styleParser.parseColor("rgb(1,2,3)");      // [1, 2, 3, 1]
 10459      *    var colorArray = wysihtml.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5]
 10460      *
 10461      *    var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1]
 10462      *    var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color");            // [0, 0, 0, 1]
 10463      */
 10464     parseColor : function (stylesStr, paramName) {
 10465       var paramsRegex, params, colorType, colorMatch, radix,
 10466           colorStr = stylesStr;
 10468       if (paramName) {
 10469         paramsRegex = makeParamRegExp(paramName);
 10471         if (!(params = stylesStr.match(paramsRegex))) { return false; }
 10473         params = params.pop().split(":")[1];
 10474         colorStr = wysihtml.lang.string(params).trim();
 10475       }
 10477       if (!(colorType = getColorParseMethod(colorStr))) { return false; }
 10478       if (!(colorMatch = colorStr.match(colorType.regex))) { return false; }
 10480       radix = colorType.radix || 10;
 10482       if (colorType === colorParseMethods.hex3) {
 10483         colorMatch.shift();
 10484         colorMatch.push(1);
 10485         return wysihtml.lang.array(colorMatch).map(function(d, idx) {
 10486           return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d);
 10487         });
 10488       }
 10490       colorMatch.shift();
 10492       if (!colorMatch[3]) {
 10493         colorMatch.push(1);
 10494       }
 10496       return wysihtml.lang.array(colorMatch).map(function(d, idx) {
 10497         return (idx < 3) ? parseInt(d, radix): parseFloat(d);
 10498       });
 10499     },
 10501     /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type
 10502      * If no format is given, rgba/rgb is returned based on alpha value
 10503      *
 10504      * Example:
 10505      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash");  // "#AABBCC"
 10506      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex");  // "AABBCC"
 10507      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv");  // "170, 187, 204, 1"
 10508      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba");  // "rgba(170,187,204,1)"
 10509      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb");  // "rgb(170,187,204)"
 10510      *
 10511      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 0.5]);  // "rgba(170,187,204,0.5)"
 10512      *    var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1]);  // "rgb(170,187,204)"
 10513      */
 10514     unparseColor: function(val, colorFormat) {
 10515       var hexRadix = 16;
 10517       if (colorFormat === "hex") {
 10518         return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
 10519       } else if (colorFormat === "hash") {
 10520         return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
 10521       } else if (colorFormat === "rgb") {
 10522         return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
 10523       } else if (colorFormat === "rgba") {
 10524         return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
 10525       } else if (colorFormat === "csv") {
 10526         return  val[0] + "," + val[1] + "," + val[2] + "," + val[3];
 10527       }
 10529       if (val[3] && val[3] !== 1) {
 10530         return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
 10531       } else {
 10532         return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
 10533       }
 10534     },
 10536     // Parses font size value from style string
 10537     parseFontSize: function(stylesStr) {
 10538       var params = stylesStr.match(makeParamRegExp("font-size"));
 10539       if (params) {
 10540         return wysihtml.lang.string(params[params.length - 1].split(":")[1]).trim();
 10541       }
 10542       return false;
 10543     }
 10544   };
 10546 })(wysihtml);
 10548 /**
 10549  * Selection API
 10550  *
 10551  * @example
 10552  *    var selection = new wysihtml.Selection(editor);
 10553  */
 10554 (function(wysihtml) {
 10555   var dom = wysihtml.dom;
 10557   function _getCumulativeOffsetTop(element) {
 10558     var top = 0;
 10559     if (element.parentNode) {
 10560       do {
 10561         top += element.offsetTop || 0;
 10562         element = element.offsetParent;
 10563       } while (element);
 10564     }
 10565     return top;
 10566   }
 10568   // Provides the depth of ``descendant`` relative to ``ancestor``
 10569   function getDepth(ancestor, descendant) {
 10570       var ret = 0;
 10571       while (descendant !== ancestor) {
 10572           ret++;
 10573           descendant = descendant.parentNode;
 10574           if (!descendant)
 10575               throw new Error("not a descendant of ancestor!");
 10576       }
 10577       return ret;
 10578   }
 10580   function getRangeNode(node, offset) {
 10581     if (node.nodeType === 3) {
 10582       return node;
 10583     } else {
 10584       return node.childNodes[offset] || node;
 10585     }
 10586   }
 10588   function getWebkitSelectionFixNode(container) {
 10589     var blankNode = document.createElement('span');
 10591     var placeholderRemover = function(event) {
 10592       // Self-destructs the caret and keeps the text inserted into it by user
 10593       var lastChild;
 10595       container.removeEventListener('mouseup', placeholderRemover);
 10596       container.removeEventListener('keydown', placeholderRemover);
 10597       container.removeEventListener('touchstart', placeholderRemover);
 10598       container.removeEventListener('focus', placeholderRemover);
 10599       container.removeEventListener('blur', placeholderRemover);
 10600       container.removeEventListener('paste', delayedPlaceholderRemover);
 10601       container.removeEventListener('drop', delayedPlaceholderRemover);
 10602       container.removeEventListener('beforepaste', delayedPlaceholderRemover);
 10604       if (blankNode && blankNode.parentNode) {
 10605         blankNode.parentNode.removeChild(blankNode);
 10606       }
 10607     },
 10608     delayedPlaceholderRemover = function (event) {
 10609       if (blankNode && blankNode.parentNode) {
 10610         setTimeout(placeholderRemover, 0);
 10611       }
 10612     };
 10614     blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml.INVISIBLE_SPACE));
 10615     blankNode.className = '_wysihtml-temp-caret-fix';
 10616     blankNode.style.display = 'block';
 10617     blankNode.style.minWidth = '1px';
 10618     blankNode.style.height = '0px';
 10620     container.addEventListener('mouseup', placeholderRemover);
 10621     container.addEventListener('keydown', placeholderRemover);
 10622     container.addEventListener('touchstart', placeholderRemover);
 10623     container.addEventListener('focus', placeholderRemover);
 10624     container.addEventListener('blur', placeholderRemover);
 10625     container.addEventListener('paste', delayedPlaceholderRemover);
 10626     container.addEventListener('drop', delayedPlaceholderRemover);
 10627     container.addEventListener('beforepaste', delayedPlaceholderRemover);
 10629     return blankNode;
 10630   }
 10632   // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
 10633   // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
 10634   function expandRangeToSurround(range) {
 10635       if (range.canSurroundContents()) return;
 10637       var common = range.commonAncestorContainer,
 10638           start_depth = getDepth(common, range.startContainer),
 10639           end_depth = getDepth(common, range.endContainer);
 10641       while(!range.canSurroundContents()) {
 10642         // 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.
 10643         if (start_depth > end_depth) {
 10644             range.setStartBefore(range.startContainer);
 10645             start_depth = getDepth(common, range.startContainer);
 10646         }
 10647         else {
 10648             range.setEndAfter(range.endContainer);
 10649             end_depth = getDepth(common, range.endContainer);
 10650         }
 10651       }
 10652   }
 10654   wysihtml.Selection = Base.extend(
 10655     /** @scope wysihtml.Selection.prototype */ {
 10656     constructor: function(editor, contain, unselectableClass) {
 10657       // Make sure that our external range library is initialized
 10658       rangy.init();
 10660       this.editor   = editor;
 10661       this.composer = editor.composer;
 10662       this.doc      = this.composer.doc;
 10663       this.win      = this.composer.win;
 10664       this.contain = contain;
 10665       this.unselectableClass = unselectableClass || false;
 10666     },
 10668     /**
 10669      * Get the current selection as a bookmark to be able to later restore it
 10670      *
 10671      * @return {Object} An object that represents the current selection
 10672      */
 10673     getBookmark: function() {
 10674       var range = this.getRange();
 10675       return range && range.cloneRange();
 10676     },
 10678     /**
 10679      * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark
 10680      *
 10681      * @param {Object} bookmark An object that represents the current selection
 10682      */
 10683     setBookmark: function(bookmark) {
 10684       if (!bookmark) {
 10685         return;
 10686       }
 10688       this.setSelection(bookmark);
 10689     },
 10691     /**
 10692      * Set the caret in front of the given node
 10693      *
 10694      * @param {Object} node The element or text node where to position the caret in front of
 10695      * @example
 10696      *    selection.setBefore(myElement);
 10697      */
 10698     setBefore: function(node) {
 10699       var range = rangy.createRange(this.doc);
 10700       range.setStartBefore(node);
 10701       range.setEndBefore(node);
 10702       return this.setSelection(range);
 10703     },
 10705     // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
 10706     // Webkit has an issue with placing caret into places where there are no textnodes near by.
 10707     createTemporaryCaretSpaceAfter: function (node) {
 10708       var caretPlaceholder = this.doc.createElement('span'),
 10709           caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE),
 10710           placeholderRemover = (function(event) {
 10711             // Self-destructs the caret and keeps the text inserted into it by user
 10712             var lastChild;
 10714             this.contain.removeEventListener('mouseup', placeholderRemover);
 10715             this.contain.removeEventListener('keydown', keyDownHandler);
 10716             this.contain.removeEventListener('touchstart', placeholderRemover);
 10717             this.contain.removeEventListener('focus', placeholderRemover);
 10718             this.contain.removeEventListener('blur', placeholderRemover);
 10719             this.contain.removeEventListener('paste', delayedPlaceholderRemover);
 10720             this.contain.removeEventListener('drop', delayedPlaceholderRemover);
 10721             this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover);
 10723             // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack
 10724             // Otherwise the wrapper can just be removed
 10725             if (caretPlaceholder && caretPlaceholder.parentNode) {
 10726               caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
 10727               if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) {
 10728                 lastChild = caretPlaceholder.lastChild;
 10729                 wysihtml.dom.unwrap(caretPlaceholder);
 10730                 this.setAfter(lastChild);
 10731               } else {
 10732                 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
 10733               }
 10735             }
 10736           }).bind(this),
 10737           delayedPlaceholderRemover = function (event) {
 10738             if (caretPlaceholder && caretPlaceholder.parentNode) {
 10739               setTimeout(placeholderRemover, 0);
 10740             }
 10741           },
 10742           keyDownHandler = function(event) {
 10743             if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
 10744               placeholderRemover();
 10745             }
 10746           };
 10748       caretPlaceholder.className = '_wysihtml-temp-caret-fix';
 10749       caretPlaceholder.style.position = 'absolute';
 10750       caretPlaceholder.style.display = 'block';
 10751       caretPlaceholder.style.minWidth = '1px';
 10752       caretPlaceholder.style.zIndex = '99999';
 10753       caretPlaceholder.appendChild(caretPlaceholderText);
 10755       node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
 10756       this.setBefore(caretPlaceholderText);
 10758       // Remove the caret fix on any of the following events (some are delayed as content change happens after event)
 10759       this.contain.addEventListener('mouseup', placeholderRemover);
 10760       this.contain.addEventListener('keydown', keyDownHandler);
 10761       this.contain.addEventListener('touchstart', placeholderRemover);
 10762       this.contain.addEventListener('focus', placeholderRemover);
 10763       this.contain.addEventListener('blur', placeholderRemover);
 10764       this.contain.addEventListener('paste', delayedPlaceholderRemover);
 10765       this.contain.addEventListener('drop', delayedPlaceholderRemover);
 10766       this.contain.addEventListener('beforepaste', delayedPlaceholderRemover);
 10768       return caretPlaceholder;
 10769     },
 10771     /**
 10772      * Set the caret after the given node
 10773      *
 10774      * @param {Object} node The element or text node where to position the caret in front of
 10775      * @example
 10776      *    selection.setBefore(myElement);
 10777      * callback is an optional parameter accepting a function to execute when selection ahs been set
 10778      */
 10779     setAfter: function(node, notVisual, callback) {
 10780       var win = this.win,
 10781           range = rangy.createRange(this.doc),
 10782           fixWebkitSelection = function() {
 10783             // Webkit fails to add selection if there are no textnodes in that region
 10784             // (like an uneditable container at the end of content).
 10785             var parent = node.parentNode,
 10786                 lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null;
 10788             if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) {
 10789               if (notVisual) {
 10790                 // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
 10791                 // and remove itself in call stack end instead on user interaction
 10792                 var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
 10793                 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
 10794                 this.selectNode(caretPlaceholder);
 10795                 setTimeout(function() {
 10796                   if (caretPlaceholder && caretPlaceholder.parentNode) {
 10797                     caretPlaceholder.parentNode.removeChild(caretPlaceholder);
 10798                   }
 10799                 }, 0);
 10800               } else {
 10801                 this.createTemporaryCaretSpaceAfter(node);
 10802               }
 10803             }
 10804           }.bind(this),
 10805           sel;
 10807       range.setStartAfter(node);
 10808       range.setEndAfter(node);
 10810       // In IE contenteditable must be focused before we can set selection
 10811       // thus setting the focus if activeElement is not this composer
 10812       if (!document.activeElement || document.activeElement !== this.composer.element) {
 10813         var scrollPos = this.composer.getScrollPos();
 10814         this.composer.element.focus();
 10815         this.composer.setScrollPos(scrollPos);
 10816         setTimeout(function() {
 10817           sel = this.setSelection(range);
 10818           fixWebkitSelection();
 10819           if (callback) {
 10820             callback(sel);
 10821           }
 10822         }.bind(this), 0);
 10823       } else {
 10824         sel = this.setSelection(range);
 10825         fixWebkitSelection();
 10826         if (callback) {
 10827           callback(sel);
 10828         }
 10829       }
 10830     },
 10832     /**
 10833      * Ability to select/mark nodes
 10834      *
 10835      * @param {Element} node The node/element to select
 10836      * @example
 10837      *    selection.selectNode(document.getElementById("my-image"));
 10838      */
 10839     selectNode: function(node, avoidInvisibleSpace) {
 10840       var range           = rangy.createRange(this.doc),
 10841           isElement       = node.nodeType === wysihtml.ELEMENT_NODE,
 10842           canHaveHTML     = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
 10843           content         = isElement ? node.innerHTML : node.data,
 10844           isEmpty         = (content === "" || content === wysihtml.INVISIBLE_SPACE),
 10845           displayStyle    = dom.getStyle("display").from(node),
 10846           isBlockElement  = (displayStyle === "block" || displayStyle === "list-item");
 10848       if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
 10849         // Make sure that caret is visible in node by inserting a zero width no breaking space
 10850         try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {}
 10851       }
 10852       if (canHaveHTML) {
 10853         range.selectNodeContents(node);
 10854       } else {
 10855         range.selectNode(node);
 10856       }
 10858       if (canHaveHTML && isEmpty && isElement) {
 10859         range.collapse(isBlockElement);
 10860       } else if (canHaveHTML && isEmpty) {
 10861         range.setStartAfter(node);
 10862         range.setEndAfter(node);
 10863       }
 10865       this.setSelection(range);
 10866     },
 10868     /**
 10869      * Get the node which contains the selection
 10870      *
 10871      * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
 10872      * @return {Object} The node that contains the caret
 10873      * @example
 10874      *    var nodeThatContainsCaret = selection.getSelectedNode();
 10875      */
 10876     getSelectedNode: function(controlRange) {
 10877       var selection,
 10878           range;
 10880       if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
 10881         range = this.doc.selection.createRange();
 10882         if (range && range.length) {
 10883           return range.item(0);
 10884         }
 10885       }
 10887       selection = this.getSelection(this.doc);
 10888       if (selection.focusNode === selection.anchorNode) {
 10889         return selection.focusNode;
 10890       } else {
 10891         range = this.getRange(this.doc);
 10892         return range ? range.commonAncestorContainer : this.doc.body;
 10893       }
 10894     },
 10896     fixSelBorders: function() {
 10897       var range = this.getRange();
 10898       expandRangeToSurround(range);
 10899       this.setSelection(range);
 10900     },
 10902     getSelectedOwnNodes: function(controlRange) {
 10903       var selection,
 10904           ranges = this.getOwnRanges(),
 10905           ownNodes = [];
 10907       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
 10908           ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
 10909       }
 10910       return ownNodes;
 10911     },
 10913     findNodesInSelection: function(nodeTypes) {
 10914       var ranges = this.getOwnRanges(),
 10915           nodes = [], curNodes;
 10916       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
 10917         curNodes = ranges[i].getNodes([1], function(node) {
 10918             return wysihtml.lang.array(nodeTypes).contains(node.nodeName);
 10919         });
 10920         nodes = nodes.concat(curNodes);
 10921       }
 10922       return nodes;
 10923     },
 10925     filterElements: function(filter) {
 10926       var ranges = this.getOwnRanges(),
 10927           nodes = [], curNodes;
 10929       for (var i = 0, maxi = ranges.length; i < maxi; i++) {
 10930         curNodes = ranges[i].getNodes([1], function(element){
 10931           return filter(element, ranges[i]);
 10932         });
 10933         nodes = nodes.concat(curNodes);
 10934       }
 10935       return nodes;
 10936     },
 10938     containsUneditable: function() {
 10939       var uneditables = this.getOwnUneditables(),
 10940           selection = this.getSelection();
 10942       for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
 10943         if (selection.containsNode(uneditables[i])) {
 10944           return true;
 10945         }
 10946       }
 10948       return false;
 10949     },
 10951     // Deletes selection contents making sure uneditables/unselectables are not partially deleted
 10952     // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted
 10953     deleteContents: function()  {
 10954       var range = this.getRange();
 10955       this.deleteRangeContents(range);
 10956       this.setSelection(range);
 10957     },
 10959     // Makes sure all uneditable sare notified before deleting contents
 10960     deleteRangeContents: function (range) {
 10961       var startParent, endParent, uneditables, ev;
 10963       if (this.unselectableClass) {
 10964         if ((startParent = wysihtml.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
 10965           range.setStartBefore(startParent);
 10966         }
 10967         if ((endParent = wysihtml.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
 10968           range.setEndAfter(endParent);
 10969         }
 10971         // If customevents present notify uneditable elements of being deleted
 10972         uneditables = range.getNodes([1], (function (node) {
 10973           return wysihtml.dom.hasClass(node, this.unselectableClass);
 10974         }).bind(this));
 10975         for (var i = uneditables.length; i--;) {
 10976           try {
 10977             ev = new CustomEvent("wysihtml:uneditable:delete");
 10978             uneditables[i].dispatchEvent(ev);
 10979           } catch (err) {}
 10980         }
 10981       }
 10982       range.deleteContents();
 10983     },
 10985     getCaretNode: function () {
 10986       var selection = this.getSelection();
 10987       return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
 10988     },
 10990     getPreviousNode: function(node, ignoreEmpty) {
 10991       var displayStyle;
 10992       if (!node) {
 10993         var selection = this.getSelection();
 10994         node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
 10995       }
 10997       if (node === this.contain) {
 10998           return false;
 10999       }
 11001       var ret = node.previousSibling,
 11002           parent;
 11004       if (ret === this.contain) {
 11005           return false;
 11006       }
 11008       if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
 11009          // do not count comments and other node types
 11010          ret = this.getPreviousNode(ret, ignoreEmpty);
 11011       } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
 11012         // do not count empty textnodes as previous nodes
 11013         ret = this.getPreviousNode(ret, ignoreEmpty);
 11014       } else if (ignoreEmpty && ret && ret.nodeType === 1) {
 11015         // Do not count empty nodes if param set.
 11016         // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like
 11017         displayStyle = wysihtml.dom.getStyle("display").from(ret);
 11018         if (
 11019             !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) &&
 11020             !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) &&
 11021             (/^[\s]*$/).test(ret.innerHTML)
 11022           ) {
 11023             ret = this.getPreviousNode(ret, ignoreEmpty);
 11024           }
 11025       } else if (!ret && node !== this.contain) {
 11026         parent = node.parentNode;
 11027         if (parent !== this.contain) {
 11028             ret = this.getPreviousNode(parent, ignoreEmpty);
 11029         }
 11030       }
 11032       return (ret !== this.contain) ? ret : false;
 11033     },
 11035     // Gather info about caret location (caret node, previous and next node)
 11036     getNodesNearCaret: function() {
 11037       if (!this.isCollapsed()) {
 11038         throw "Selection must be caret when using selection.getNodesNearCaret()";
 11039       }
 11041       var r = this.getOwnRanges(),
 11042           caretNode, prevNode, nextNode, offset;
 11044       if (r && r.length > 0) {
 11045         if (r[0].startContainer.nodeType === 1) {
 11046           caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1];
 11047           if (!caretNode && r[0].startOffset === 0) {
 11048             // Is first position before all nodes
 11049             nextNode = r[0].startContainer.childNodes[0];
 11050           } else if (caretNode) {
 11051             prevNode = caretNode.previousSibling;
 11052             nextNode = caretNode.nextSibling;
 11053           }
 11054         } else {
 11055           if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) {
 11056             caretNode = r[0].startContainer.previousSibling;
 11057             if (caretNode.nodeType === 3) {
 11058               offset = caretNode.data.length;
 11059             }
 11060           } else {
 11061             caretNode = r[0].startContainer;
 11062             offset = r[0].startOffset;
 11063           }
 11064           prevNode = caretNode.previousSibling;
 11065           nextNode = caretNode.nextSibling;
 11066         }
 11068         return {
 11069           "caretNode": caretNode,
 11070           "prevNode": prevNode,
 11071           "nextNode": nextNode,
 11072           "textOffset": offset
 11073         };
 11074       }
 11076       return null;
 11077     },
 11079     getSelectionParentsByTag: function(tagName) {
 11080       var nodes = this.getSelectedOwnNodes(),
 11081           curEl, parents = [];
 11083       for (var i = 0, maxi = nodes.length; i < maxi; i++) {
 11084         curEl = (nodes[i].nodeName &&  nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain);
 11085         if (curEl) {
 11086           parents.push(curEl);
 11087         }
 11088       }
 11089       return (parents.length) ? parents : null;
 11090     },
 11092     getRangeToNodeEnd: function() {
 11093       if (this.isCollapsed()) {
 11094         var range = this.getRange(),
 11095             sNode, pos, lastR;
 11096         if (range) {
 11097           sNode = range.startContainer;
 11098           pos = range.startOffset;
 11099           lastR = rangy.createRange(this.doc);
 11101           lastR.selectNodeContents(sNode);
 11102           lastR.setStart(sNode, pos);
 11103           return lastR;
 11104         }
 11105       }
 11106     },
 11108     getRangeToNodeBeginning: function() {
 11109       if (this.isCollapsed()) {
 11110         var range = this.getRange(),
 11111             sNode = range.startContainer,
 11112             pos = range.startOffset,
 11113             lastR = rangy.createRange(this.doc);
 11115         lastR.selectNodeContents(sNode);
 11116         lastR.setEnd(sNode, pos);
 11117         return lastR;
 11118       }
 11119     },
 11121     // This function returns if caret is last in a node (no textual visible content follows)
 11122     caretIsInTheEndOfNode: function(ignoreIfSpaceIsBeforeCaret) {
 11123       var r = rangy.createRange(this.doc),
 11124           s = this.getSelection(),
 11125           rangeToNodeEnd = this.getRangeToNodeEnd(),
 11126           endc, endtxt, beginc, begintxt;
 11128       if (rangeToNodeEnd) {
 11129         endc = rangeToNodeEnd.cloneContents();
 11130         endtxt = endc.textContent;
 11132         if ((/^\s*$/).test(endtxt)) {
 11133           if (ignoreIfSpaceIsBeforeCaret) {
 11134             beginc = this.getRangeToNodeBeginning().cloneContents();
 11135             begintxt = beginc.textContent;
 11136             return !(/[\u00A0 ][\s\uFEFF]*$/).test(begintxt);
 11137           } else {
 11138             return true;
 11139           }
 11140         } else {
 11141           return false;
 11142         }
 11143       } else {
 11144         return false;
 11145       }
 11146     },
 11148     caretIsFirstInSelection: function(includeLineBreaks) {
 11149       var r = rangy.createRange(this.doc),
 11150           s = this.getSelection(),
 11151           range = this.getRange(),
 11152           startNode = getRangeNode(range.startContainer, range.startOffset);
 11154       if (startNode) {
 11155         if (startNode.nodeType === wysihtml.TEXT_NODE) {
 11156           if (!startNode.parentNode) {
 11157             return false;
 11158           }
 11159           if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.dom.domNode(startNode.previousSibling).is.block())) {
 11160             return false;
 11161           }
 11162           var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace;
 11163           return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset));
 11164         } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) {
 11165           return true;
 11166         } else {
 11167           r.selectNodeContents(this.getRange().commonAncestorContainer);
 11168           r.collapse(true);
 11169           return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
 11170         }
 11171       }
 11172     },
 11174     caretIsInTheBeginnig: function(ofNode) {
 11175         var selection = this.getSelection(),
 11176             node = selection.anchorNode,
 11177             offset = selection.anchorOffset;
 11178         if (ofNode && node) {
 11179           return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1)));
 11180         } else if (node) {
 11181           return (offset === 0 && !this.getPreviousNode(node, true));
 11182         }
 11183     },
 11185     // Returns object describing node/text before selection
 11186     // If includePrevLeaves is true returns  also previous last leaf child if selection is in the beginning of current node
 11187     getBeforeSelection: function(includePrevLeaves) {
 11188       var sel = this.getSelection(),
 11189           startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode,
 11190           startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset,
 11191           rng = this.createRange(), endNode, inTmpCaret;
 11193       // If start is textnode and all is whitespace before caret. Set start offset to 0
 11194       if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) {
 11195         startOffset = 0;
 11196       }
 11198       // Escape temproray helper nodes if selection in them
 11199       inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1);
 11200       if (inTmpCaret) {
 11201         startNode = inTmpCaret.parentNode;
 11202         startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret);
 11203       }
 11205       if (startNode) {
 11206         if (startOffset > 0) {
 11207           if (startNode.nodeType === 3) {
 11208             rng.setStart(startNode, 0);
 11209             rng.setEnd(startNode, startOffset);
 11210             return {
 11211               type: "text",
 11212               range: rng,
 11213               offset : startOffset,
 11214               node: startNode
 11215             };
 11216           } else {
 11217             rng.setStartBefore(startNode.childNodes[0]);
 11218             endNode = startNode.childNodes[startOffset - 1];
 11219             rng.setEndAfter(endNode);
 11220             return {
 11221               type: "element",
 11222               range: rng,
 11223               offset : startOffset,
 11224               node: endNode
 11225             };
 11226           }
 11227         } else {
 11228           rng.setStartAndEnd(startNode, 0);
 11230           if (includePrevLeaves) {
 11231             var prevNode = this.getPreviousNode(startNode, true),
 11232                 prevLeaf = null;
 11234             if(prevNode) {
 11235               if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) {
 11236                 prevLeaf = prevNode;
 11237               } else {
 11238                 prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode();
 11239               }
 11240             }
 11242             if (prevLeaf) {
 11243               return {
 11244                 type: "leafnode",
 11245                 range: rng,
 11246                 offset : startOffset,
 11247                 node: prevLeaf
 11248               };
 11249             }
 11250           }
 11252           return {
 11253             type: "none",
 11254             range: rng,
 11255             offset : startOffset,
 11256             node: startNode
 11257           };
 11258         }
 11259       }
 11260       return null;
 11261     },
 11263     // TODO: Figure out a method from following 2 that would work universally
 11264     executeAndRestoreRangy: function(method, restoreScrollPosition) {
 11265       var sel = rangy.saveSelection(this.win);
 11266       if (!sel) {
 11267         method();
 11268       } else {
 11269         try {
 11270           method();
 11271         } catch(e) {
 11272           setTimeout(function() { throw e; }, 0);
 11273         }
 11274       }
 11275       rangy.restoreSelection(sel);
 11276     },
 11278     // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
 11279     executeAndRestore: function(method, restoreScrollPosition) {
 11280       var body                  = this.doc.body,
 11281           oldScrollTop          = restoreScrollPosition && body.scrollTop,
 11282           oldScrollLeft         = restoreScrollPosition && body.scrollLeft,
 11283           className             = "_wysihtml-temp-placeholder",
 11284           placeholderHtml       = '<span class="' + className + '">' + wysihtml.INVISIBLE_SPACE + '</span>',
 11285           range                 = this.getRange(true),
 11286           caretPlaceholder,
 11287           newCaretPlaceholder,
 11288           nextSibling, prevSibling,
 11289           node, node2, range2,
 11290           newRange;
 11292       // Nothing selected, execute and say goodbye
 11293       if (!range) {
 11294         method(body, body);
 11295         return;
 11296       }
 11298       if (!range.collapsed) {
 11299         range2 = range.cloneRange();
 11300         node2 = range2.createContextualFragment(placeholderHtml);
 11301         range2.collapse(false);
 11302         range2.insertNode(node2);
 11303         range2.detach();
 11304       }
 11306       node = range.createContextualFragment(placeholderHtml);
 11307       range.insertNode(node);
 11309       if (node2) {
 11310         caretPlaceholder = this.contain.querySelectorAll("." + className);
 11311         range.setStartBefore(caretPlaceholder[0]);
 11312         range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
 11313       }
 11314       this.setSelection(range);
 11316       // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
 11317       try {
 11318         method(range.startContainer, range.endContainer);
 11319       } catch(e) {
 11320         setTimeout(function() { throw e; }, 0);
 11321       }
 11322       caretPlaceholder = this.contain.querySelectorAll("." + className);
 11323       if (caretPlaceholder && caretPlaceholder.length) {
 11324         newRange = rangy.createRange(this.doc);
 11325         nextSibling = caretPlaceholder[0].nextSibling;
 11326         if (caretPlaceholder.length > 1) {
 11327           prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
 11328         }
 11329         if (prevSibling && nextSibling) {
 11330           newRange.setStartBefore(nextSibling);
 11331           newRange.setEndAfter(prevSibling);
 11332         } else {
 11333           newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
 11334           dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
 11335           newRange.setStartBefore(newCaretPlaceholder);
 11336           newRange.setEndAfter(newCaretPlaceholder);
 11337         }
 11338         this.setSelection(newRange);
 11339         for (var i = caretPlaceholder.length; i--;) {
 11340           caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
 11341         }
 11343       } else {
 11344         // fallback for when all hell breaks loose
 11345         this.contain.focus();
 11346       }
 11348       if (restoreScrollPosition) {
 11349         body.scrollTop  = oldScrollTop;
 11350         body.scrollLeft = oldScrollLeft;
 11351       }
 11353       // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
 11354       try {
 11355         caretPlaceholder.parentNode.removeChild(caretPlaceholder);
 11356       } catch(e2) {}
 11357     },
 11359     set: function(node, offset) {
 11360       var newRange = rangy.createRange(this.doc);
 11361       newRange.setStart(node, offset || 0);
 11362       this.setSelection(newRange);
 11363     },
 11365     /**
 11366      * Insert html at the caret or selection position and move the cursor after the inserted html
 11367      * Replaces selection content if present
 11368      *
 11369      * @param {String} html HTML string to insert
 11370      * @example
 11371      *    selection.insertHTML("<p>foobar</p>");
 11372      */
 11373     insertHTML: function(html) {
 11374       var range     = this.getRange(),
 11375           node = this.doc.createElement('DIV'),
 11376           fragment = this.doc.createDocumentFragment(),
 11377           lastChild, lastEditorElement;
 11379       if (range) {
 11380         range.deleteContents();
 11381         node.innerHTML = html;
 11382         lastChild = node.lastChild;
 11384         while (node.firstChild) {
 11385           fragment.appendChild(node.firstChild);
 11386         }
 11387         range.insertNode(fragment);
 11389         lastEditorElement = this.contain.lastChild;
 11390         while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) {
 11391           lastEditorElement = lastEditorElement.previousSibling;
 11392         }
 11394         if (lastChild) {
 11395           // fixes some pad cases mostly on webkit where last nr is needed
 11396           if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) {
 11397             this.contain.appendChild(this.doc.createElement('br'));
 11398           }
 11399           this.setAfter(lastChild);
 11400         }
 11401       }
 11402     },
 11404     /**
 11405      * Insert a node at the caret position and move the cursor behind it
 11406      *
 11407      * @param {Object} node HTML string to insert
 11408      * @example
 11409      *    selection.insertNode(document.createTextNode("foobar"));
 11410      */
 11411     insertNode: function(node) {
 11412       var range = this.getRange();
 11413       if (range) {
 11414         range.deleteContents();
 11415         range.insertNode(node);
 11416       }
 11417     },
 11419     canAppendChild: function (node) {
 11420       var anchorNode, anchorNodeTagNameLower,
 11421           voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"],
 11422           range = this.getRange();
 11424       anchorNode = node || range.startContainer;
 11426       if (anchorNode) {
 11427         anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase();
 11428       }
 11430       return voidElements.indexOf(anchorNodeTagNameLower) === -1;
 11431     },
 11433     splitElementAtCaret: function (element, insertNode) {
 11434       var sel = this.getSelection(),
 11435           range, contentAfterRangeStart,
 11436           firstChild, lastChild, childNodes;
 11438       if (sel.rangeCount > 0) {
 11439         range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
 11441         range.setEndAfter(element); // Place the end of the range after the element
 11442         contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
 11444         childNodes = contentAfterRangeStart.childNodes;
 11446         // Empty elements are cleaned up from extracted content
 11447         for (var i = childNodes.length; i --;) {
 11448           if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) {
 11449             contentAfterRangeStart.removeChild(childNodes[i]);
 11450           }
 11451         }
 11453         element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
 11455         if (insertNode) {
 11456           firstChild = insertNode.firstChild || insertNode;
 11457           lastChild = insertNode.lastChild || insertNode;
 11459           element.parentNode.insertBefore(insertNode, element.nextSibling);
 11461           // Select inserted node contents
 11462           if (firstChild && lastChild) {
 11463              range.setStartBefore(firstChild);
 11464              range.setEndAfter(lastChild);
 11465              this.setSelection(range);
 11466           }
 11467         } else {
 11468           range.setStartAfter(element);
 11469           range.setEndAfter(element);
 11470         }
 11472         if (!wysihtml.dom.domNode(element).is.visible()) {
 11473           if (wysihtml.dom.getTextContent(element) === '') {
 11474             element.parentNode.removeChild(element);
 11475           } else {
 11476             element.parentNode.replaceChild(this.doc.createTextNode(" "), element);
 11477           }
 11478         }
 11481       }
 11482     },
 11484     /**
 11485      * Wraps current selection with the given node
 11486      *
 11487      * @param {Object} node The node to surround the selected elements with
 11488      */
 11489     surround: function(nodeOptions) {
 11490       var ranges = this.getOwnRanges(),
 11491           node, nodes = [];
 11492       if (ranges.length == 0) {
 11493         return nodes;
 11494       }
 11496       for (var i = ranges.length; i--;) {
 11497         node = this.doc.createElement(nodeOptions.nodeName);
 11498         nodes.push(node);
 11499         if (nodeOptions.className) {
 11500           node.className = nodeOptions.className;
 11501         }
 11502         if (nodeOptions.cssStyle) {
 11503           node.setAttribute('style', nodeOptions.cssStyle);
 11504         }
 11505         try {
 11506           // This only works when the range boundaries are not overlapping other elements
 11507           ranges[i].surroundContents(node);
 11508           this.selectNode(node);
 11509         } catch(e) {
 11510           // fallback
 11511           node.appendChild(ranges[i].extractContents());
 11512           ranges[i].insertNode(node);
 11513         }
 11514       }
 11515       return nodes;
 11516     },
 11518     /**
 11519      * Scroll the current caret position into the view
 11520      * FIXME: This is a bit hacky, there might be a smarter way of doing this
 11521      *
 11522      * @example
 11523      *    selection.scrollIntoView();
 11524      */
 11525     scrollIntoView: function() {
 11526       var doc           = this.doc,
 11527           tolerance     = 5, // px
 11528           hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
 11529           tempElement   = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (function() {
 11530             var element = doc.createElement("span");
 11531             // The element needs content in order to be able to calculate it's position properly
 11532             element.innerHTML = wysihtml.INVISIBLE_SPACE;
 11533             return element;
 11534           })(),
 11535           offsetTop;
 11537       if (hasScrollBars) {
 11538         this.insertNode(tempElement);
 11539         offsetTop = _getCumulativeOffsetTop(tempElement);
 11540         tempElement.parentNode.removeChild(tempElement);
 11541         if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
 11542           doc.body.scrollTop = offsetTop;
 11543         }
 11544       }
 11545     },
 11547     /**
 11548      * Select line where the caret is in
 11549      */
 11550     selectLine: function() {
 11551       var r = rangy.createRange();
 11552       if (wysihtml.browser.supportsSelectionModify()) {
 11553         this._selectLine_W3C();
 11554       } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
 11555         // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
 11556         this._selectLineUniversal();
 11557       }
 11558     },
 11560     includeRangyRangeHelpers: function() {
 11561       var s = this.getSelection(),
 11562           r = s.getRangeAt(0),
 11563           isHelperNode = function(node) {
 11564             return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
 11565           },
 11566           getNodeLength = function (node) {
 11567             if (node.nodeType === 1) {
 11568               return node.childNodes && node.childNodes.length || 0;
 11569             } else {
 11570               return node.data && node.data.length || 0;
 11571             }
 11572           },
 11573           anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
 11574           fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
 11576       if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
 11577         r.setEndAfter(fnode.nextSibling);
 11578       }
 11579       if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
 11580         r.setStartBefore(anode.previousSibling);
 11581       }
 11582       r.select();
 11583     },
 11585     /**
 11586      * See https://developer.mozilla.org/en/DOM/Selection/modify
 11587      */
 11588     _selectLine_W3C: function() {
 11589       var selection = this.win.getSelection(),
 11590           initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset];
 11592       selection.modify("move", "left", "lineboundary");
 11593       selection.modify("extend", "right", "lineboundary");
 11595       // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason)
 11596       if (selection.anchorNode === initialBoundry[0] &&
 11597           selection.anchorOffset === initialBoundry[1] &&
 11598           selection.focusNode === initialBoundry[2] &&
 11599           selection.focusOffset === initialBoundry[3]
 11600       ) {
 11601         this._selectLineUniversal();
 11602       } else {
 11603         this.includeRangyRangeHelpers();
 11604       }
 11605     },
 11607     // collapses selection to current line beginning or end
 11608     toLineBoundary: function (location, collapse) {
 11609       collapse = (typeof collapse === 'undefined') ? false : collapse;
 11610       if (wysihtml.browser.supportsSelectionModify()) {
 11611         var selection = this.win.getSelection();
 11613         selection.modify("extend", location, "lineboundary");
 11614         if (collapse) {
 11615           if (location === "left") {
 11616             selection.collapseToStart();
 11617           } else if (location === "right") {
 11618             selection.collapseToEnd();
 11619           }
 11620         }
 11621       }
 11622     },
 11624     getRangeRect: function(r) {
 11625       var textNode = this.doc.createTextNode("i"),
 11626           testNode = this.doc.createTextNode("i"),
 11627           rect, cr;
 11629       /*testNode.style.visibility = "hidden";
 11630       testNode.style.width = "0px";
 11631       testNode.style.display = "inline-block";
 11632       testNode.style.overflow = "hidden";
 11633       testNode.appendChild(textNode);*/
 11635       if (r.collapsed) {
 11636         r.insertNode(testNode);
 11637         r.selectNode(testNode);
 11638         rect = r.nativeRange.getBoundingClientRect();
 11639         r.deleteContents();
 11641       } else {
 11642         rect = r.nativeRange.getBoundingClientRect();
 11643       }
 11645       return rect;
 11647     },
 11649     _selectLineUniversal: function() {
 11650       var s = this.getSelection(),
 11651           r = s.getRangeAt(0),
 11652           rect,
 11653           startRange, endRange, testRange,
 11654           count = 0,
 11655           amount, testRect, found,
 11656           that = this,
 11657           isLineBreakingElement = function(el) {
 11658             return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName));
 11659           },
 11660           prevNode = function(node) {
 11661             var pnode = node;
 11662             if (pnode) {
 11663               while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
 11664                 pnode = pnode.previousSibling;
 11665               }
 11666             }
 11667             return pnode;
 11668           };
 11670       startRange = r.cloneRange();
 11671       endRange = r.cloneRange();
 11673       if (r.collapsed) {
 11674         // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
 11675         // TODO: figure out a shorter and more readable way
 11676         if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
 11677           r.moveEnd('character', 1);
 11678         } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
 11679           r.moveEnd('character', 1);
 11680         } else if (
 11681           r.startOffset > 0 &&
 11682           (
 11683             r.startContainer.nodeType === 3 ||
 11684             (
 11685               r.startContainer.nodeType === 1 &&
 11686               !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))
 11687             )
 11688           )
 11689         ) {
 11690           r.moveStart('character', -1);
 11691         }
 11692       }
 11693       if (!r.collapsed) {
 11694         r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
 11695       }
 11697       // Is probably just empty line as can not be expanded
 11698       rect = r.nativeRange.getBoundingClientRect();
 11699       // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes;
 11700       do {
 11701         amount = r.moveStart('character', -1);
 11702         testRect =  r.nativeRange.getBoundingClientRect();
 11704         if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
 11705           r.moveStart('character', 1);
 11706           found = true;
 11707         }
 11708         count++;
 11709       } while (amount !== 0 && !found && count < 2000);
 11710       count = 0;
 11711       found = false;
 11712       rect = r.nativeRange.getBoundingClientRect();
 11714       if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) {
 11715         do {
 11716           amount = r.moveEnd('character', 1);
 11717           testRect =  r.nativeRange.getBoundingClientRect();
 11718           if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
 11719             r.moveEnd('character', -1);
 11721             // Fix a IE line end marked by linebreak element although caret is before it
 11722             // If causes problems should be changed to be applied only to IE
 11723             if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
 11724               if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
 11725                 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
 11726               } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
 11727                 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
 11728               }
 11729             }
 11730             found = true;
 11731           }
 11732           count++;
 11733         } while (amount !== 0 && !found && count < 2000);
 11734       }
 11735       r.select();
 11736       this.includeRangyRangeHelpers();
 11737     },
 11739     getText: function() {
 11740       var selection = this.getSelection();
 11741       return selection ? selection.toString() : "";
 11742     },
 11744     getNodes: function(nodeType, filter) {
 11745       var range = this.getRange();
 11746       if (range) {
 11747         return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter);
 11748       } else {
 11749         return [];
 11750       }
 11751     },
 11753     // Gets all the elements in selection with nodeType
 11754     // Ignores the elements not belonging to current editable area
 11755     // If filter is defined nodes must pass the filter function with true to be included in list
 11756     getOwnNodes: function(nodeType, filter, splitBounds) {
 11757       var ranges = this.getOwnRanges(),
 11758           nodes = [];
 11759       for (var r = 0, rmax = ranges.length; r < rmax; r++) {
 11760         if (ranges[r]) {
 11761           if (splitBounds) {
 11762             ranges[r].splitBoundaries();
 11763           }
 11764           nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter));
 11765         }
 11766       }
 11768       return nodes;
 11769     },
 11771     fixRangeOverflow: function(range) {
 11772       if (this.contain && this.contain.firstChild && range) {
 11773         var containment = range.compareNode(this.contain);
 11774         if (containment !== 2) {
 11775           if (containment === 1) {
 11776             range.setStartBefore(this.contain.firstChild);
 11777           }
 11778           if (containment === 0) {
 11779             range.setEndAfter(this.contain.lastChild);
 11780           }
 11781           if (containment === 3) {
 11782             range.setStartBefore(this.contain.firstChild);
 11783             range.setEndAfter(this.contain.lastChild);
 11784           }
 11785         } else if (this._detectInlineRangeProblems(range)) {
 11786           var previousElementSibling = range.endContainer.previousElementSibling;
 11787           if (previousElementSibling) {
 11788             range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
 11789           }
 11790         }
 11791       }
 11792     },
 11794     _endOffsetForNode: function(node) {
 11795       var range = document.createRange();
 11796       range.selectNodeContents(node);
 11797       return range.endOffset;
 11798     },
 11800     _detectInlineRangeProblems: function(range) {
 11801       var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
 11802       return (
 11803         range.endOffset == 0 &&
 11804         position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
 11805       );
 11806     },
 11808     getRange: function(dontFix) {
 11809       var selection = this.getSelection(),
 11810           range = selection && selection.rangeCount && selection.getRangeAt(0);
 11812       if (dontFix !== true) {
 11813         this.fixRangeOverflow(range);
 11814       }
 11816       return range;
 11817     },
 11819     getOwnUneditables: function() {
 11820       var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
 11821           deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
 11823       return wysihtml.lang.array(allUneditables).without(deepUneditables);
 11824     },
 11826     // Returns an array of ranges that belong only to this editable
 11827     // Needed as uneditable block in contenteditabel can split range into pieces
 11828     // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
 11829     getOwnRanges: function()  {
 11830       var ranges = [],
 11831           r = this.getRange(),
 11832           tmpRanges;
 11834       if (r) { ranges.push(r); }
 11836       if (this.unselectableClass && this.contain && r) {
 11837         var uneditables = this.getOwnUneditables(),
 11838             tmpRange;
 11839         if (uneditables.length > 0) {
 11840           for (var i = 0, imax = uneditables.length; i < imax; i++) {
 11841             tmpRanges = [];
 11842             for (var j = 0, jmax = ranges.length; j < jmax; j++) {
 11843               if (ranges[j]) {
 11844                 switch (ranges[j].compareNode(uneditables[i])) {
 11845                   case 2:
 11846                     // all selection inside uneditable. remove
 11847                   break;
 11848                   case 3:
 11849                     //section begins before and ends after uneditable. spilt
 11850                     tmpRange = ranges[j].cloneRange();
 11851                     tmpRange.setEndBefore(uneditables[i]);
 11852                     tmpRanges.push(tmpRange);
 11854                     tmpRange = ranges[j].cloneRange();
 11855                     tmpRange.setStartAfter(uneditables[i]);
 11856                     tmpRanges.push(tmpRange);
 11857                   break;
 11858                   default:
 11859                     // in all other cases uneditable does not touch selection. dont modify
 11860                     tmpRanges.push(ranges[j]);
 11861                 }
 11862               }
 11863               ranges = tmpRanges;
 11864             }
 11865           }
 11866         }
 11867       }
 11868       return ranges;
 11869     },
 11871     getSelection: function() {
 11872       return rangy.getSelection(this.win);
 11873     },
 11875     // Sets selection in document to a given range
 11876     // Set selection method detects if it fails to set any selection in document and returns null on fail
 11877     // (especially needed in webkit where some ranges just can not create selection for no reason)
 11878     setSelection: function(range) {
 11879       var selection = rangy.getSelection(this.win);
 11880       selection.setSingleRange(range);
 11881       return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
 11882     },
 11886     // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area
 11887     selectAll: function() {
 11888       var range = this.createRange(),
 11889           composer = this.composer,
 11890           that = this,
 11891           blankEndNode = getWebkitSelectionFixNode(this.composer.element),
 11892           blankStartNode = getWebkitSelectionFixNode(this.composer.element),
 11893           s;
 11895       var doSelect = function() {
 11896         range.setStart(composer.element, 0);
 11897         range.setEnd(composer.element, composer.element.childNodes.length);
 11898         s = that.setSelection(range);
 11899       };
 11901       var notSelected = function() {
 11902         return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None"));
 11903       }
 11905       wysihtml.dom.removeInvisibleSpaces(this.composer.element);
 11906       doSelect();
 11908       if (this.composer.element.firstChild && notSelected())  {
 11909         // Try fixing end
 11910         this.composer.element.appendChild(blankEndNode);
 11911         doSelect();
 11913         if (notSelected()) {
 11914           // Remove end fix
 11915           blankEndNode.parentNode.removeChild(blankEndNode);
 11917           // Try fixing beginning
 11918           this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild);
 11919           doSelect();
 11921           if (notSelected()) {
 11922             // Try fixing both
 11923             this.composer.element.appendChild(blankEndNode);
 11924             doSelect();
 11925           }
 11926         }
 11927       }
 11928     },
 11930     createRange: function() {
 11931       return rangy.createRange(this.doc);
 11932     },
 11934     isCollapsed: function() {
 11935         return this.getSelection().isCollapsed;
 11936     },
 11938     getHtml: function() {
 11939       return this.getSelection().toHtml();
 11940     },
 11942     getPlainText: function () {
 11943       return this.getSelection().toString();
 11944     },
 11946     isEndToEndInNode: function(nodeNames) {
 11947       var range = this.getRange(),
 11948           parentElement = range.commonAncestorContainer,
 11949           startNode = range.startContainer,
 11950           endNode = range.endContainer;
 11953         if (parentElement.nodeType === wysihtml.TEXT_NODE) {
 11954           parentElement = parentElement.parentNode;
 11955         }
 11957         if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
 11958           return false;
 11959         }
 11961         if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
 11962           return false;
 11963         }
 11965         while (startNode && startNode !== parentElement) {
 11966           if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) {
 11967             return false;
 11968           }
 11969           if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
 11970             return false;
 11971           }
 11972           startNode = startNode.parentNode;
 11973         }
 11975         while (endNode && endNode !== parentElement) {
 11976           if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) {
 11977             return false;
 11978           }
 11979           if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
 11980             return false;
 11981           }
 11982           endNode = endNode.parentNode;
 11983         }
 11985         return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
 11986     },
 11988     isInThisEditable: function() {
 11989       var sel = this.getSelection(),
 11990           fnode = sel.focusNode,
 11991           anode = sel.anchorNode;
 11993       // In IE node contains will not work for textnodes, thus taking parentNode
 11994       if (fnode && fnode.nodeType !== 1) {
 11995         fnode = fnode.parentNode;
 11996       }
 11998       if (anode && anode.nodeType !== 1) {
 11999         anode = anode.parentNode;
 12000       }
 12002       return anode && fnode &&
 12003              (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) &&
 12004              (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode);
 12005     },
 12007     deselect: function() {
 12008       var sel = this.getSelection();
 12009       sel && sel.removeAllRanges();
 12010     }
 12011   });
 12013 })(wysihtml);
 12015 /**
 12016  * Rich Text Query/Formatting Commands
 12017  *
 12018  * @example
 12019  *    var commands = new wysihtml.Commands(editor);
 12020  */
 12021 wysihtml.Commands = Base.extend(
 12022   /** @scope wysihtml.Commands.prototype */ {
 12023   constructor: function(editor) {
 12024     this.editor   = editor;
 12025     this.composer = editor.composer;
 12026     this.doc      = this.composer.doc;
 12027   },
 12029   /**
 12030    * Check whether the browser supports the given command
 12031    *
 12032    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
 12033    * @example
 12034    *    commands.supports("createLink");
 12035    */
 12036   support: function(command) {
 12037     return wysihtml.browser.supportsCommand(this.doc, command);
 12038   },
 12040   /**
 12041    * Check whether the browser supports the given command
 12042    *
 12043    * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
 12044    * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
 12045    * @example
 12046    *    commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
 12047    */
 12048   exec: function(command, value) {
 12049     var obj     = wysihtml.commands[command],
 12050         args    = wysihtml.lang.array(arguments).get(),
 12051         method  = obj && obj.exec,
 12052         result  = null;
 12054     // If composer ahs placeholder unset it before command
 12055     // Do not apply on commands that are behavioral 
 12056     if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) {
 12057       this.composer.element.innerHTML = "";
 12058       this.composer.selection.selectNode(this.composer.element);
 12059     }
 12061     this.editor.fire("beforecommand:composer");
 12063     if (method) {
 12064       args.unshift(this.composer);
 12065       result = method.apply(obj, args);
 12066     } else {
 12067       try {
 12068         // try/catch for buggy firefox
 12069         result = this.doc.execCommand(command, false, value);
 12070       } catch(e) {}
 12071     }
 12073     this.editor.fire("aftercommand:composer");
 12074     return result;
 12075   },
 12077   remove: function(command, commandValue) {
 12078     var obj     = wysihtml.commands[command],
 12079         args    = wysihtml.lang.array(arguments).get(),
 12080         method  = obj && obj.remove;
 12081     if (method) {
 12082       args.unshift(this.composer);
 12083       return method.apply(obj, args);
 12084     }
 12085   },
 12087   /**
 12088    * Check whether the current command is active
 12089    * If the caret is within a bold text, then calling this with command "bold" should return true
 12090    *
 12091    * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
 12092    * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
 12093    * @return {Boolean} Whether the command is active
 12094    * @example
 12095    *    var isCurrentSelectionBold = commands.state("bold");
 12096    */
 12097   state: function(command, commandValue) {
 12098     var obj     = wysihtml.commands[command],
 12099         args    = wysihtml.lang.array(arguments).get(),
 12100         method  = obj && obj.state;
 12101     if (method) {
 12102       args.unshift(this.composer);
 12103       return method.apply(obj, args);
 12104     } else {
 12105       try {
 12106         // try/catch for buggy firefox
 12107         return this.doc.queryCommandState(command);
 12108       } catch(e) {
 12109         return false;
 12110       }
 12111     }
 12112   },
 12114   /* Get command state parsed value if command has stateValue parsing function */
 12115   stateValue: function(command) {
 12116     var obj     = wysihtml.commands[command],
 12117         args    = wysihtml.lang.array(arguments).get(),
 12118         method  = obj && obj.stateValue;
 12119     if (method) {
 12120       args.unshift(this.composer);
 12121       return method.apply(obj, args);
 12122     } else {
 12123       return false;
 12124     }
 12125   }
 12126 });
 12128 (function(wysihtml) {
 12130   var nodeOptions = {
 12131     nodeName: "A",
 12132     toggle: false
 12133   };
 12135   function getOptions(value) {
 12136     var options = typeof value === 'object' ? value : {'href': value};
 12137     return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get();
 12138   }
 12140   wysihtml.commands.createLink  = {
 12141     exec: function(composer, command, value) {
 12142       var opts = getOptions(value);
 12144       if (composer.selection.isCollapsed() && !this.state(composer, command)) {
 12145         var textNode = composer.doc.createTextNode(opts.attribute.href);
 12146         composer.selection.insertNode(textNode);
 12147         composer.selection.selectNode(textNode);
 12148       }
 12149       wysihtml.commands.formatInline.exec(composer, command, opts);
 12150     },
 12152     state: function(composer, command) {
 12153       return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
 12154     }
 12155   };
 12157 })(wysihtml);
 12159 /* Formatblock
 12160  * Is used to insert block level elements 
 12161  * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...)
 12162  * 
 12163 */
 12164 (function(wysihtml) {
 12166   var dom = wysihtml.dom,
 12167       // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
 12168       // instead of creating a H4 within a H1 which would result in semantically invalid html
 12169       UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre",
 12170       BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote",
 12171       INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u";
 12173   function correctOptionsForSimilarityCheck(options) {
 12174     return {
 12175       nodeName: options.nodeName || null,
 12176       className: (!options.classRegExp) ? options.className || null : null,
 12177       classRegExp: options.classRegExp || null,
 12178       styleProperty: options.styleProperty || null
 12179     };
 12180   }
 12182   function getRangeNode(node, offset) {
 12183     if (node.nodeType === 3) {
 12184       return node;
 12185     } else {
 12186       return node.childNodes[offset] || node;
 12187     }
 12188   }
 12190   // Returns if node is a line break
 12191   function isBr(n) {
 12192     return n && n.nodeType === 1 && n.nodeName === "BR";
 12193   }
 12195   // Is block level element
 12196   function isBlock(n, composer) {
 12197     return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
 12198   }
 12200   // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
 12201   function isBookmark(n) {
 12202     return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
 12203   }
 12205   // Is line breaking node
 12206   function isLineBreaking(n, composer) {
 12207     return isBr(n) || isBlock(n, composer);
 12208   }
 12210   // Removes empty block level elements
 12211   function cleanup(composer, newBlockElements) {
 12212     wysihtml.dom.removeInvisibleSpaces(composer.element);
 12213     var container = composer.element,
 12214         allElements = container.querySelectorAll(BLOCK_ELEMENTS),
 12215         noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
 12216         uneditables = container.querySelectorAll(noEditQuery),
 12217         elements = wysihtml.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
 12218         nbIdx;
 12220     for (var i = elements.length; i--;) {
 12221       if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) {
 12222         // If cleanup removes some new block elements. remove them from newblocks array too
 12223         nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]);
 12224         if (nbIdx > -1) {
 12225           newBlockElements.splice(nbIdx, 1);
 12226         }
 12227         elements[i].parentNode.removeChild(elements[i]);
 12228       }
 12229     }
 12231     return newBlockElements;
 12232   }
 12234   function defaultNodeName(composer) {
 12235     return composer.config.useLineBreaks ? "DIV" : "P";
 12236   }
 12238   // The outermost un-nestable block element parent of from node
 12239   function findOuterBlock(node, container, allBlocks) {
 12240     var n = node,
 12241         block = null;
 12243     while (n && container && n !== container) {
 12244       if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) {
 12245         block = n;
 12246       }
 12247       n = n.parentNode;
 12248     }
 12250     return block;
 12251   }
 12253   // Clone for splitting the inner inline element out of its parent inline elements context
 12254   // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
 12255   function cloneOuterInlines(node, container) {
 12256     var n = node,
 12257         innerNode,
 12258         parentNode,
 12259         el = null,
 12260         el2;
 12262     while (n && container && n !== container) {
 12263       if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
 12264         parentNode = n;
 12265         if (el === null) {
 12266           el = n.cloneNode(false);
 12267           innerNode = el;
 12268         } else {
 12269           el2 = n.cloneNode(false);
 12270           el2.appendChild(el);
 12271           el = el2;
 12272         }
 12273       }
 12274       n = n.parentNode;
 12275     }
 12277     return {
 12278       parent: parentNode,
 12279       outerNode: el,
 12280       innerNode: innerNode
 12281     };
 12282   }
 12284   // Formats an element according to options nodeName, className, styleProperty, styleValue
 12285   // If element is not defined, creates new element
 12286   // if opotions is null, remove format instead
 12287   function applyOptionsToElement(element, options, composer) {
 12289     if (!element) {
 12290       element = composer.doc.createElement(options.nodeName || defaultNodeName(composer));
 12291       // Add invisible space as otherwise webkit cannot set selection or range to it correctly
 12292       element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
 12293     }
 12295     if (options.nodeName && element.nodeName !== options.nodeName) {
 12296       element = dom.renameElement(element, options.nodeName);
 12297     }
 12299     // Remove similar classes before applying className
 12300     if (options.classRegExp) {
 12301       element.className = element.className.replace(options.classRegExp, "");
 12302     }
 12303     if (options.className) {
 12304       element.classList.add(options.className);
 12305     }
 12307     if (options.styleProperty && typeof options.styleValue !== "undefined") {
 12308       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
 12309     }
 12311     return element;
 12312   }
 12314   // Unsets element properties by options
 12315   // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
 12316   function removeOptionsFromElement(element, options, composer) {
 12317     var style, classes,
 12318         prevNode = element.previousSibling,
 12319         nextNode = element.nextSibling,
 12320         unwrapped = false;
 12322     if (options.styleProperty) {
 12323       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
 12324     }
 12325     if (options.className) {
 12326       element.classList.remove(options.className);
 12327     }
 12329     if (options.classRegExp) {
 12330       element.className = element.className.replace(options.classRegExp, "");
 12331     }
 12333     // Clean up blank class attribute
 12334     if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") {
 12335       element.removeAttribute('class');
 12336     }
 12338     if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
 12339       style = element.getAttribute('style');
 12340       if (!style || style.trim() === '') {
 12341         dom.unwrap(element);
 12342         unwrapped = true;
 12343       } else {
 12344         element = dom.renameElement(element, defaultNodeName(composer));
 12345       }
 12346     }
 12348     // Clean up blank style attribute
 12349     if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
 12350       element.removeAttribute('style');
 12351     }
 12353     if (unwrapped) {
 12354       applySurroundingLineBreaks(prevNode, nextNode, composer);
 12355     }
 12356   }
 12358   // Unwraps block level elements from inside content
 12359   // Useful as not all block level elements can contain other block-levels
 12360   function unwrapBlocksFromContent(element) {
 12361     var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
 12362         nextEl, prevEl;
 12364     for (var i = blocks.length; i--;) {
 12365       nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
 12366       prevEl = wysihtml.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 12368       if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
 12369         if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
 12370           blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
 12371         }
 12372       }
 12373       if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
 12374         if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
 12375           blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
 12376         }
 12377       }
 12378       wysihtml.dom.unwrap(blocks[i]);
 12379     }
 12380   }
 12382   // Fix ranges that visually cover whole block element to actually cover the block
 12383   function fixRangeCoverage(range, composer) {
 12384     var node,
 12385         start = range.startContainer,
 12386         end = range.endContainer;
 12388     // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
 12389     // This ensures the wrapper node is modified and optios added to it
 12390     if (start && start.nodeType === 1 && start === end) {
 12391       if (start.firstChild === start.lastChild && range.endOffset === 1) {
 12392         if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
 12393           range.setStartBefore(start);
 12394           range.setEndAfter(end);
 12395         }
 12396       }
 12397       return;
 12398     }
 12400     // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
 12401     if (start && start.nodeType === 1 && end.nodeType === 3) {
 12402       if (start.firstChild === end && range.endOffset === end.data.length) {
 12403         if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
 12404           range.setEndAfter(start);
 12405         }
 12406       }
 12407       return;
 12408     }
 12410     // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
 12411     if (end && end.nodeType === 1 && start.nodeType === 3) {
 12412       if (end.firstChild === start && range.startOffset === 0) {
 12413         if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
 12414           range.setStartBefore(end);
 12415         }
 12416       }
 12417       return;
 12418     }
 12420     // If range covers a whole textnode and the textnode is the only child of node, extend range to node 
 12421     if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
 12422       if (range.endOffset == end.data.length && range.startOffset === 0) {
 12423         node = start.parentNode;
 12424         if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
 12425           range.setStartBefore(node);
 12426           range.setEndAfter(node);
 12427         }
 12428       }
 12429       return;
 12430     }
 12431   }
 12433   // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
 12434   // Some places do not allow block level elements inbetween (inside ul and outside li)
 12435   // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
 12436   function fixNotPermittedInsertionPoints(ranges) {
 12437     var newRanges = [],
 12438         lis, j, maxj, tmpRange, rangePos, closestLI;
 12440     for (var i = 0, maxi = ranges.length; i < maxi; i++) {
 12442       // Fixes range start and end positions if inside UL or OL element (outside of LI)
 12443       if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
 12444         ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
 12445       }
 12446       if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
 12447         closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
 12448         if (closestLI.childNodes) {
 12449           ranges[i].setEnd(closestLI, closestLI.childNodes.length);
 12450         }
 12451       }
 12453       // Get all LI eleemnts in selection (fully or partially covered)
 12454       // And make sure ranges are either inside LI or outside UL/OL
 12455       // Split and add new ranges as needed to cover same range content
 12456       // TODO: Needs improvement to accept DL, DD, DT
 12457       lis = ranges[i].getNodes([1], function(node) {
 12458         return node.nodeName === "LI";
 12459       });
 12460       if (lis.length > 0) {
 12462         for (j = 0, maxj = lis.length; j < maxj; j++) {
 12463           rangePos = ranges[i].compareNode(lis[j]);
 12465           // Fixes start of range that crosses LI border
 12466           if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
 12467             // Range starts before and ends inside the node
 12469             tmpRange = ranges[i].cloneRange();
 12470             closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]});
 12472             if (closestLI) {
 12473               tmpRange.setEnd(closestLI, closestLI.childNodes.length);
 12474             } else if (lis[j].closest('ul, ol')) {
 12475               tmpRange.setEndBefore(lis[j].closest('ul, ol'));
 12476             } else {
 12477               tmpRange.setEndBefore(lis[j]);
 12478             }
 12479             newRanges.push(tmpRange);
 12480             ranges[i].setStart(lis[j], 0);
 12481           }
 12483           // Fixes end of range that crosses li border
 12484           if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
 12485             // Range starts inside the node and ends after node
 12487             tmpRange = ranges[i].cloneRange();
 12488             tmpRange.setEnd(lis[j], lis[j].childNodes.length);
 12489             newRanges.push(tmpRange);
 12491             // Find next LI in list and if present set range to it, else 
 12492             closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]});
 12493             if (closestLI) {
 12494               ranges[i].setStart(closestLI, 0);
 12495             } else if (lis[j].closest('ul, ol')) {
 12496               ranges[i].setStartAfter(lis[j].closest('ul, ol'));
 12497             } else {
 12498               ranges[i].setStartAfter(lis[j]);
 12499             } 
 12500           }
 12501         }
 12502         newRanges.push(ranges[i]);
 12503       } else {
 12504         newRanges.push(ranges[i]);
 12505       }
 12506     }
 12507     return newRanges;
 12508   }
 12510   // Return options object with nodeName set if original did not have any
 12511   // Node name is set to local or global default
 12512   function getOptionsWithNodename(options, defaultName, composer) {
 12513     var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null;
 12514     if (correctedOptions) {  
 12515       correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
 12516     }
 12517     return correctedOptions;
 12518   }
 12520   // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
 12521   // Also wraps empty clones of split parent tags around fragment to keep formatting
 12522   // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
 12523   function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
 12524     var rangeStartContainer = range.startContainer,
 12525         firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
 12526         outerInlines, first, last, prev, next;
 12528     if (firstOuterBlock) {
 12529       // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
 12530       first = fragment.firstChild;
 12531       last = fragment.lastChild;
 12533       composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
 12535       next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
 12536       prev = wysihtml.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 12538       if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
 12539         first.parentNode.insertBefore(composer.doc.createElement('br'), first);
 12540       }
 12542       if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
 12543         next.parentNode.insertBefore(composer.doc.createElement('br'), next);
 12544       }
 12546     } else {
 12547       // Ensure node does not get inserted into an inline where it is not allowed
 12548       outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
 12549       if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
 12550         if (fragment.childNodes.length === 1) {
 12551           while(fragment.firstChild.firstChild) {
 12552             outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
 12553           }
 12554           fragment.firstChild.appendChild(outerInlines.outerNode);
 12555         }
 12556         composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
 12557       } else {
 12558         var fc = fragment.firstChild,
 12559             lc = fragment.lastChild;
 12561         range.insertNode(fragment);
 12562         // restore range position as it might get lost in webkit sometimes
 12563         range.setStartBefore(fc);
 12564         range.setEndAfter(lc);
 12565       }
 12566     }
 12567   }
 12569   // Removes all block formatting from range
 12570   function clearRangeBlockFromating(range, closestBlockName, composer) {
 12571     var r = range.cloneRange(),
 12572         prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
 12573         nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
 12574         content = r.extractContents(),
 12575         fragment = composer.doc.createDocumentFragment(),
 12576         children, blocks,
 12577         first = true;
 12579     while(content.firstChild) {
 12580       // Iterate over all selection content first level childNodes
 12581       if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
 12582         // If node is a block element
 12583         // Split block formating and add new block to wrap caret
 12585         unwrapBlocksFromContent(content.firstChild);
 12586         children = wysihtml.dom.unwrap(content.firstChild);
 12588         // Add line break before if needed
 12589         if (children.length > 0) {
 12590           if (
 12591             (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
 12592             (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
 12593           ){
 12594             fragment.appendChild(composer.doc.createElement('BR'));
 12595           }
 12596         }
 12598         for (var c = 0, cmax = children.length; c < cmax; c++) {
 12599           fragment.appendChild(children[c]);
 12600         }
 12602         // Add line break after if needed
 12603         if (children.length > 0) {
 12604           if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
 12605             if (nextNode || fragment.lastChild !== content.lastChild) {
 12606               fragment.appendChild(composer.doc.createElement('BR'));
 12607             }
 12608           }
 12609         }
 12611       } else {
 12612         fragment.appendChild(content.firstChild);
 12613       }
 12615       first = false;
 12616     }
 12617     blocks = wysihtml.lang.array(fragment.childNodes).get();
 12618     injectFragmentToRange(fragment, r, composer);
 12619     return blocks;
 12620   }
 12622   // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
 12623   function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
 12624     var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 12625     if (isBr(nextNode)) {
 12626       nextNode.parentNode.removeChild(nextNode);
 12627     }
 12628     if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
 12629       prevNode.parentNode.removeChild(prevNode);
 12630     }
 12631   }
 12633   function applySurroundingLineBreaks(prevNode, nextNode, composer) {
 12634     var prevPrev;
 12636     if (prevNode && isBookmark(prevNode)) {
 12637       prevNode = prevNode.previousSibling;
 12638     }
 12639     if (nextNode && isBookmark(nextNode)) {
 12640       nextNode = nextNode.nextSibling;
 12641     }
 12643     prevPrev = prevNode && prevNode.previousSibling;
 12645     if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
 12646       prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
 12647     }
 12649     if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
 12650       nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
 12651     }
 12652   }
 12654   var isWhitespaceBefore = function (textNode, offset) {
 12655     var str = textNode.data ? textNode.data.slice(0, offset) : "";
 12656     return (/^\s*$/).test(str);
 12657   }
 12659   var isWhitespaceAfter = function (textNode, offset) {
 12660     var str = textNode.data ? textNode.data.slice(offset) : "";
 12661     return (/^\s*$/).test(str);
 12662   }
 12664   var trimBlankTextsAndBreaks = function(fragment) {
 12665     if (fragment) {
 12666       while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) {
 12667         fragment.removeChild(fragment.firstChild);
 12668       }
 12670       while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) {
 12671         fragment.removeChild(fragment.lastChild);
 12672       }
 12674       if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
 12675         fragment.removeChild(fragment.firstChild);
 12676       }
 12678       if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
 12679         fragment.removeChild(fragment.lastChild);
 12680       }
 12681     }
 12682   }
 12684   // Wrap the range with a block level element
 12685   // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
 12686   function wrapRangeWithElement(range, options, closestBlockName, composer) {
 12687     var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
 12688         r = range.cloneRange(),
 12689         rangeStartContainer = r.startContainer,
 12690         startNode = getRangeNode(r.startContainer, r.startOffset),
 12691         endNode = getRangeNode(r.endContainer, r.endOffset),
 12692         prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode :  wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
 12693         nextNode = (
 12694           (
 12695             r.endContainer.nodeType === 1 &&
 12696             r.endContainer.childNodes[r.endOffset] === endNode &&
 12697             (
 12698               endNode.nodeType === 1 ||
 12699               !isWhitespaceAfter(endNode, r.endOffset) &&
 12700               !wysihtml.dom.domNode(endNode).is.rangyBookmark()
 12701             )
 12702           ) || (
 12703             r.endContainer === endNode &&
 12704             endNode.nodeType === 3 &&
 12705             !isWhitespaceAfter(endNode, r.endOffset)
 12706           )
 12707         ) ? endNode : wysihtml.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
 12708         content = r.extractContents(),
 12709         fragment = composer.doc.createDocumentFragment(),
 12710         similarOuterBlock = similarOptions ? wysihtml.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
 12711         splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
 12712         firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
 12713         wrapper, blocks, children,
 12714         firstc, lastC;
 12716     if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) {
 12717       endNode = nextNode;
 12718       nextNode = endNode.nextSibling;
 12719     }
 12721     trimBlankTextsAndBreaks(content);
 12723     if (options && options.nodeName === "BLOCKQUOTE") {
 12725       // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
 12726       var tmpEl = applyOptionsToElement(null, options, composer);
 12727       tmpEl.appendChild(content);
 12728       fragment.appendChild(tmpEl);
 12729       blocks = [tmpEl];
 12731     } else {
 12733       if (!content.firstChild) {
 12734         // IF selection is caret (can happen if line is empty) add format around tag 
 12735         fragment.appendChild(applyOptionsToElement(null, options, composer));
 12736       } else {
 12738         while(content.firstChild) {
 12739           // Iterate over all selection content first level childNodes
 12741           if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
 12743             // If node is a block element
 12744             // Escape(split) block formatting at caret
 12745             applyOptionsToElement(content.firstChild, options, composer);
 12746             if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
 12747               unwrapBlocksFromContent(content.firstChild);
 12748             }
 12749             fragment.appendChild(content.firstChild);
 12751           } else {
 12753             // Wrap subsequent non-block nodes inside new block element
 12754             wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
 12755             while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
 12756               if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
 12757                 unwrapBlocksFromContent(content.firstChild);
 12758               }
 12759               wrapper.appendChild(content.firstChild);
 12760             }
 12761             fragment.appendChild(wrapper);
 12762           }
 12763         }
 12764       }
 12766       blocks = wysihtml.lang.array(fragment.childNodes).get();
 12767     }
 12768     injectFragmentToRange(fragment, r, composer, firstOuterBlock);
 12769     removeSurroundingLineBreaks(prevNode, nextNode, composer);
 12771     // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block
 12772     // (if it contains rangy bookmark, so selection can be restored later correctly)
 12773     if (blocks.length > 0 &&
 12774       (
 12775         typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark()
 12776       )
 12777     ) {
 12778       blocks[blocks.length - 1].appendChild(composer.doc.createElement('br'));
 12779     }
 12780     return blocks;
 12781   }
 12783   // Find closest block level element
 12784   function getParentBlockNodeName(element, composer) {
 12785     var parentNode = wysihtml.dom.getParentElement(element, {
 12786           query: BLOCK_ELEMENTS
 12787         }, null, composer.element);
 12789     return (parentNode) ? parentNode.nodeName : null;
 12790   }
 12792   // Expands caret to cover the closest block that:
 12793   //   * cannot contain other block level elements (h1-6,p, etc)
 12794   //   * Has the same nodeName that is to be inserted
 12795   //   * has insertingNodeName
 12796   //   * is DIV if insertingNodeName is not present
 12797   //
 12798   // If nothing found selects the current line
 12799   function expandCaretToBlock(composer, insertingNodeName) {
 12800     var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
 12801           query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
 12802         }, null, composer.element),
 12803         range;
 12805     if (parent) {
 12806       range = composer.selection.createRange();
 12807       range.selectNode(parent);
 12808       composer.selection.setSelection(range);
 12809     } else if (!composer.isEmpty()) {
 12810       composer.selection.selectLine();
 12811     }
 12812   }
 12814   // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
 12815   // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
 12816   function selectElements(newBlockElements, composer) {
 12817     var range = composer.selection.createRange(),
 12818         lastEl = newBlockElements[newBlockElements.length - 1],
 12819         lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 :  lastEl.length || 0;
 12821     range.setStart(newBlockElements[0], 0);
 12822     range.setEnd(lastEl, lastOffset);
 12823     range.select();
 12824   }
 12826   // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
 12827   // Return created/modified block level elements 
 12828   // Method can be either "apply" or "remove"
 12829   function formatSelection(method, composer, options) {
 12830     var ranges = composer.selection.getOwnRanges(),
 12831         newBlockElements = [],
 12832         closestBlockName;
 12834     // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
 12835     ranges = fixNotPermittedInsertionPoints(ranges);
 12837     for (var i = ranges.length; i--;) {
 12838       fixRangeCoverage(ranges[i], composer);
 12839       closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
 12840       if (method === "remove") {
 12841         newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
 12842       } else {
 12843         newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
 12844       }
 12845     }
 12846     return newBlockElements;
 12847   }
 12849   // If properties is passed as a string, look for tag with that tagName/query 
 12850   function parseOptions(options) {
 12851     if (typeof options === "string") {
 12852       options = {
 12853         nodeName: options.toUpperCase()
 12854       };
 12855     }
 12856     return options;
 12857   }
 12859   function caretIsOnEmptyLine(composer) {
 12860     var caretInfo;
 12861     if (composer.selection.isCollapsed()) {
 12862       caretInfo = composer.selection.getNodesNearCaret();
 12863       if (caretInfo && caretInfo.caretNode) {
 12864         if (
 12865           // caret is allready breaknode
 12866           wysihtml.dom.domNode(caretInfo.caretNode).is.lineBreak() ||
 12867           // caret is textnode
 12868           (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak())) ||
 12869           // Caret is temprorary rangy selection marker
 12870           (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') &&
 12871             (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) &&
 12872             (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block())
 12873           )
 12874         ) {
 12875           return true;
 12876         }
 12877       }
 12878     }
 12879     return false;
 12880   }
 12882   wysihtml.commands.formatBlock = {
 12883     exec: function(composer, command, options) {
 12884       options = parseOptions(options);
 12885       var newBlockElements = [],
 12886           ranges, range, bookmark, state, closestBlockName;
 12888       // Find if current format state is active if options.toggle is set as true
 12889       // In toggle case active state elemets are formatted instead of working directly on selection
 12890       if (options && options.toggle) {
 12891         state = this.state(composer, command, options);
 12892       }
 12893       if (state) {
 12894         // Remove format from state nodes if toggle set and state on and selection is collapsed
 12895         bookmark = rangy.saveSelection(composer.win);
 12896         for (var j = 0, jmax = state.length; j < jmax; j++) {
 12897           removeOptionsFromElement(state[j], options, composer);
 12898         }
 12900       } else {
 12901         // If selection is caret expand it to cover nearest suitable block element or row if none found
 12902         if (composer.selection.isCollapsed()) {
 12903           bookmark = rangy.saveSelection(composer.win);
 12904           if (caretIsOnEmptyLine(composer)) {
 12905             composer.selection.selectLine();
 12906           } else {
 12907             expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
 12908           }
 12909         }
 12910         if (options) {
 12911           newBlockElements = formatSelection("apply", composer, options);
 12912         } else {
 12913           // Options == null means block formatting should be removed from selection
 12914           newBlockElements = formatSelection("remove", composer);
 12915         }
 12917       }
 12919       // Remove empty block elements that may be left behind
 12920       // Also remove them from new blocks list
 12921       newBlockElements = cleanup(composer, newBlockElements);
 12923       // Restore selection
 12924       if (bookmark) {
 12925         rangy.restoreSelection(bookmark);
 12926       } else {
 12927         selectElements(newBlockElements, composer);
 12928       }
 12929     },
 12931     // Removes all block formatting from selection
 12932     remove: function(composer, command, options) {
 12933       options = parseOptions(options);
 12934       var newBlockElements, bookmark;
 12936       // If selection is caret expand it to cover nearest suitable block element or row if none found
 12937       if (composer.selection.isCollapsed()) {
 12938         bookmark = rangy.saveSelection(composer.win);
 12939         expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
 12940       }
 12942       newBlockElements = formatSelection("remove", composer);
 12943       newBlockElements = cleanup(composer, newBlockElements);
 12945       // Restore selection
 12946       if (bookmark) {
 12947         rangy.restoreSelection(bookmark);
 12948       } else {
 12949         selectElements(newBlockElements, composer);
 12950       }
 12951     },
 12953     // If options as null is passed returns status describing all block level elements
 12954     state: function(composer, command, options) {
 12955       options = parseOptions(options);
 12957       var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
 12958             return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
 12959           }).bind(this)),
 12960           parentNodes = composer.selection.getSelectedOwnNodes(),
 12961           parent;
 12963       // Finds matching elements that are parents of selection and adds to nodes list
 12964       for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
 12965         parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
 12966         if (parent && nodes.indexOf(parent) === -1) {
 12967           nodes.push(parent);
 12968         }
 12969       }
 12971       return (nodes.length === 0) ? false : nodes;
 12972     }
 12974   };
 12975 })(wysihtml);
 12977 /**
 12978  * Unifies all inline tags additions and removals
 12979  * See https://github.com/Voog/wysihtml/pull/169 for specification of action
 12980  */
 12982 (function(wysihtml) {
 12984   var defaultTag = "SPAN",
 12985       INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u",
 12986       queryAliasMap = {
 12987         "b": "b, strong",
 12988         "strong": "b, strong",
 12989         "em": "em, i",
 12990         "i": "em, i"
 12991       };
 12993   function hasNoClass(element) {
 12994     return (/^\s*$/).test(element.className);
 12995   }
 12997   function hasNoStyle(element) {
 12998     return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style'));
 12999   }
 13001   // Associative arrays in javascript are really objects and do not have length defined
 13002   // Thus have to check emptyness in a different way
 13003   function hasNoAttributes(element) {
 13004     var attr = wysihtml.dom.getAttributes(element);
 13005     return wysihtml.lang.object(attr).isEmpty();
 13006   }
 13008   // compares two nodes if they are semantically the same
 13009   // Used in cleanup to find consequent semantically similar elements for merge
 13010   function isSameNode(element1, element2) {
 13011     var classes1, classes2,
 13012         attr1, attr2;
 13014     if (element1.nodeType !== 1 || element2.nodeType !== 1) {
 13015       return false;
 13016     }
 13018     if (element1.nodeName !== element2.nodeName) {
 13019       return false;
 13020     }
 13022     classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' ');
 13023     classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' ');
 13024     if (wysihtml.lang.array(classes1).without(classes2).length > 0) {
 13025       return false;
 13026     }
 13028     attr1 = wysihtml.dom.getAttributes(element1);
 13029     attr2 = wysihtml.dom.getAttributes(element2);
 13031     if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) {
 13032       return false;
 13033     }
 13035     return true;
 13036   }
 13038   function createWrapNode(textNode, options) {
 13039     var nodeName = options && options.nodeName || defaultTag,
 13040         element = textNode.ownerDocument.createElement(nodeName);
 13042     // Remove similar classes before applying className
 13043     if (options.classRegExp) {
 13044       element.className = element.className.replace(options.classRegExp, "");
 13045     }
 13047     if (options.className) {
 13048       element.classList.add(options.className);
 13049     }
 13051     if (options.styleProperty && typeof options.styleValue !== "undefined") {
 13052       element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
 13053     }
 13055     if (options.attribute) {
 13056       if (typeof options.attribute === "object") {
 13057         for (var a in options.attribute) {
 13058           if (options.attribute.hasOwnProperty(a)) {
 13059             element.setAttribute(a, options.attribute[a]);
 13060           }
 13061         }
 13062       } else if (typeof options.attributeValue !== "undefined") {
 13063         element.setAttribute(options.attribute, options.attributeValue);
 13064       }
 13065     }
 13067     return element;
 13068   }
 13070   // Tests if attr2 list contains all attributes present in attr1
 13071   // Note: attr 1 can have more attributes than attr2
 13072   function containsSameAttributes(attr1, attr2) {
 13073     for (var a in attr1) {
 13074       if (attr1.hasOwnProperty(a)) {
 13075         if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) {
 13076           return false;
 13077         }
 13078       }
 13079     }
 13080     return true;
 13081   }
 13083   // If attrbutes and values are the same > remove
 13084   // if attributes or values 
 13085   function updateElementAttributes(element, newAttributes, toggle) {
 13086     var attr = wysihtml.dom.getAttributes(element),
 13087         fullContain = containsSameAttributes(newAttributes, attr),
 13088         attrDifference = wysihtml.lang.object(attr).difference(newAttributes),
 13089         a, b;
 13091     if (fullContain && toggle !== false) {
 13092       for (a in newAttributes) {
 13093         if (newAttributes.hasOwnProperty(a)) {
 13094           element.removeAttribute(a);
 13095         }
 13096       }
 13097     } else {
 13099       /*if (!wysihtml.lang.object(attrDifference).isEmpty()) {
 13100         for (b in attrDifference) {
 13101           if (attrDifference.hasOwnProperty(b)) {
 13102             element.removeAttribute(b);
 13103           }
 13104         }
 13105       }*/
 13107       for (a in newAttributes) {
 13108         if (newAttributes.hasOwnProperty(a)) {
 13109           element.setAttribute(a, newAttributes[a]);
 13110         }
 13111       }
 13112     }
 13113   }
 13115   function updateFormatOfElement(element, options) {
 13116     var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch;
 13118     if (options.className) {
 13119       if (options.toggle !== false && element.classList.contains(options.className)) {
 13120         element.classList.remove(options.className);
 13121       } else {
 13122         if (options.classRegExp) {
 13123           element.className = element.className.replace(options.classRegExp, '');
 13124         }
 13125         element.classList.add(options.className);
 13126       }
 13127       if (hasNoClass(element)) {
 13128         element.removeAttribute('class');
 13129       }
 13130     }
 13132     // change/remove style
 13133     if (options.styleProperty) {
 13134       if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) {
 13135         element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
 13136       } else {
 13137         element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
 13138       }
 13139     }
 13140     if (hasNoStyle(element)) {
 13141       element.removeAttribute('style');
 13142     }
 13144     if (options.attribute) {
 13145       if (typeof options.attribute === "object") {
 13146         newAttributes =  options.attribute;
 13147       } else {
 13148         newAttributes = {};
 13149         newAttributes[options.attribute] = options.attributeValue || '';
 13150       }
 13151       updateElementAttributes(element, newAttributes, options.toggle);
 13152     }
 13155     // Handle similar semantically same elements (queryAliasMap)
 13156     nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
 13157     nodeQueryMatch = nodeNameQuery ? wysihtml.dom.domNode(element).test({ query: nodeNameQuery }) : false;
 13159     // Unwrap element if no attributes present and node name given
 13160     // or no attributes and if no nodename set but node is the default
 13161     if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) {
 13162       if (
 13163         ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) &&
 13164         hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)
 13165       ) {
 13166         wysihtml.dom.unwrap(element);
 13167       }
 13169     }
 13170   }
 13172   // Fetch all textnodes in selection
 13173   // Empty textnodes are ignored except the one containing text caret
 13174   function getSelectedTextNodes(selection, splitBounds) {
 13175     var textNodes = [];
 13177     if (!selection.isCollapsed()) {
 13178       textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) {
 13179         // Exclude empty nodes except caret node
 13180         return (!wysihtml.dom.domNode(node).is.emptyTextNode());
 13181       }, splitBounds));
 13182     }
 13184     return textNodes;
 13185   }
 13187   function findSimilarTextNodeWrapper(textNode, options, container, exact) {
 13188     var node = textNode,
 13189         similarOptions = exact ? options : correctOptionsForSimilarityCheck(options);
 13191     do {
 13192       if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) {
 13193         return node;
 13194       }
 13195       node = node.parentNode;
 13196     } while (node && node !== container);
 13198     return null;
 13199   }
 13201   function correctOptionsForSimilarityCheck(options) {
 13202     return {
 13203       nodeName: options.nodeName || null,
 13204       className: (!options.classRegExp) ? options.className || null : null,
 13205       classRegExp: options.classRegExp || null,
 13206       styleProperty: options.styleProperty || null
 13207     };
 13208   }
 13210   // Finds inline node with similar nodeName/style/className
 13211   // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes
 13212   function isSimilarNode(node, options) {
 13213     var o;
 13214     if (options.nodeName) {
 13215       var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase();
 13216       return wysihtml.dom.domNode(node).test({ query: query });
 13217     } else {
 13218       o = wysihtml.lang.object(options).clone();
 13219       o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted
 13220       return wysihtml.dom.domNode(node).test(o);
 13221     }
 13222   }
 13224   function selectRange(composer, range) {
 13225     var d = document.documentElement || document.body,
 13226         oldScrollTop  = d.scrollTop,
 13227         oldScrollLeft = d.scrollLeft,
 13228         selection = rangy.getSelection(composer.win);
 13230     rangy.getSelection(composer.win).removeAllRanges();
 13232     // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again
 13233     try {
 13234       rangy.getSelection(composer.win).addRange(range);
 13235     } catch (e) {}
 13236     if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) {
 13237       composer.element.focus();
 13238       d.scrollTop  = oldScrollTop;
 13239       d.scrollLeft = oldScrollLeft;
 13240       rangy.getSelection(composer.win).addRange(range);
 13241     }
 13242   }
 13244   function selectTextNodes(textNodes, composer) {
 13245     var range = rangy.createRange(composer.doc),
 13246         lastText = textNodes[textNodes.length - 1];
 13248     if (textNodes[0] && lastText) {
 13249       range.setStart(textNodes[0], 0);
 13250       range.setEnd(lastText, lastText.length);
 13251       selectRange(composer, range);
 13252     }
 13254   }
 13256   function selectTextNode(composer, node, start, end) {
 13257     var range = rangy.createRange(composer.doc);
 13258     if (node) {
 13259       range.setStart(node, start);
 13260       range.setEnd(node, typeof end !== 'undefined' ? end : start);
 13261       selectRange(composer, range);
 13262     }
 13263   }
 13265   function getState(composer, options, exact) {
 13266     var searchNodes = getSelectedTextNodes(composer.selection),
 13267         nodes = [],
 13268         partial = false,
 13269         node, range, caretNode;
 13271     if (composer.selection.isInThisEditable()) {
 13273       if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
 13274         caretNode = composer.selection.getSelection().anchorNode;
 13275         if (!caretNode) {
 13276           // selection not in editor
 13277           return {
 13278               nodes: [],
 13279               partial: false
 13280           };
 13281         }
 13282         if (caretNode.nodeType === 3) {
 13283           searchNodes = [caretNode];
 13284         }
 13285       }
 13287       // Handle collapsed selection caret
 13288       if (!searchNodes.length) {
 13289         range = composer.selection.getOwnRanges()[0];
 13290         if (range) {
 13291           searchNodes = [range.endContainer];
 13292         }
 13293       }
 13295       for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
 13296         node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
 13297         if (node) {
 13298           nodes.push(node);
 13299         } else {
 13300           partial = true;
 13301         }
 13302       }
 13304     }
 13306     return {
 13307       nodes: nodes,
 13308       partial: partial
 13309     };
 13310   }
 13312   // Returns if caret is inside a word in textnode (not on boundary)
 13313   // If selection anchornode is not text node, returns false
 13314   function caretIsInsideWord(selection) {
 13315     var anchor, offset, beforeChar, afterChar;
 13316     if (selection) {
 13317       anchor = selection.anchorNode;
 13318       offset = selection.anchorOffset;
 13319       if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) {
 13320         beforeChar = anchor.data[offset - 1];
 13321         afterChar = anchor.data[offset];
 13322         return (/\w/).test(beforeChar) && (/\w/).test(afterChar);
 13323       }
 13324     }
 13325     return false;
 13326   }
 13328   // Returns a range and textnode containing object from caret position covering a whole word
 13329   // wordOffsety describes the original position of caret in the new textNode 
 13330   // Caret has to be inside a textNode.
 13331   function getRangeForWord(selection) {
 13332     var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar,
 13333         txtNodes = [];
 13334     if (selection) {
 13335       anchor = selection.anchorNode;
 13336       offset = offsetStart = offsetEnd = selection.anchorOffset;
 13337       doc = anchor.ownerDocument;
 13338       range = rangy.createRange(doc);
 13340       if (anchor && anchor.nodeType === 3) {
 13342         while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) {
 13343           offsetStart--;
 13344         }
 13346         while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) {
 13347           offsetEnd++;
 13348         }
 13350         range.setStartAndEnd(anchor, offsetStart, offsetEnd);
 13351         range.splitBoundaries();
 13352         txtNodes = range.getNodes([3], function(node) {
 13353           return (!wysihtml.dom.domNode(node).is.emptyTextNode());
 13354         });
 13356         return {
 13357           wordOffset: offset - offsetStart,
 13358           range: range,
 13359           textNode: txtNodes[0]
 13360         };
 13362       }
 13363     }
 13364     return false;
 13365   }
 13367   // Contents of 2 elements are merged to fitst element. second element is removed as consequence
 13368   function mergeContents(element1, element2) {
 13369     while (element2.firstChild) {
 13370       element1.appendChild(element2.firstChild);
 13371     }
 13372     element2.parentNode.removeChild(element2);
 13373   }
 13375   function mergeConsequentSimilarElements(elements) {
 13376     for (var i = elements.length; i--;) {
 13378       if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup
 13380         if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) {
 13381           mergeContents(elements[i], elements[i].nextSibling);
 13382         }
 13384         if (elements[i].previousSibling && isSameNode(elements[i]  , elements[i].previousSibling)) {
 13385           mergeContents(elements[i].previousSibling, elements[i]);
 13386         }
 13388       }
 13389     }
 13390   }
 13392   function cleanupAndSetSelection(composer, textNodes, options) {
 13393     if (textNodes.length > 0) {
 13394       selectTextNodes(textNodes, composer);
 13395     }
 13396     mergeConsequentSimilarElements(getState(composer, options).nodes);
 13397     if (textNodes.length > 0) {
 13398       selectTextNodes(textNodes, composer);
 13399     }
 13400   }
 13402   function cleanupAndSetCaret(composer, textNode, offset, options) {
 13403     selectTextNode(composer, textNode, offset);
 13404     mergeConsequentSimilarElements(getState(composer, options).nodes);
 13405     selectTextNode(composer, textNode, offset);
 13406   }
 13408   // Formats a textnode with given options
 13409   function formatTextNode(textNode, options) {
 13410     var wrapNode = createWrapNode(textNode, options);
 13412     textNode.parentNode.insertBefore(wrapNode, textNode);
 13413     wrapNode.appendChild(textNode);
 13414   }
 13416   // Changes/toggles format of a textnode
 13417   function unformatTextNode(textNode, composer, options) {
 13418     var container = composer.element,
 13419         wrapNode = findSimilarTextNodeWrapper(textNode, options, container),
 13420         newWrapNode;
 13422     if (wrapNode) {
 13423       newWrapNode = wrapNode.cloneNode(false);
 13425       wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode);
 13426       updateFormatOfElement(newWrapNode, options);
 13427     }
 13428   }
 13430   // Removes the format around textnode
 13431   function removeFormatFromTextNode(textNode, composer, options) {
 13432     var container = composer.element,
 13433         wrapNode = findSimilarTextNodeWrapper(textNode, options, container);
 13435     if (wrapNode) {
 13436       wysihtml.dom.domNode(textNode).escapeParent(wrapNode);
 13437     }
 13438   }
 13440   // Creates node around caret formated with options
 13441   function formatTextRange(range, composer, options) {
 13442     var wrapNode = createWrapNode(range.endContainer, options);
 13444     range.surroundContents(wrapNode);
 13445     composer.selection.selectNode(wrapNode);
 13446   }
 13448   // Changes/toggles format of whole selection
 13449   function updateFormat(composer, textNodes, state, options) {
 13450     var exactState = getState(composer, options, true),
 13451         selection = composer.selection.getSelection(),
 13452         wordObj, textNode, newNode, i;
 13454     if (!textNodes.length) {
 13455       // Selection is caret
 13458       if (options.toggle !== false) {
 13459         if (caretIsInsideWord(selection)) {
 13461           // Unformat whole word 
 13462           wordObj = getRangeForWord(selection);
 13463           textNode = wordObj.textNode;
 13464           unformatTextNode(wordObj.textNode, composer, options);
 13465           cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
 13467         } else {
 13469           // Escape caret out of format
 13470           textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
 13471           newNode = state.nodes[0].cloneNode(false);
 13472           newNode.appendChild(textNode);
 13473           composer.selection.splitElementAtCaret(state.nodes[0], newNode);
 13474           updateFormatOfElement(newNode, options);
 13475           cleanupAndSetSelection(composer, [textNode], options);
 13476           var s = composer.selection.getSelection();
 13477           if (s.anchorNode && s.focusNode) {
 13478             // Has an error in IE when collapsing selection. probably from rangy
 13479             try {
 13480               s.collapseToEnd();
 13481             } catch (e) {}
 13482           }
 13483         }
 13484       } else {
 13485         // In non-toggle mode the closest state element has to be found and the state updated differently
 13486         for (i = state.nodes.length; i--;) {
 13487           updateFormatOfElement(state.nodes[i], options);
 13488         }
 13489       }
 13491     } else {
 13493       if (!exactState.partial && options.toggle !== false) {
 13495         // If whole selection (all textnodes) are in the applied format
 13496         // remove the format from selection
 13497         // Non-toggle mode never removes. Remove has to be called explicitly
 13498         for (i = textNodes.length; i--;) {
 13499           unformatTextNode(textNodes[i], composer, options);
 13500         }
 13502       } else {
 13504         // Selection is partially in format
 13505         // change it to new if format if textnode allreafy in similar state
 13506         // else just apply
 13508         for (i = textNodes.length; i--;) {
 13510           if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
 13511             unformatTextNode(textNodes[i], composer, options);
 13512           }
 13514           if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
 13515             formatTextNode(textNodes[i], options);
 13516           }
 13517         }
 13519       }
 13521       cleanupAndSetSelection(composer, textNodes, options);
 13522     }
 13523   }
 13525   // Removes format from selection
 13526   function removeFormat(composer, textNodes, state, options) {
 13527     var textNode, textOffset, newNode, i,
 13528         selection = composer.selection.getSelection();
 13530     if (!textNodes.length) {    
 13531       textNode = selection.anchorNode;
 13532       textOffset = selection.anchorOffset;
 13534       for (i = state.nodes.length; i--;) {
 13535         wysihtml.dom.unwrap(state.nodes[i]);
 13536       }
 13538       cleanupAndSetCaret(composer, textNode, textOffset, options);
 13539     } else {
 13540       for (i = textNodes.length; i--;) {
 13541         removeFormatFromTextNode(textNodes[i], composer, options);
 13542       }
 13543       cleanupAndSetSelection(composer, textNodes, options);
 13544     }
 13545   }
 13547   // Adds format to selection
 13548   function applyFormat(composer, textNodes, options) {
 13549     var wordObj, i,
 13550         selection = composer.selection.getSelection();
 13552     if (!textNodes.length) {
 13553       // Handle collapsed selection caret and return
 13554       if (caretIsInsideWord(selection)) {
 13556         wordObj = getRangeForWord(selection);
 13557         formatTextNode(wordObj.textNode, options);
 13558         cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
 13560       } else {
 13561         var r = composer.selection.getOwnRanges()[0];
 13562         if (r) {
 13563           formatTextRange(r, composer, options);
 13564         }
 13565       }
 13567     } else {
 13568       // Handle textnodes in selection and apply format
 13569       for (i = textNodes.length; i--;) {
 13570         formatTextNode(textNodes[i], options);
 13571       }
 13572       cleanupAndSetSelection(composer, textNodes, options);
 13573     }
 13574   }
 13576   // If properties is passed as a string, correct options with that nodeName
 13577   function fixOptions(options) {
 13578     options = (typeof options === "string") ? { nodeName: options } : options;
 13579     if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); }
 13580     return options;
 13581   }
 13583   wysihtml.commands.formatInline = {
 13585     // Basics:
 13586     // In case of plain text or inline state not set wrap all non-empty textnodes with
 13587     // In case a similar inline wrapper node is detected on one of textnodes, the wrapper node is changed (if fully contained) or split and changed (partially contained)
 13588     //    In case of changing mode every textnode is addressed separatly
 13589     exec: function(composer, command, options) {
 13590       options = fixOptions(options);
 13592       // Join adjactent textnodes first
 13593       composer.element.normalize();
 13595       var textNodes = getSelectedTextNodes(composer.selection, true),
 13596           state = getState(composer, options);
 13597       if (state.nodes.length > 0) {
 13598         // Text allready has the format applied
 13599         updateFormat(composer, textNodes, state, options);
 13600       } else {
 13601         // Selection is not in the applied format
 13602         applyFormat(composer, textNodes, options);
 13603       }
 13604       composer.element.normalize();
 13605     },
 13607     remove: function(composer, command, options) {
 13608       options = fixOptions(options);
 13609       composer.element.normalize();
 13611       var textNodes = getSelectedTextNodes(composer.selection, true),
 13612           state = getState(composer, options);
 13614       if (state.nodes.length > 0) {
 13615         // Text allready has the format applied
 13616         removeFormat(composer, textNodes, state, options);
 13617       }
 13619       composer.element.normalize();
 13620     },
 13622     state: function(composer, command, options) {
 13623       options = fixOptions(options);
 13624       var nodes = getState(composer, options, true).nodes;
 13625       return (nodes.length === 0) ? false : nodes;
 13626     }
 13627   };
 13629 })(wysihtml);
 13631 (function(wysihtml){
 13632   wysihtml.commands.indentList = {
 13633     exec: function(composer, command, value) {
 13634       var listEls = composer.selection.getSelectionParentsByTag('LI');
 13635       if (listEls) {
 13636         return this.tryToPushLiLevel(listEls, composer.selection);
 13637       }
 13638       return false;
 13639     },
 13641     state: function(composer, command) {
 13642         return false;
 13643     },
 13645     tryToPushLiLevel: function(liNodes, selection) {
 13646       var listTag, list, prevLi, liNode, prevLiList,
 13647           found = false;
 13649       selection.executeAndRestoreRangy(function() {
 13651         for (var i = liNodes.length; i--;) {
 13652           liNode = liNodes[i];
 13653           listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
 13654           list = liNode.ownerDocument.createElement(listTag);
 13655           prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]});
 13656           prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
 13658           if (prevLi) {
 13659             if (prevLiList) {
 13660               prevLiList.appendChild(liNode);
 13661             } else {
 13662               list.appendChild(liNode);
 13663               prevLi.appendChild(list);
 13664             }
 13665             found = true;
 13666           }
 13667         }
 13669       });
 13670       return found;
 13671     }
 13672   };
 13673 }(wysihtml));
 13675 (function(wysihtml){
 13676   wysihtml.commands.insertHTML = {
 13677     exec: function(composer, command, html) {
 13678         composer.selection.insertHTML(html);
 13679     },
 13681     state: function() {
 13682       return false;
 13683     }
 13684   };
 13685 }(wysihtml));
 13687 (function(wysihtml) {
 13688   var LINE_BREAK = "<br>" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : "");
 13690   wysihtml.commands.insertLineBreak = {
 13691     exec: function(composer, command) {
 13692       composer.selection.insertHTML(LINE_BREAK);
 13693     },
 13695     state: function() {
 13696       return false;
 13697     }
 13698   };
 13699 })(wysihtml);
 13701 wysihtml.commands.insertList = (function(wysihtml) {
 13703   var isNode = function(node, name) {
 13704     if (node && node.nodeName) {
 13705       if (typeof name === 'string') {
 13706         name = [name];
 13707       }
 13708       for (var n = name.length; n--;) {
 13709         if (node.nodeName === name[n]) {
 13710           return true;
 13711         }
 13712       }
 13713     }
 13714     return false;
 13715   };
 13717   var findListEl = function(node, nodeName, composer) {
 13718     var ret = {
 13719           el: null,
 13720           other: false
 13721         };
 13723     if (node) {
 13724       var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element),
 13725           otherNodeName = (nodeName === "UL") ? "OL" : "UL";
 13727       if (isNode(node, nodeName)) {
 13728         ret.el = node;
 13729       } else if (isNode(node, otherNodeName)) {
 13730         ret = {
 13731           el: node,
 13732           other: true
 13733         };
 13734       } else if (parentLi) {
 13735         if (isNode(parentLi.parentNode, nodeName)) {
 13736           ret.el = parentLi.parentNode;
 13737         } else if (isNode(parentLi.parentNode, otherNodeName)) {
 13738           ret = {
 13739             el : parentLi.parentNode,
 13740             other: true
 13741           };
 13742         }
 13743       }
 13744     }
 13746     // do not count list elements outside of composer
 13747     if (ret.el && !composer.element.contains(ret.el)) {
 13748       ret.el = null;
 13749     }
 13751     return ret;
 13752   };
 13754   var handleSameTypeList = function(el, nodeName, composer) {
 13755     var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
 13756         otherLists, innerLists;
 13757     // Unwrap list
 13758     // <ul><li>foo</li><li>bar</li></ul>
 13759     // becomes:
 13760     // foo<br>bar<br>
 13762     composer.selection.executeAndRestoreRangy(function() {
 13763       otherLists = getListsInSelection(otherNodeName, composer);
 13764       if (otherLists.length) {
 13765         for (var l = otherLists.length; l--;) {
 13766           wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase());
 13767         }
 13768       } else {
 13769         innerLists = getListsInSelection(['OL', 'UL'], composer);
 13770         for (var i = innerLists.length; i--;) {
 13771           wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
 13772         }
 13773         if (innerLists.length === 0) {
 13774           wysihtml.dom.resolveList(el, composer.config.useLineBreaks);
 13775         }
 13776       }
 13777     });
 13778   };
 13780   var handleOtherTypeList =  function(el, nodeName, composer) {
 13781     var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
 13782     // Turn an ordered list into an unordered list
 13783     // <ol><li>foo</li><li>bar</li></ol>
 13784     // becomes:
 13785     // <ul><li>foo</li><li>bar</li></ul>
 13786     // Also rename other lists in selection
 13787     composer.selection.executeAndRestoreRangy(function() {
 13788       var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
 13790       // All selection inner lists get renamed too
 13791       for (var l = renameLists.length; l--;) {
 13792         wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase());
 13793       }
 13794     });
 13795   };
 13797   var getListsInSelection = function(nodeName, composer) {
 13798       var ranges = composer.selection.getOwnRanges(),
 13799           renameLists = [];
 13801       for (var r = ranges.length; r--;) {
 13802         renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
 13803           return isNode(node, nodeName);
 13804         }));
 13805       }
 13807       return renameLists;
 13808   };
 13810   var createListFallback = function(nodeName, composer) {
 13811     var sel = rangy.saveSelection(composer.win);
 13813     // Fallback for Create list
 13814     var tempClassName =  "_wysihtml-temp-" + new Date().getTime(),
 13815         isEmpty, list;
 13817     composer.commands.exec("formatBlock", {
 13818       "nodeName": "div",
 13819       "className": tempClassName
 13820     });
 13822     var tempElement = composer.element.querySelector("." + tempClassName);
 13824     // This space causes new lists to never break on enter
 13825     var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
 13826     tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
 13827     if (tempElement) {
 13828       isEmpty = (/^(\s|(<br>))+$/i).test(tempElement.innerHTML);
 13829       list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer);
 13830       if (sel) {
 13831         rangy.restoreSelection(sel);
 13832       }
 13833       if (isEmpty) {
 13834         composer.selection.selectNode(list.querySelector("li"), true);
 13835       }
 13836     }
 13837   };
 13839   return {
 13840     exec: function(composer, command, nodeName) {
 13841       var doc           = composer.doc,
 13842           cmd           = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
 13843           s = composer.selection.getSelection(),
 13844           anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
 13845           fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode,
 13846           selectedNode, list;
 13848       if (s.isBackwards()) {
 13849         // swap variables
 13850         anode = [fnode, fnode = anode][0];
 13851       }
 13853       if (wysihtml.dom.domNode(fnode).is.emptyTextNode(true) && fnode) {
 13854         fnode = wysihtml.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 13855       }
 13856       if (wysihtml.dom.domNode(anode).is.emptyTextNode(true) && anode) {
 13857         anode = wysihtml.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true});
 13858       }
 13860       if (anode && fnode) {
 13861         if (anode === fnode) {
 13862           selectedNode = anode;
 13863         } else {
 13864           selectedNode = wysihtml.dom.domNode(anode).commonAncestor(fnode, composer.element);
 13865         }
 13866       } else {
 13867         selectedNode  = composer.selection.getSelectedNode();
 13868       }
 13870       list = findListEl(selectedNode, nodeName, composer);
 13872       if (!list.el) {
 13873         if (composer.commands.support(cmd)) {
 13874           doc.execCommand(cmd, false, null);
 13875         } else {
 13876           createListFallback(nodeName, composer);
 13877         }
 13878       } else if (list.other) {
 13879         handleOtherTypeList(list.el, nodeName, composer);
 13880       } else {
 13881         handleSameTypeList(list.el, nodeName, composer);
 13882       }
 13883     },
 13885     state: function(composer, command, nodeName) {
 13886       var selectedNode = composer.selection.getSelectedNode(),
 13887           list         = findListEl(selectedNode, nodeName, composer);
 13889       return (list.el && !list.other) ? list.el : false;
 13890     }
 13891   };
 13893 })(wysihtml);
 13895 (function(wysihtml){
 13897   wysihtml.commands.outdentList = {
 13898     exec: function(composer, command, value) {
 13899       var listEls = composer.selection.getSelectionParentsByTag('LI');
 13900       if (listEls) {
 13901         return this.tryToPullLiLevel(listEls, composer);
 13902       }
 13903       return false;
 13904     },
 13906     state: function(composer, command) {
 13907         return false;
 13908     },
 13910     tryToPullLiLevel: function(liNodes, composer) {
 13911       var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
 13912           found = false,
 13913           that = this;
 13915       composer.selection.executeAndRestoreRangy(function() {
 13917         for (var i = liNodes.length; i--;) {
 13918           liNode = liNodes[i];
 13919           if (liNode.parentNode) {
 13920             listNode = liNode.parentNode;
 13922             if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
 13923               found = true;
 13925               outerListNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element);
 13926               outerLiNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element);
 13928               if (outerListNode && outerLiNode) {
 13930                 if (liNode.nextSibling) {
 13931                   afterList = that.getAfterList(listNode, liNode);
 13932                   liNode.appendChild(afterList);
 13933                 }
 13934                 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
 13936               } else {
 13938                 if (liNode.nextSibling) {
 13939                   afterList = that.getAfterList(listNode, liNode);
 13940                   liNode.appendChild(afterList);
 13941                 }
 13943                 for (var j = liNode.childNodes.length; j--;) {
 13944                   listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
 13945                 }
 13947                 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
 13948                 liNode.parentNode.removeChild(liNode);
 13950               }
 13952               // cleanup
 13953               if (listNode.childNodes.length === 0) {
 13954                   listNode.parentNode.removeChild(listNode);
 13955               }
 13956             }
 13957           }
 13958         }
 13960       });
 13961       return found;
 13962     },
 13964     getAfterList: function(listNode, liNode) {
 13965       var nodeName = listNode.nodeName,
 13966           newList = document.createElement(nodeName);
 13968       while (liNode.nextSibling) {
 13969         newList.appendChild(liNode.nextSibling);
 13970       }
 13971       return newList;
 13972     }
 13974   };
 13975 }(wysihtml));
 13977 (function(wysihtml){
 13978   wysihtml.commands.redo = {
 13979     exec: function(composer) {
 13980       return composer.undoManager.redo();
 13981     },
 13983     state: function(composer) {
 13984       return false;
 13985     }
 13986   };
 13987 }(wysihtml));
 13989 (function(wysihtml) {
 13991   var nodeOptions = {
 13992     nodeName: "A"
 13993   };
 13995   wysihtml.commands.removeLink = {
 13996     exec: function(composer, command) {
 13997       wysihtml.commands.formatInline.remove(composer, command, nodeOptions);
 13998     },
 14000     state: function(composer, command) {
 14001       return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
 14002     }
 14003   };
 14005 })(wysihtml);
 14007 (function(wysihtml){
 14008   wysihtml.commands.undo = {
 14009     exec: function(composer) {
 14010       return composer.undoManager.undo();
 14011     },
 14013     state: function(composer) {
 14014       return false;
 14015     }
 14016   };
 14017 }(wysihtml));
 14019 /**
 14020  * Undo Manager for wysihtml
 14021  * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
 14022  */
 14023 (function(wysihtml) {
 14024   var Z_KEY               = 90,
 14025       Y_KEY               = 89,
 14026       BACKSPACE_KEY       = 8,
 14027       DELETE_KEY          = 46,
 14028       MAX_HISTORY_ENTRIES = 25,
 14029       DATA_ATTR_NODE      = "data-wysihtml-selection-node",
 14030       DATA_ATTR_OFFSET    = "data-wysihtml-selection-offset",
 14031       UNDO_HTML           = '<span id="_wysihtml-undo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
 14032       REDO_HTML           = '<span id="_wysihtml-redo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
 14033       dom                 = wysihtml.dom;
 14035   function cleanTempElements(doc) {
 14036     var tempElement;
 14037     while (tempElement = doc.querySelector("._wysihtml-temp")) {
 14038       tempElement.parentNode.removeChild(tempElement);
 14039     }
 14040   }
 14042   wysihtml.UndoManager = wysihtml.lang.Dispatcher.extend(
 14043     /** @scope wysihtml.UndoManager.prototype */ {
 14044     constructor: function(editor) {
 14045       this.editor = editor;
 14046       this.composer = editor.composer;
 14047       this.element = this.composer.element;
 14049       this.position = 0;
 14050       this.historyStr = [];
 14051       this.historyDom = [];
 14053       this.transact();
 14055       this._observe();
 14056     },
 14058     _observe: function() {
 14059       var that      = this,
 14060           doc       = this.composer.sandbox.getDocument(),
 14061           lastKey;
 14063       // Catch CTRL+Z and CTRL+Y
 14064       dom.observe(this.element, "keydown", function(event) {
 14065         if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
 14066           return;
 14067         }
 14069         var keyCode = event.keyCode,
 14070             isUndo = keyCode === Z_KEY && !event.shiftKey,
 14071             isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
 14073         if (isUndo) {
 14074           that.undo();
 14075           event.preventDefault();
 14076         } else if (isRedo) {
 14077           that.redo();
 14078           event.preventDefault();
 14079         }
 14080       });
 14082       // Catch delete and backspace
 14083       dom.observe(this.element, "keydown", function(event) {
 14084         var keyCode = event.keyCode;
 14085         if (keyCode === lastKey) {
 14086           return;
 14087         }
 14089         lastKey = keyCode;
 14091         if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
 14092           that.transact();
 14093         }
 14094       });
 14096       this.editor
 14097         .on("newword:composer", function() {
 14098           that.transact();
 14099         })
 14101         .on("beforecommand:composer", function() {
 14102           that.transact();
 14103         });
 14104     },
 14106     transact: function() {
 14107       var previousHtml      = this.historyStr[this.position - 1],
 14108           currentHtml       = this.composer.getValue(false, false),
 14109           composerIsVisible   = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
 14110           range, node, offset, element, position;
 14112       if (currentHtml === previousHtml) {
 14113         return;
 14114       }
 14116       var length = this.historyStr.length = this.historyDom.length = this.position;
 14117       if (length > MAX_HISTORY_ENTRIES) {
 14118         this.historyStr.shift();
 14119         this.historyDom.shift();
 14120         this.position--;
 14121       }
 14123       this.position++;
 14125       if (composerIsVisible) {
 14126         // Do not start saving selection if composer is not visible
 14127         range   = this.composer.selection.getRange();
 14128         node    = (range && range.startContainer) ? range.startContainer : this.element;
 14129         offset  = (range && range.startOffset) ? range.startOffset : 0;
 14131         if (node.nodeType === wysihtml.ELEMENT_NODE) {
 14132           element = node;
 14133         } else {
 14134           element  = node.parentNode;
 14135           position = this.getChildNodeIndex(element, node);
 14136         }
 14138         element.setAttribute(DATA_ATTR_OFFSET, offset);
 14139         if (typeof(position) !== "undefined") {
 14140           element.setAttribute(DATA_ATTR_NODE, position);
 14141         }
 14142       }
 14144       var clone = this.element.cloneNode(!!currentHtml);
 14145       this.historyDom.push(clone);
 14146       this.historyStr.push(currentHtml);
 14148       if (element) {
 14149         element.removeAttribute(DATA_ATTR_OFFSET);
 14150         element.removeAttribute(DATA_ATTR_NODE);
 14151       }
 14153     },
 14155     undo: function() {
 14156       this.transact();
 14158       if (!this.undoPossible()) {
 14159         return;
 14160       }
 14162       this.set(this.historyDom[--this.position - 1]);
 14163       this.editor.fire("undo:composer");
 14164     },
 14166     redo: function() {
 14167       if (!this.redoPossible()) {
 14168         return;
 14169       }
 14171       this.set(this.historyDom[++this.position - 1]);
 14172       this.editor.fire("redo:composer");
 14173     },
 14175     undoPossible: function() {
 14176       return this.position > 1;
 14177     },
 14179     redoPossible: function() {
 14180       return this.position < this.historyStr.length;
 14181     },
 14183     set: function(historyEntry) {
 14184       this.element.innerHTML = "";
 14186       var i = 0,
 14187           childNodes = historyEntry.childNodes,
 14188           length = historyEntry.childNodes.length;
 14190       for (; i<length; i++) {
 14191         this.element.appendChild(childNodes[i].cloneNode(true));
 14192       }
 14194       // Restore selection
 14195       var offset,
 14196           node,
 14197           position;
 14199       if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
 14200         offset    = historyEntry.getAttribute(DATA_ATTR_OFFSET);
 14201         position  = historyEntry.getAttribute(DATA_ATTR_NODE);
 14202         node      = this.element;
 14203       } else {
 14204         node      = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
 14205         offset    = node.getAttribute(DATA_ATTR_OFFSET);
 14206         position  = node.getAttribute(DATA_ATTR_NODE);
 14207         node.removeAttribute(DATA_ATTR_OFFSET);
 14208         node.removeAttribute(DATA_ATTR_NODE);
 14209       }
 14211       if (position !== null) {
 14212         node = this.getChildNodeByIndex(node, +position);
 14213       }
 14215       this.composer.selection.set(node, offset);
 14216     },
 14218     getChildNodeIndex: function(parent, child) {
 14219       var i           = 0,
 14220           childNodes  = parent.childNodes,
 14221           length      = childNodes.length;
 14222       for (; i<length; i++) {
 14223         if (childNodes[i] === child) {
 14224           return i;
 14225         }
 14226       }
 14227     },
 14229     getChildNodeByIndex: function(parent, index) {
 14230       return parent.childNodes[index];
 14231     }
 14232   });
 14233 })(wysihtml);
 14235 /**
 14236  * TODO: the following methods still need unit test coverage
 14237  */
 14238 wysihtml.views.View = Base.extend(
 14239   /** @scope wysihtml.views.View.prototype */ {
 14240   constructor: function(parent, textareaElement, config) {
 14241     this.parent   = parent;
 14242     this.element  = textareaElement;
 14243     this.config   = config;
 14244     if (!this.config.noTextarea) {
 14245         this._observeViewChange();
 14246     }
 14247   },
 14249   _observeViewChange: function() {
 14250     var that = this;
 14251     this.parent.on("beforeload", function() {
 14252       that.parent.on("change_view", function(view) {
 14253         if (view === that.name) {
 14254           that.parent.currentView = that;
 14255           that.show();
 14256           // Using tiny delay here to make sure that the placeholder is set before focusing
 14257           setTimeout(function() { that.focus(); }, 0);
 14258         } else {
 14259           that.hide();
 14260         }
 14261       });
 14262     });
 14263   },
 14265   focus: function() {
 14266     if (this.element && this.element.ownerDocument && this.element.ownerDocument.querySelector(":focus") === this.element) {
 14267       return;
 14268     }
 14270     try { if(this.element) { this.element.focus(); } } catch(e) {}
 14271   },
 14273   hide: function() {
 14274     this.element.style.display = "none";
 14275   },
 14277   show: function() {
 14278     this.element.style.display = "";
 14279   },
 14281   disable: function() {
 14282     this.element.setAttribute("disabled", "disabled");
 14283   },
 14285   enable: function() {
 14286     this.element.removeAttribute("disabled");
 14287   }
 14288 });
 14290 (function(wysihtml) {
 14291   var dom       = wysihtml.dom,
 14292       browser   = wysihtml.browser;
 14294   wysihtml.views.Composer = wysihtml.views.View.extend(
 14295     /** @scope wysihtml.views.Composer.prototype */ {
 14296     name: "composer",
 14298     constructor: function(parent, editableElement, config) {
 14299       this.base(parent, editableElement, config);
 14300       if (!this.config.noTextarea) {
 14301           this.textarea = this.parent.textarea;
 14302       } else {
 14303           this.editableArea = editableElement;
 14304       }
 14305       if (this.config.contentEditableMode) {
 14306           this._initContentEditableArea();
 14307       } else {
 14308           this._initSandbox();
 14309       }
 14310     },
 14312     clear: function() {
 14313       this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : "<br>";
 14314     },
 14316     getValue: function(parse, clearInternals) {
 14317       var value = this.isEmpty() ? "" : wysihtml.quirks.getCorrectInnerHTML(this.element);
 14318       if (parse !== false) {
 14319         value = this.parent.parse(value, (clearInternals === false) ? false : true);
 14320       }
 14321       return value;
 14322     },
 14324     setValue: function(html, parse) {
 14325       if (parse !== false) {
 14326         html = this.parent.parse(html);
 14327       }
 14329       try {
 14330         this.element.innerHTML = html;
 14331       } catch (e) {
 14332         this.element.innerText = html;
 14333       }
 14334     },
 14336     cleanUp: function(rules) {
 14337       var bookmark;
 14338       if (this.selection && this.selection.isInThisEditable()) {
 14339         bookmark = rangy.saveSelection(this.win);
 14340       }
 14341       this.parent.parse(this.element, undefined, rules);
 14342       if (bookmark) {
 14343         rangy.restoreSelection(bookmark);
 14344       }
 14345     },
 14347     show: function() {
 14348       this.editableArea.style.display = this._displayStyle || "";
 14350       if (!this.config.noTextarea && !this.textarea.element.disabled) {
 14351         // Firefox needs this, otherwise contentEditable becomes uneditable
 14352         this.disable();
 14353         this.enable();
 14354       }
 14355     },
 14357     hide: function() {
 14358       this._displayStyle = dom.getStyle("display").from(this.editableArea);
 14359       if (this._displayStyle === "none") {
 14360         this._displayStyle = null;
 14361       }
 14362       this.editableArea.style.display = "none";
 14363     },
 14365     disable: function() {
 14366       this.parent.fire("disable:composer");
 14367       this.element.removeAttribute("contentEditable");
 14368     },
 14370     enable: function() {
 14371       this.parent.fire("enable:composer");
 14372       this.element.setAttribute("contentEditable", "true");
 14373     },
 14375     focus: function(setToEnd) {
 14376       // IE 8 fires the focus event after .focus()
 14377       // This is needed by our simulate_placeholder.js to work
 14378       // therefore we clear it ourselves this time
 14379       if (wysihtml.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
 14380         this.clear();
 14381       }
 14383       this.base();
 14385       var lastChild = this.element.lastChild;
 14386       if (setToEnd && lastChild && this.selection) {
 14387         if (lastChild.nodeName === "BR") {
 14388           this.selection.setBefore(this.element.lastChild);
 14389         } else {
 14390           this.selection.setAfter(this.element.lastChild);
 14391         }
 14392       }
 14393     },
 14395     getScrollPos: function() {
 14396       if (this.doc && this.win) {
 14397         var pos = {};
 14399         if (typeof this.win.pageYOffset !== "undefined") {
 14400           pos.y = this.win.pageYOffset;
 14401         } else {
 14402           pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop;
 14403         }
 14405         if (typeof this.win.pageXOffset !== "undefined") {
 14406           pos.x = this.win.pageXOffset;
 14407         } else {
 14408           pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft;
 14409         }
 14411         return pos;
 14412       }
 14413     },
 14415     setScrollPos: function(pos) {
 14416       if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") {
 14417         this.win.scrollTo(pos.x, pos.y);
 14418       }
 14419     },
 14421     getTextContent: function() {
 14422       return dom.getTextContent(this.element);
 14423     },
 14425     hasPlaceholderSet: function() {
 14426       return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
 14427     },
 14429     isEmpty: function() {
 14430       var innerHTML = this.element.innerHTML.toLowerCase();
 14431       return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML)  ||
 14432              innerHTML === ""            ||
 14433              innerHTML === "<br>"        ||
 14434              innerHTML === "<p></p>"     ||
 14435              innerHTML === "<p><br></p>" ||
 14436              this.hasPlaceholderSet();
 14437     },
 14439     _initContentEditableArea: function() {
 14440         var that = this;
 14441         if (this.config.noTextarea) {
 14442             this.sandbox = new dom.ContentEditableArea(function() {
 14443                 that._create();
 14444             }, {
 14445               className: this.config.classNames.sandbox
 14446             }, this.editableArea);
 14447         } else {
 14448             this.sandbox = new dom.ContentEditableArea(function() {
 14449                 that._create();
 14450             }, {
 14451               className: this.config.classNames.sandbox
 14452             });
 14453             this.editableArea = this.sandbox.getContentEditable();
 14454             dom.insert(this.editableArea).after(this.textarea.element);
 14455             this._createWysiwygFormField();
 14456         }
 14457     },
 14459     _initSandbox: function() {
 14460       var that = this;
 14461       this.sandbox = new dom.Sandbox(function() {
 14462         that._create();
 14463       }, {
 14464         stylesheets:  this.config.stylesheets,
 14465         className: this.config.classNames.sandbox
 14466       });
 14467       this.editableArea  = this.sandbox.getIframe();
 14469       var textareaElement = this.textarea.element;
 14470       dom.insert(this.editableArea).after(textareaElement);
 14472       this._createWysiwygFormField();
 14473     },
 14475     // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
 14476     _createWysiwygFormField: function() {
 14477         if (this.textarea.element.form) {
 14478           var hiddenField = document.createElement("input");
 14479           hiddenField.type   = "hidden";
 14480           hiddenField.name   = "_wysihtml_mode";
 14481           hiddenField.value  = 1;
 14482           dom.insert(hiddenField).after(this.textarea.element);
 14483         }
 14484     },
 14486     _create: function() {
 14487       var that = this;
 14488       this.doc                = this.sandbox.getDocument();
 14489       this.win                = this.sandbox.getWindow();
 14490       this.element            = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
 14491       if (!this.config.noTextarea) {
 14492           this.textarea           = this.parent.textarea;
 14493           this.element.innerHTML  = this.textarea.getValue(true, false);
 14494       } else {
 14495           this.cleanUp(); // cleans contenteditable on initiation as it may contain html
 14496       }
 14498       // Make sure our selection handler is ready
 14499       this.selection = new wysihtml.Selection(this.parent, this.element, this.config.classNames.uneditableContainer);
 14501       // Make sure commands dispatcher is ready
 14502       this.commands  = new wysihtml.Commands(this.parent);
 14504       if (!this.config.noTextarea) {
 14505           dom.copyAttributes([
 14506               "className", "spellcheck", "title", "lang", "dir", "accessKey"
 14507           ]).from(this.textarea.element).to(this.element);
 14508       }
 14510       this._initAutoLinking();
 14512       dom.addClass(this.element, this.config.classNames.composer);
 14513       //
 14514       // Make the editor look like the original textarea, by syncing styles
 14515       if (this.config.style && !this.config.contentEditableMode) {
 14516         this.style();
 14517       }
 14519       this.observe();
 14521       var name = this.config.name;
 14522       if (name) {
 14523         dom.addClass(this.element, name);
 14524         if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
 14525       }
 14527       this.enable();
 14529       if (!this.config.noTextarea && this.textarea.element.disabled) {
 14530         this.disable();
 14531       }
 14533       // Simulate html5 placeholder attribute on contentEditable element
 14534       var placeholderText = typeof(this.config.placeholder) === "string"
 14535         ? this.config.placeholder
 14536         : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
 14537       if (placeholderText) {
 14538         dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder);
 14539       }
 14541       // Make sure that the browser avoids using inline styles whenever possible
 14542       this.commands.exec("styleWithCSS", false);
 14544       this._initObjectResizing();
 14545       this._initUndoManager();
 14546       this._initLineBreaking();
 14548       // Simulate html5 autofocus on contentEditable element
 14549       // This doesn't work on IOS (5.1.1)
 14550       if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
 14551         setTimeout(function() { that.focus(true); }, 100);
 14552       }
 14554       // IE sometimes leaves a single paragraph, which can't be removed by the user
 14555       if (!browser.clearsContentEditableCorrectly()) {
 14556         wysihtml.quirks.ensureProperClearing(this);
 14557       }
 14559       // Set up a sync that makes sure that textarea and editor have the same content
 14560       if (this.initSync && this.config.sync) {
 14561         this.initSync();
 14562       }
 14564       // Okay hide the textarea, we are ready to go
 14565       if (!this.config.noTextarea) { this.textarea.hide(); }
 14567       // Fire global (before-)load event
 14568       this.parent.fire("beforeload").fire("load");
 14569     },
 14571     _initAutoLinking: function() {
 14572       var that                           = this,
 14573           supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
 14574           supportsAutoLinking            = browser.doesAutoLinkingInContentEditable();
 14576       if (supportsDisablingOfAutoLinking) {
 14577         this.commands.exec("AutoUrlDetect", false, false);
 14578       }
 14580       if (!this.config.autoLink) {
 14581         return;
 14582       }
 14584       // Only do the auto linking by ourselves when the browser doesn't support auto linking
 14585       // OR when he supports auto linking but we were able to turn it off (IE9+)
 14586       if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
 14587         this.parent.on("newword:composer", function() {
 14588           if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
 14589             var nodeWithSelection = that.selection.getSelectedNode(),
 14590                 uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer),
 14591                 isInUneditable = false;
 14593             for (var i = uneditables.length; i--;) {
 14594               if (wysihtml.dom.contains(uneditables[i], nodeWithSelection)) {
 14595                 isInUneditable = true;
 14596               }
 14597             }
 14599             if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]);
 14600           }
 14601         });
 14603         dom.observe(this.element, "blur", function() {
 14604           dom.autoLink(that.element, [that.config.classNames.uneditableContainer]);
 14605         });
 14606       }
 14608       // Assuming we have the following:
 14609       //  <a href="http://www.google.de">http://www.google.de</a>
 14610       // If a user now changes the url in the innerHTML we want to make sure that
 14611       // it's synchronized with the href attribute (as long as the innerHTML is still a url)
 14612       var // Use a live NodeList to check whether there are any links in the document
 14613           links           = this.sandbox.getDocument().getElementsByTagName("a"),
 14614           // The autoLink helper method reveals a reg exp to detect correct urls
 14615           urlRegExp       = dom.autoLink.URL_REG_EXP,
 14616           getTextContent  = function(element) {
 14617             var textContent = wysihtml.lang.string(dom.getTextContent(element)).trim();
 14618             if (textContent.substr(0, 4) === "www.") {
 14619               textContent = "http://" + textContent;
 14620             }
 14621             return textContent;
 14622           };
 14624       dom.observe(this.element, "keydown", function(event) {
 14625         if (!links.length) {
 14626           return;
 14627         }
 14629         var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
 14630             link         = dom.getParentElement(selectedNode, { query: "a" }, 4),
 14631             textContent;
 14633         if (!link) {
 14634           return;
 14635         }
 14637         textContent = getTextContent(link);
 14638         // keydown is fired before the actual content is changed
 14639         // therefore we set a timeout to change the href
 14640         setTimeout(function() {
 14641           var newTextContent = getTextContent(link);
 14642           if (newTextContent === textContent) {
 14643             return;
 14644           }
 14646           // Only set href when new href looks like a valid url
 14647           if (newTextContent.match(urlRegExp)) {
 14648             link.setAttribute("href", newTextContent);
 14649           }
 14650         }, 0);
 14651       });
 14652     },
 14654     _initObjectResizing: function() {
 14655       this.commands.exec("enableObjectResizing", true);
 14657       // IE sets inline styles after resizing objects
 14658       // The following lines make sure that the width/height css properties
 14659       // are copied over to the width/height attributes
 14660       if (browser.supportsEvent("resizeend")) {
 14661         var properties        = ["width", "height"],
 14662             propertiesLength  = properties.length,
 14663             element           = this.element;
 14665         dom.observe(element, "resizeend", function(event) {
 14666           var target = event.target || event.srcElement,
 14667               style  = target.style,
 14668               i      = 0,
 14669               property;
 14671           if (target.nodeName !== "IMG") {
 14672             return;
 14673           }
 14675           for (; i<propertiesLength; i++) {
 14676             property = properties[i];
 14677             if (style[property]) {
 14678               target.setAttribute(property, parseInt(style[property], 10));
 14679               style[property] = "";
 14680             }
 14681           }
 14683           // After resizing IE sometimes forgets to remove the old resize handles
 14684           wysihtml.quirks.redraw(element);
 14685         });
 14686       }
 14687     },
 14689     _initUndoManager: function() {
 14690       this.undoManager = new wysihtml.UndoManager(this.parent);
 14691     },
 14693     _initLineBreaking: function() {
 14694       var that                              = this,
 14695           USE_NATIVE_LINE_BREAK_INSIDE_TAGS = "li, p, h1, h2, h3, h4, h5, h6",
 14696           LIST_TAGS                         = "ul, ol, menu";
 14698       function adjust(selectedNode) {
 14699         var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2);
 14700         if (parentElement && dom.contains(that.element, parentElement)) {
 14701           that.selection.executeAndRestoreRangy(function() {
 14702             if (that.config.useLineBreaks) {
 14703               if (!parentElement.firstChild || (parentElement.firstChild === parentElement.lastChild && parentElement.firstChild.nodeType === 1 && parentElement.firstChild.classList.contains('rangySelectionBoundary'))) {
 14704                 parentElement.appendChild(that.doc.createElement('br'));
 14705               }
 14706               dom.replaceWithChildNodes(parentElement);
 14707             } else if (parentElement.nodeName !== "P") {
 14708               dom.renameElement(parentElement, "p");
 14709             }
 14710           });
 14711         }
 14712       }
 14714       // Ensures when editor is empty and not line breaks mode, the inital state has a paragraph in it on focus with caret inside paragraph
 14715       if (!this.config.useLineBreaks) {
 14716         dom.observe(this.element, ["focus"], function() {
 14717           if (that.isEmpty()) {
 14718             setTimeout(function() {
 14719               var paragraph = that.doc.createElement("P");
 14720               that.element.innerHTML = "";
 14721               that.element.appendChild(paragraph);
 14722               if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
 14723                 paragraph.innerHTML = "<br>";
 14724                 that.selection.setBefore(paragraph.firstChild);
 14725               } else {
 14726                 that.selection.selectNode(paragraph, true);
 14727               }
 14728             }, 0);
 14729           }
 14730         });
 14731       }
 14733       dom.observe(this.element, "keydown", function(event) {
 14734         var keyCode = event.keyCode;
 14736         if (event.shiftKey || event.ctrlKey || event.defaultPrevented) {
 14737           return;
 14738         }
 14740         if (keyCode !== wysihtml.ENTER_KEY && keyCode !== wysihtml.BACKSPACE_KEY) {
 14741           return;
 14742         }
 14743         var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
 14744         if (blockElement) {
 14745           setTimeout(function() {
 14746             // Unwrap paragraph after leaving a list or a H1-6
 14747             var selectedNode = that.selection.getSelectedNode(),
 14748                 list;
 14750             if (blockElement.nodeName === "LI") {
 14751               if (!selectedNode) {
 14752                 return;
 14753               }
 14755               list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2);
 14757               if (!list) {
 14758                 adjust(selectedNode);
 14759               }
 14760             }
 14762             if (keyCode === wysihtml.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
 14763               adjust(selectedNode);
 14764             }
 14765           }, 0);
 14766           return;
 14767         }
 14768         if (that.config.useLineBreaks && keyCode === wysihtml.ENTER_KEY && !wysihtml.browser.insertsLineBreaksOnReturn()) {
 14769           event.preventDefault();
 14770           that.commands.exec("insertLineBreak");
 14771         }
 14772       });
 14773     }
 14774   });
 14775 })(wysihtml);
 14777 (function(wysihtml) {
 14778   var dom             = wysihtml.dom,
 14779       doc             = document,
 14780       win             = window,
 14781       HOST_TEMPLATE   = doc.createElement("div"),
 14782       /**
 14783        * Styles to copy from textarea to the composer element
 14784        */
 14785       TEXT_FORMATTING = [
 14786         "background-color",
 14787         "color", "cursor",
 14788         "font-family", "font-size", "font-style", "font-variant", "font-weight",
 14789         "line-height", "letter-spacing",
 14790         "text-align", "text-decoration", "text-indent", "text-rendering",
 14791         "word-break", "word-wrap", "word-spacing"
 14792       ],
 14793       /**
 14794        * Styles to copy from textarea to the iframe
 14795        */
 14796       BOX_FORMATTING = [
 14797         "background-color",
 14798         "border-collapse",
 14799         "border-bottom-color", "border-bottom-style", "border-bottom-width",
 14800         "border-left-color", "border-left-style", "border-left-width",
 14801         "border-right-color", "border-right-style", "border-right-width",
 14802         "border-top-color", "border-top-style", "border-top-width",
 14803         "clear", "display", "float",
 14804         "margin-bottom", "margin-left", "margin-right", "margin-top",
 14805         "outline-color", "outline-offset", "outline-width", "outline-style",
 14806         "padding-left", "padding-right", "padding-top", "padding-bottom",
 14807         "position", "top", "left", "right", "bottom", "z-index",
 14808         "vertical-align", "text-align",
 14809         "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
 14810         "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
 14811         "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
 14812         "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
 14813         "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
 14814         "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
 14815         "width", "height"
 14816       ],
 14817       ADDITIONAL_CSS_RULES = [
 14818         "html                 { height: 100%; }",
 14819         "body                 { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }",
 14820         "body > p:first-child { margin-top: 0; }",
 14821         "._wysihtml-temp     { display: none; }",
 14822         wysihtml.browser.isGecko ?
 14823           "body.placeholder { color: graytext !important; }" :
 14824           "body.placeholder { color: #a9a9a9 !important; }",
 14825         // Ensure that user see's broken images and can delete them
 14826         "img:-moz-broken      { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
 14827       ];
 14829   /**
 14830    * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
 14831    * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
 14832    *
 14833    * Other browsers need a more hacky way: (pssst don't tell my mama)
 14834    * In order to prevent the element being scrolled into view when focusing it, we simply
 14835    * move it out of the scrollable area, focus it, and reset it's position
 14836    */
 14837   var focusWithoutScrolling = function(element) {
 14838     if (element.setActive) {
 14839       // Following line could cause a js error when the textarea is invisible
 14840       // See https://github.com/xing/wysihtml5/issues/9
 14841       try { element.setActive(); } catch(e) {}
 14842     } else {
 14843       var elementStyle = element.style,
 14844           originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
 14845           originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
 14846           originalStyles = {
 14847             position:         elementStyle.position,
 14848             top:              elementStyle.top,
 14849             left:             elementStyle.left,
 14850             WebkitUserSelect: elementStyle.WebkitUserSelect
 14851           };
 14853       dom.setStyles({
 14854         position:         "absolute",
 14855         top:              "-99999px",
 14856         left:             "-99999px",
 14857         // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
 14858         WebkitUserSelect: "none"
 14859       }).on(element);
 14861       element.focus();
 14863       dom.setStyles(originalStyles).on(element);
 14865       if (win.scrollTo) {
 14866         // Some browser extensions unset this method to prevent annoyances
 14867         // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
 14868         // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
 14869         win.scrollTo(originalScrollLeft, originalScrollTop);
 14870       }
 14871     }
 14872   };
 14875   wysihtml.views.Composer.prototype.style = function() {
 14876     var that                  = this,
 14877         originalActiveElement = doc.querySelector(":focus"),
 14878         textareaElement       = this.textarea.element,
 14879         hasPlaceholder        = textareaElement.hasAttribute("placeholder"),
 14880         originalPlaceholder   = hasPlaceholder && textareaElement.getAttribute("placeholder"),
 14881         originalDisplayValue  = textareaElement.style.display,
 14882         originalDisabled      = textareaElement.disabled,
 14883         displayValueForCopying;
 14885     this.focusStylesHost      = HOST_TEMPLATE.cloneNode(false);
 14886     this.blurStylesHost       = HOST_TEMPLATE.cloneNode(false);
 14887     this.disabledStylesHost   = HOST_TEMPLATE.cloneNode(false);
 14889     // Remove placeholder before copying (as the placeholder has an affect on the computed style)
 14890     if (hasPlaceholder) {
 14891       textareaElement.removeAttribute("placeholder");
 14892     }
 14894     if (textareaElement === originalActiveElement) {
 14895       textareaElement.blur();
 14896     }
 14898     // enable for copying styles
 14899     textareaElement.disabled = false;
 14901     // set textarea to display="none" to get cascaded styles via getComputedStyle
 14902     textareaElement.style.display = displayValueForCopying = "none";
 14904     if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
 14905         (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
 14906       textareaElement.style.display = displayValueForCopying = originalDisplayValue;
 14907     }
 14909     // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
 14910     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
 14912     // --------- editor styles ---------
 14913     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
 14915     // --------- apply standard rules ---------
 14916     dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
 14918     // --------- :disabled styles ---------
 14919     textareaElement.disabled = true;
 14920     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
 14921     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
 14922     textareaElement.disabled = originalDisabled;
 14924     // --------- :focus styles ---------
 14925     textareaElement.style.display = originalDisplayValue;
 14926     focusWithoutScrolling(textareaElement);
 14927     textareaElement.style.display = displayValueForCopying;
 14929     dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
 14930     dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
 14932     // reset textarea
 14933     textareaElement.style.display = originalDisplayValue;
 14935     dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
 14937     // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
 14938     // this is needed for when the change_view event is fired where the iframe is hidden and then
 14939     // the blur event fires and re-displays it
 14940     var boxFormattingStyles = wysihtml.lang.array(BOX_FORMATTING).without(["display"]);
 14942     // --------- restore focus ---------
 14943     if (originalActiveElement) {
 14944       focusWithoutScrolling(originalActiveElement);
 14945     } else {
 14946       textareaElement.blur();
 14947     }
 14949     // --------- restore placeholder ---------
 14950     if (hasPlaceholder) {
 14951       textareaElement.setAttribute("placeholder", originalPlaceholder);
 14952     }
 14954     // --------- Sync focus/blur styles ---------
 14955     this.parent.on("focus:composer", function() {
 14956       dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
 14957       dom.copyStyles(TEXT_FORMATTING)     .from(that.focusStylesHost).to(that.element);
 14958     });
 14960     this.parent.on("blur:composer", function() {
 14961       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
 14962       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
 14963     });
 14965     this.parent.observe("disable:composer", function() {
 14966       dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
 14967       dom.copyStyles(TEXT_FORMATTING)     .from(that.disabledStylesHost).to(that.element);
 14968     });
 14970     this.parent.observe("enable:composer", function() {
 14971       dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
 14972       dom.copyStyles(TEXT_FORMATTING)     .from(that.blurStylesHost).to(that.element);
 14973     });
 14975     return this;
 14976   };
 14977 })(wysihtml);
 14979 /**
 14980  * Taking care of events
 14981  *  - Simulating 'change' event on contentEditable element
 14982  *  - Handling drag & drop logic
 14983  *  - Catch paste events
 14984  *  - Dispatch proprietary newword:composer event
 14985  *  - Keyboard shortcuts
 14986  */
 14987 (function(wysihtml) {
 14988   var dom       = wysihtml.dom,
 14989       domNode = dom.domNode,
 14990       browser   = wysihtml.browser,
 14991       /**
 14992        * Map keyCodes to query commands
 14993        */
 14994       shortcuts = {
 14995         "66": "bold",     // B
 14996         "73": "italic",   // I
 14997         "85": "underline" // U
 14998       };
 15000   var actions = {
 15002     // Adds multiple eventlisteners to target, bound to one callback
 15003     // TODO: If needed elsewhere make it part of wysihtml.dom or sth
 15004     addListeners: function (target, events, callback) {
 15005       for(var i = 0, max = events.length; i < max; i++) {
 15006         target.addEventListener(events[i], callback, false);
 15007       }
 15008     },
 15010     // Removes multiple eventlisteners from target, bound to one callback
 15011     // TODO: If needed elsewhere make it part of wysihtml.dom or sth
 15012     removeListeners: function (target, events, callback) {
 15013       for(var i = 0, max = events.length; i < max; i++) {
 15014         target.removeEventListener(events[i], callback, false);
 15015       }
 15016     },
 15018     // Override for giving user ability to delete last line break in table cell
 15019     fixLastBrDeletionInTable: function(composer, force) {
 15020       if (composer.selection.caretIsInTheEndOfNode()) {
 15021         var sel = composer.selection.getSelection(),
 15022             aNode = sel.anchorNode;
 15023         if (aNode && aNode.nodeType === 1 && (wysihtml.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) {
 15024           var nextNode = aNode.childNodes[sel.anchorOffset];
 15025           if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") {
 15026             nextNode.parentNode.removeChild(nextNode);
 15027             return true;
 15028           }
 15029         }
 15030       }
 15031       return false;
 15032     },
 15034     // If found an uneditable before caret then notify it before deletion
 15035     handleUneditableDeletion: function(composer) {
 15036       var before = composer.selection.getBeforeSelection(true);
 15037       if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) {
 15038         if (actions.fixLastBrDeletionInTable(composer, true)) {
 15039           return true;
 15040         }
 15041         try {
 15042           var ev = new CustomEvent("wysihtml:uneditable:delete", {bubbles: true, cancelable: false});
 15043           before.node.dispatchEvent(ev);
 15044         } catch (err) {}
 15045         before.node.parentNode.removeChild(before.node);
 15046         return true;
 15047       }
 15048       return false;
 15049     },
 15051     // Deletion with caret in the beginning of headings and other block elvel elements needs special attention
 15052     // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
 15053     fixDeleteInTheBeginningOfBlock: function(composer) {
 15054       var selection = composer.selection,
 15055           prevNode = selection.getPreviousNode();
 15057       if (selection.caretIsFirstInSelection(wysihtml.browser.usesControlRanges()) && prevNode) {
 15058         if (prevNode.nodeType === 1 &&
 15059             wysihtml.dom.domNode(prevNode).is.block() &&
 15060             !domNode(prevNode).test({
 15061               query: "ol, ul, table, tr, dl"
 15062             })
 15063         ) {
 15064           if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
 15065             // If heading is empty remove the heading node
 15066             prevNode.parentNode.removeChild(prevNode);
 15067             return true;
 15068           } else {
 15069             if (prevNode.lastChild) {
 15070               var selNode = prevNode.lastChild,
 15071                   selectedNode = selection.getSelectedNode(),
 15072                   commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element),
 15073                   curNode = wysihtml.dom.getParentElement(selectedNode, {
 15074                     query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
 15075                   }, false, commonAncestorNode || composer.element);
 15077               if (curNode) {
 15078                 domNode(curNode).transferContentTo(prevNode, true);
 15079                 selection.setAfter(selNode);
 15080                 return true;
 15081               } else if (wysihtml.browser.usesControlRanges()) {
 15082                 selectedNode = selection.getCaretNode();
 15083                 domNode(selectedNode).transferContentTo(prevNode, true);
 15084                 selection.setAfter(selNode);
 15085                 return true;
 15086               }
 15087             }
 15088           }
 15089         }
 15090       }
 15091       return false;
 15092     },
 15094     /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */
 15095     /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */
 15096     fixDeleteInTheBeginningOfLi: function(composer) {
 15097       if (wysihtml.browser.hasLiDeletingProblem()) {
 15098         var selection = composer.selection.getSelection(),
 15099             aNode = selection.anchorNode,
 15100             listNode, prevNode, firstNode,
 15101             isInBeginnig = composer.selection.caretIsFirstInSelection(),
 15102             prevNode,
 15103             intermediaryNode;
 15105         // Fix caret at the beginnig of first textNode in LI
 15106         if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) {
 15107           aNode = aNode.parentNode;
 15108           isInBeginnig = true;
 15109         }
 15111         if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") {
 15112           prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 15113           if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) {
 15114             prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
 15115             intermediaryNode = aNode.parentNode;
 15116           }
 15117           if (prevNode) {
 15118             firstNode = aNode.firstChild;
 15119             domNode(aNode).transferContentTo(prevNode, true);
 15121             if (intermediaryNode && intermediaryNode.children.length === 0){
 15122               intermediaryNode.remove();
 15123             }
 15125             if (firstNode) {
 15126               composer.selection.setBefore(firstNode);
 15127             } else if (prevNode) {
 15128               if (prevNode.nodeType === 1) {
 15129                 if (prevNode.lastChild) {
 15130                   composer.selection.setAfter(prevNode.lastChild);
 15131                 } else {
 15132                   composer.selection.selectNode(prevNode);
 15133                 }
 15134               } else {
 15135                 composer.selection.setAfter(prevNode);
 15136               }
 15137             }
 15138             return true;
 15139           }
 15140         }
 15141       }
 15142       return false;
 15143     },
 15145     fixDeleteInTheBeginningOfControlSelection: function(composer) {
 15146       var selection = composer.selection,
 15147           prevNode = selection.getPreviousNode(),
 15148           selectedNode = selection.getSelectedNode(),
 15149           afterCaretNode;
 15151       if (selection.caretIsFirstInSelection()) {
 15152         if (selectedNode.nodeType === 3) {
 15153           selectedNode = selectedNode.parentNode;
 15154         }
 15155         afterCaretNode = selectedNode.firstChild;
 15156         domNode(selectedNode).transferContentTo(prevNode, true);
 15157         if (afterCaretNode) {
 15158           composer.selection.setBefore(afterCaretNode);
 15159         }
 15160         return true;
 15161       }
 15162       return false;
 15163     },
 15165     // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
 15166     // Returns true if some corrections is applied so events know when to prevent default
 15167     doLineBreaksModeEnterWithCaret: function(composer) {
 15168       var breakNodes = "p, pre, div, blockquote",
 15169           caretInfo, parent, txtNode,
 15170           ret = false;
 15172       caretInfo = composer.selection.getNodesNearCaret();
 15173       if (caretInfo) {
 15175         if (caretInfo.caretNode || caretInfo.nextNode) {
 15176           parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2);
 15177           if (parent === composer.element) {
 15178             parent = undefined;
 15179           }
 15180         }
 15182         if (parent && caretInfo.caretNode) {
 15183           if (domNode(caretInfo.caretNode).is.lineBreak()) {
 15185             if (composer.config.doubleLineBreakEscapesBlock) {
 15186               // Double enter (enter on blank line) exits block element in useLineBreaks mode.
 15187               ret = true;
 15188               caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode);
 15190               // Ensure surplous line breaks are not added to preceding element
 15191               if (domNode(caretInfo.nextNode).is.lineBreak()) {
 15192                 caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode);
 15193               }
 15195               var brNode = composer.doc.createElement('br');
 15196               if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) {
 15197                 parent.parentNode.insertBefore(brNode, parent.nextSibling);
 15198               } else {
 15199                 composer.selection.splitElementAtCaret(parent, brNode);
 15200               }
 15202               // Ensure surplous blank lines are not added to preceding element
 15203               if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) {
 15204                 // Replaces blank lines at the beginning of textnode
 15205                 caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, '');
 15206               }
 15207               composer.selection.setBefore(brNode);
 15208             }
 15210           } else if (caretInfo.caretNode.nodeType === 3 && wysihtml.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) {
 15212             // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens.
 15213             // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br
 15214             ret = true;
 15215             var br1 = composer.doc.createElement('br'),
 15216                 br2 = composer.doc.createElement('br'),
 15217                 f = composer.doc.createDocumentFragment();
 15218             f.appendChild(br1);
 15219             f.appendChild(br2);
 15220             composer.selection.insertNode(f);
 15221             composer.selection.setBefore(br2);
 15223           }
 15224         }
 15225       }
 15226       return ret;
 15227     }
 15228   };
 15230   var handleDeleteKeyPress = function(event, composer) {
 15231     var selection = composer.selection,
 15232         element = composer.element;
 15234     if (selection.isCollapsed()) {
 15235       /**
 15236        * when the editor is empty in useLineBreaks = false mode, preserve
 15237        * the default value in it which is <p><br></p>
 15238        */
 15239       if (composer.isEmpty() && !composer.config.useLineBreaks) {
 15240         event.preventDefault();
 15241         return;
 15242       }
 15243       if (actions.handleUneditableDeletion(composer)) {
 15244         event.preventDefault();
 15245         return;
 15246       }
 15247       if (actions.fixDeleteInTheBeginningOfLi(composer)) {
 15248         event.preventDefault();
 15249         return;
 15250       }
 15251       if (actions.fixDeleteInTheBeginningOfBlock(composer)) {
 15252         event.preventDefault();
 15253         return;
 15254       }
 15255       if (actions.fixLastBrDeletionInTable(composer)) {
 15256         event.preventDefault();
 15257         return;
 15258       }
 15259       if (wysihtml.browser.usesControlRanges()) {
 15260         if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) {
 15261           event.preventDefault();
 15262           return;
 15263         }
 15264       }
 15265     } else {
 15266       if (selection.containsUneditable()) {
 15267         event.preventDefault();
 15268         selection.deleteContents();
 15269       }
 15270     }
 15271   };
 15273   var handleEnterKeyPress = function(event, composer) {
 15274     if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) {
 15275       // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
 15277       var breakNodes = "p, pre, div, blockquote",
 15278           caretInfo, parent, txtNode;
 15280       if (composer.selection.isCollapsed()) {
 15281         if (actions.doLineBreaksModeEnterWithCaret(composer)) {
 15282           event.preventDefault();
 15283         }
 15284       }
 15285     }
 15287     if (browser.hasCaretAtLinkEndInsertionProblems() && composer.selection.caretIsInTheEndOfNode()) {
 15288       var target = composer.selection.getSelectedNode(true),
 15289           targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
 15290           invisibleSpace, space;
 15292       if (targetEl && targetEl.closest('a') && target.nodeType === 3 && target === targetEl.lastChild) {
 15293         // Seems like enter was pressed and caret was at the end of link node
 15294         // This means user wants to escape the link now (caret is last in link node too).
 15295         composer.selection.setAfter(targetEl);
 15296       }
 15297     }
 15298   };
 15300   var handleTabKeyDown = function(composer, element, shiftKey) {
 15301     if (!composer.selection.isCollapsed()) {
 15302       composer.selection.deleteContents();
 15303     } else if (composer.selection.caretIsInTheBeginnig('li')) {
 15304       if (shiftKey) {
 15305         if (composer.commands.exec('outdentList')) return;
 15306       } else {
 15307         if (composer.commands.exec('indentList')) return;
 15308       }
 15309     }
 15311     // Is   close enough to tab. Could not find enough counter arguments for now.
 15312     composer.commands.exec("insertHTML", " ");
 15313   };
 15315   var handleDomNodeRemoved = function(event) {
 15316       if (this.domNodeRemovedInterval) {
 15317         clearInterval(domNodeRemovedInterval);
 15318       }
 15319       this.parent.fire("destroy:composer");
 15320   };
 15322   // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires
 15323   var handleUserInteraction = function (event) {
 15324     this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event);
 15325     setTimeout((function() {
 15326       this.parent.fire("interaction", event).fire("interaction:composer", event);
 15327     }).bind(this), 0);
 15328   };
 15330   var handleFocus = function(event) {
 15331     this.parent.fire("focus", event).fire("focus:composer", event);
 15333     // Delay storing of state until all focus handler are fired
 15334     // especially the one which resets the placeholder
 15335     setTimeout((function() {
 15336       this.focusState = this.getValue(false, false);
 15337     }).bind(this), 0);
 15338   };
 15340   var handleBlur = function(event) {
 15341     if (this.focusState !== this.getValue(false, false)) {
 15342       //create change event if supported (all except IE8)
 15343       var changeevent = event;
 15344       if(typeof Object.create == 'function') {
 15345         changeevent = Object.create(event, { type: { value: 'change' } });
 15346       }
 15347       this.parent.fire("change", changeevent).fire("change:composer", changeevent);
 15348     }
 15349     this.parent.fire("blur", event).fire("blur:composer", event);
 15350   };
 15352   var handlePaste = function(event) {
 15353     this.parent.fire(event.type, event).fire(event.type + ":composer", event);
 15354     if (event.type === "paste") {
 15355       setTimeout((function() {
 15356         this.parent.fire("newword:composer");
 15357       }).bind(this), 0);
 15358     }
 15359   };
 15361   var handleCopy = function(event) {
 15362     if (this.config.copyedFromMarking) {
 15363       // If supported the copied source can be based directly on selection
 15364       // 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.
 15365       if (wysihtml.browser.supportsModernPaste()) {
 15366         event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml());
 15367         event.clipboardData.setData("text/plain", this.selection.getPlainText());
 15368         event.preventDefault();
 15369       }
 15370       this.parent.fire(event.type, event).fire(event.type + ":composer", event);
 15371     }
 15372   };
 15374   var handleKeyUp = function(event) {
 15375     var keyCode = event.keyCode;
 15376     if (keyCode === wysihtml.SPACE_KEY || keyCode === wysihtml.ENTER_KEY) {
 15377       this.parent.fire("newword:composer");
 15378     }
 15379   };
 15381   var handleMouseDown = function(event) {
 15382     if (!browser.canSelectImagesInContentEditable()) {
 15383       // Make sure that images are selected when clicking on them
 15384       var target = event.target,
 15385           allImages = this.element.querySelectorAll('img'),
 15386           notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'),
 15387           myImages = wysihtml.lang.array(allImages).without(notMyImages);
 15389       if (target.nodeName === "IMG" && wysihtml.lang.array(myImages).contains(target)) {
 15390         this.selection.selectNode(target);
 15391       }
 15392     }
 15394     // Saves mousedown position for IE controlSelect fix
 15395     if (wysihtml.browser.usesControlRanges()) {
 15396       this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY};
 15397       setTimeout(function() {
 15398         delete this.selection.lastMouseDownPos;
 15399       }.bind(this), 0);
 15400     }
 15401   };
 15403   // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text)
 15404   // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made
 15405   var handleIEControlSelect = function(event) {
 15406     var target = event.target,
 15407         pos = this.selection.lastMouseDownPos;
 15408     if (pos) {
 15409       var caretPosition = document.body.createTextRange();
 15410         setTimeout(function() {
 15411           try {
 15412             caretPosition.moveToPoint(pos.x, pos.y);
 15413             caretPosition.select();
 15414           } catch (e) {}
 15415         }.bind(this), 0);
 15416     }
 15417   };
 15419   var handleClick = function(event) {
 15420     if (this.config.classNames.uneditableContainer) {
 15421       // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
 15422       // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour
 15423       var uneditable = wysihtml.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element);
 15424       if (uneditable) {
 15425         this.selection.setAfter(uneditable);
 15426       }
 15427     }
 15428   };
 15430   var handleDrop = function(event) {
 15431     if (!browser.canSelectImagesInContentEditable()) {
 15432       // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
 15433       setTimeout((function() {
 15434         this.selection.getSelection().removeAllRanges();
 15435       }).bind(this), 0);
 15436     }
 15437   };
 15439   var handleKeyDown = function(event) {
 15440     var keyCode = event.keyCode,
 15441         command = shortcuts[keyCode],
 15442         target = this.selection.getSelectedNode(true),
 15443         targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
 15444         parent;
 15446     // Select all (meta/ctrl + a)
 15447     if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) {
 15448       this.selection.selectAll();
 15449       event.preventDefault();
 15450       return;
 15451     }
 15453     // Shortcut logic
 15454     if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
 15455       this.commands.exec(command);
 15456       event.preventDefault();
 15457     }
 15459     if (keyCode === wysihtml.BACKSPACE_KEY) {
 15460       // Delete key override for special cases
 15461       handleDeleteKeyPress(event, this);
 15462     }
 15464     // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor
 15465     if (keyCode === wysihtml.BACKSPACE_KEY || keyCode === wysihtml.DELETE_KEY) {
 15466       if (target && target.nodeName === "IMG") {
 15467         event.preventDefault();
 15468         parent = target.parentNode;
 15469         parent.removeChild(target);// delete the <img>
 15470         // And it's parent <a> too if it hasn't got any other child nodes
 15471         if (parent.nodeName === "A" && !parent.firstChild) {
 15472           parent.parentNode.removeChild(parent);
 15473         }
 15474         setTimeout((function() {
 15475           wysihtml.quirks.redraw(this.element);
 15476         }).bind(this), 0);
 15477       }
 15478     }
 15480     if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) {
 15481       // TAB key handling
 15482       event.preventDefault();
 15483       handleTabKeyDown(this, this.element, event.shiftKey);
 15484     }
 15486     if (keyCode === wysihtml.ENTER_KEY) {
 15487       handleEnterKeyPress(event, this);
 15488     }
 15490   };
 15492   var handleKeyPress = function(event) {
 15494     // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.)
 15495     if (event.which !== 0) {
 15497       // Test if caret is last in a link in webkit and try to fix webkit problem,
 15498       // that all inserted content is added outside of link.
 15499       // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area.
 15500       // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links.
 15501       // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text.
 15502       // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it.
 15503       // Also adds link escaping by double space with caret at the end of link for all browsers
 15505       if (this.selection.caretIsInTheEndOfNode()) {
 15506         var target = this.selection.getSelectedNode(true),
 15507             targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
 15508             invisibleSpace, space;
 15510         if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) {
 15512           if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) {
 15513             // Executed if there is no whitespace before caret in textnode in case of pressing space.
 15514             // Whitespace before marks that user wants to escape the node by pressing double space.
 15515             // Otherwise insert the character in the link not out as it would like to go natively
 15517             invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
 15518             this.selection.insertNode(invisibleSpace);
 15519             this.selection.setBefore(invisibleSpace);
 15520             setTimeout(function() {
 15522               if (invisibleSpace.textContent.length > 1) {
 15523                 invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, '');
 15524                 this.selection.setAfter(invisibleSpace);
 15525               } else {
 15526                 invisibleSpace.remove();
 15527               }
 15529             }.bind(this), 0);
 15530           } else if (event.which === 32) {
 15531             // Seems like space was pressed and there was a space before the caret allready
 15532             // This means user wants to escape the link now (caret is last in link node too) so we let the native browser do it-s job and escape.
 15533             // But lets move the trailing space too out of link if present
 15535             if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) {
 15537               target.textContent = target.textContent.replace(/[\u00A0 ]$/, '');
 15538               space = this.doc.createTextNode(' ');
 15539               targetEl.parentNode.insertBefore(space, targetEl.nextSibling);
 15540               this.selection.setAfter(space, false);
 15541               event.preventDefault();
 15543             }
 15544           }
 15545         }
 15546       }
 15547     }
 15548   }
 15550   var handleIframeFocus = function(event) {
 15551     setTimeout((function() {
 15552       if (this.doc.querySelector(":focus") !== this.element) {
 15553         this.focus();
 15554       }
 15555     }).bind(this), 0);
 15556   };
 15558   var handleIframeBlur = function(event) {
 15559     setTimeout((function() {
 15560       this.selection.getSelection().removeAllRanges();
 15561     }).bind(this), 0);
 15562   };
 15564   // Testing requires actions to be accessible from out of scope
 15565   wysihtml.views.Composer.prototype.observeActions = actions;
 15567   wysihtml.views.Composer.prototype.observe = function() {
 15568     var that                = this,
 15569         container           = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
 15570         element             = this.element,
 15571         focusBlurElement    = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow();
 15573     this.focusState = this.getValue(false, false);
 15574     this.actions = actions;
 15576     // --------- destroy:composer event ---------
 15577     container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false);
 15579     // DOMNodeRemoved event is not supported in IE 8
 15580     // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed
 15581     if (!browser.supportsMutationEvents()) {
 15582       this.domNodeRemovedInterval = setInterval(function() {
 15583         if (!dom.contains(document.documentElement, container)) {
 15584           handleDomNodeRemoved.call(this);
 15585         }
 15586       }, 250);
 15587     }
 15589     actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this));
 15590     focusBlurElement.addEventListener('focus', handleFocus.bind(this), false);
 15591     focusBlurElement.addEventListener('blur',  handleBlur.bind(this), false);
 15593     actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false);
 15594     this.element.addEventListener('copy',       handleCopy.bind(this), false);
 15595     this.element.addEventListener('mousedown',  handleMouseDown.bind(this), false);
 15596     this.element.addEventListener('click',      handleClick.bind(this), false);
 15597     this.element.addEventListener('drop',       handleDrop.bind(this), false);
 15598     this.element.addEventListener('keyup',      handleKeyUp.bind(this), false);
 15599     this.element.addEventListener('keydown',    handleKeyDown.bind(this), false);
 15600     this.element.addEventListener('keypress',   handleKeyPress.bind(this), false);
 15602     // IE controlselect madness fix
 15603     if (wysihtml.browser.usesControlRanges()) {
 15604       this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false);
 15605     }
 15607     this.element.addEventListener("dragenter", (function() {
 15608       this.parent.fire("unset_placeholder");
 15609     }).bind(this), false);
 15611   };
 15612 })(wysihtml);
 15614 /**
 15615  * Class that takes care that the value of the composer and the textarea is always in sync
 15616  */
 15617 (function(wysihtml) {
 15618   var INTERVAL = 400;
 15620   wysihtml.views.Synchronizer = Base.extend(
 15621     /** @scope wysihtml.views.Synchronizer.prototype */ {
 15623     constructor: function(editor, textarea, composer) {
 15624       this.editor   = editor;
 15625       this.textarea = textarea;
 15626       this.composer = composer;
 15628       this._observe();
 15629     },
 15631     /**
 15632      * Sync html from composer to textarea
 15633      * Takes care of placeholders
 15634      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
 15635      */
 15636     fromComposerToTextarea: function(shouldParseHtml) {
 15637       this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
 15638     },
 15640     /**
 15641      * Sync value of textarea to composer
 15642      * Takes care of placeholders
 15643      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
 15644      */
 15645     fromTextareaToComposer: function(shouldParseHtml) {
 15646       var textareaValue = this.textarea.getValue(false, false);
 15647       if (textareaValue) {
 15648         this.composer.setValue(textareaValue, shouldParseHtml);
 15649       } else {
 15650         this.composer.clear();
 15651         this.editor.fire("set_placeholder");
 15652       }
 15653     },
 15655     /**
 15656      * Invoke syncing based on view state
 15657      * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
 15658      */
 15659     sync: function(shouldParseHtml) {
 15660       if (this.editor.currentView.name === "textarea") {
 15661         this.fromTextareaToComposer(shouldParseHtml);
 15662       } else {
 15663         this.fromComposerToTextarea(shouldParseHtml);
 15664       }
 15665     },
 15667     /**
 15668      * Initializes interval-based syncing
 15669      * also makes sure that on-submit the composer's content is synced with the textarea
 15670      * immediately when the form gets submitted
 15671      */
 15672     _observe: function() {
 15673       var interval,
 15674           that          = this,
 15675           form          = this.textarea.element.form,
 15676           startInterval = function() {
 15677             interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
 15678           },
 15679           stopInterval  = function() {
 15680             clearInterval(interval);
 15681             interval = null;
 15682           };
 15684       startInterval();
 15686       if (form) {
 15687         // If the textarea is in a form make sure that after onreset and onsubmit the composer
 15688         // has the correct state
 15689         wysihtml.dom.observe(form, "submit", function() {
 15690           that.sync(true);
 15691         });
 15692         wysihtml.dom.observe(form, "reset", function() {
 15693           setTimeout(function() { that.fromTextareaToComposer(); }, 0);
 15694         });
 15695       }
 15697       this.editor.on("change_view", function(view) {
 15698         if (view === "composer" && !interval) {
 15699           that.fromTextareaToComposer(true);
 15700           startInterval();
 15701         } else if (view === "textarea") {
 15702           that.fromComposerToTextarea(true);
 15703           stopInterval();
 15704         }
 15705       });
 15707       this.editor.on("destroy:composer", stopInterval);
 15708     }
 15709   });
 15710 })(wysihtml);
 15712 (function(wysihtml) {
 15714   wysihtml.views.SourceView = Base.extend(
 15715     /** @scope wysihtml.views.SourceView.prototype */ {
 15717     constructor: function(editor, composer) {
 15718       this.editor   = editor;
 15719       this.composer = composer;
 15721       this._observe();
 15722     },
 15724     switchToTextarea: function(shouldParseHtml) {
 15725       var composerStyles = this.composer.win.getComputedStyle(this.composer.element),
 15726           width = parseFloat(composerStyles.width),
 15727           height = Math.max(parseFloat(composerStyles.height), 100);
 15729       if (!this.textarea) {
 15730         this.textarea = this.composer.doc.createElement('textarea');
 15731         this.textarea.className = "wysihtml-source-view";
 15732       }
 15733       this.textarea.style.width = width + 'px';
 15734       this.textarea.style.height = height + 'px';
 15735       this.textarea.value = this.editor.getValue(shouldParseHtml, true);
 15736       this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element);
 15737       this.editor.currentView = "source";
 15738       this.composer.element.style.display = 'none';
 15739     },
 15741     switchToComposer: function(shouldParseHtml) {
 15742       var textareaValue = this.textarea.value;
 15743       if (textareaValue) {
 15744         this.composer.setValue(textareaValue, shouldParseHtml);
 15745       } else {
 15746         this.composer.clear();
 15747         this.editor.fire("set_placeholder");
 15748       }
 15749       this.textarea.parentNode.removeChild(this.textarea);
 15750       this.editor.currentView = this.composer;
 15751       this.composer.element.style.display = '';
 15752     },
 15754     _observe: function() {
 15755       this.editor.on("change_view", function(view) {
 15756         if (view === "composer") {
 15757           this.switchToComposer(true);
 15758         } else if (view === "textarea") {
 15759           this.switchToTextarea(true);
 15760         }
 15761       }.bind(this));
 15762     }
 15764   });
 15766 })(wysihtml);
 15768 wysihtml.views.Textarea = wysihtml.views.View.extend(
 15769   /** @scope wysihtml.views.Textarea.prototype */ {
 15770   name: "textarea",
 15772   constructor: function(parent, textareaElement, config) {
 15773     this.base(parent, textareaElement, config);
 15775     this._observe();
 15776   },
 15778   clear: function() {
 15779     this.element.value = "";
 15780   },
 15782   getValue: function(parse) {
 15783     var value = this.isEmpty() ? "" : this.element.value;
 15784     if (parse !== false) {
 15785       value = this.parent.parse(value);
 15786     }
 15787     return value;
 15788   },
 15790   setValue: function(html, parse) {
 15791     if (parse !== false) {
 15792       html = this.parent.parse(html);
 15793     }
 15794     this.element.value = html;
 15795   },
 15797   cleanUp: function(rules) {
 15798       var html = this.parent.parse(this.element.value, undefined, rules);
 15799       this.element.value = html;
 15800   },
 15802   hasPlaceholderSet: function() {
 15803     var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element),
 15804         placeholderText     = this.element.getAttribute("placeholder") || null,
 15805         value               = this.element.value,
 15806         isEmpty             = !value;
 15807     return (supportsPlaceholder && isEmpty) || (value === placeholderText);
 15808   },
 15810   isEmpty: function() {
 15811     return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
 15812   },
 15814   _observe: function() {
 15815     var element = this.element,
 15816         parent  = this.parent,
 15817         eventMapping = {
 15818           focusin:  "focus",
 15819           focusout: "blur"
 15820         },
 15821         /**
 15822          * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
 15823          * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
 15824          */
 15825         events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
 15827     parent.on("beforeload", function() {
 15828       wysihtml.dom.observe(element, events, function(event) {
 15829         var eventName = eventMapping[event.type] || event.type;
 15830         parent.fire(eventName).fire(eventName + ":textarea");
 15831       });
 15833       wysihtml.dom.observe(element, ["paste", "drop"], function() {
 15834         setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
 15835       });
 15836     });
 15837   }
 15838 });
 15840 /**
 15841  * WYSIHTML Editor
 15842  *
 15843  * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
 15844  * @param {Object} [config] See defaults object below for explanation of each individual config option
 15845  *
 15846  * @events
 15847  *    load
 15848  *    beforeload (for internal use only)
 15849  *    focus
 15850  *    focus:composer
 15851  *    focus:textarea
 15852  *    blur
 15853  *    blur:composer
 15854  *    blur:textarea
 15855  *    change
 15856  *    change:composer
 15857  *    change:textarea
 15858  *    paste
 15859  *    paste:composer
 15860  *    paste:textarea
 15861  *    newword:composer
 15862  *    destroy:composer
 15863  *    undo:composer
 15864  *    redo:composer
 15865  *    beforecommand:composer
 15866  *    aftercommand:composer
 15867  *    enable:composer
 15868  *    disable:composer
 15869  *    change_view
 15870  */
 15871 (function(wysihtml) {
 15872   var undef;
 15874   wysihtml.Editor = wysihtml.lang.Dispatcher.extend({
 15875     /** @scope wysihtml.Editor.prototype */
 15876     defaults: {
 15877       // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
 15878       name:                 undef,
 15879       // Whether the editor should look like the textarea (by adopting styles)
 15880       style:                true,
 15881       // Whether urls, entered by the user should automatically become clickable-links
 15882       autoLink:             true,
 15883       // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
 15884       handleTabKey:         true,
 15885       // Object which includes parser rules to apply when html gets cleaned
 15886       // See parser_rules/*.js for examples
 15887       parserRules:          { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} },
 15888       // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
 15889       pasteParserRulesets: null,
 15890       // Parser method to use when the user inserts content
 15891       parser:               wysihtml.dom.parse,
 15892       // By default wysihtml will insert a <br> for line breaks, set this to false to use <p>
 15893       useLineBreaks:        true,
 15894       // Double enter (enter on blank line) exits block element in useLineBreaks mode.
 15895       // It enables a way of escaping out of block elements and splitting block elements
 15896       doubleLineBreakEscapesBlock: true,
 15897       // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
 15898       stylesheets:          [],
 15899       // Placeholder text to use, defaults to the placeholder attribute on the textarea element
 15900       placeholderText:      undef,
 15901       // Whether the rich text editor should be rendered on touch devices (wysihtml >= 0.3.0 comes with basic support for iOS 5)
 15902       supportTouchDevices:  true,
 15903       // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
 15904       cleanUp:              true,
 15905       // Whether to use div instead of secure iframe
 15906       contentEditableMode: false,
 15907       classNames: {
 15908         // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
 15909         composer: "wysihtml-editor",
 15910         // Class name to add to the body when the wysihtml editor is supported
 15911         body: "wysihtml-supported",
 15912         // classname added to editable area element (iframe/div) on creation
 15913         sandbox: "wysihtml-sandbox",
 15914         // class on editable area with placeholder
 15915         placeholder: "wysihtml-placeholder",
 15916         // Classname of container that editor should not touch and pass through
 15917         uneditableContainer: "wysihtml-uneditable-container"
 15918       },
 15919       // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
 15920       // Also copied source is based directly on selection - 
 15921       // (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).
 15922       // If falsy value is passed source override is also disabled
 15923       copyedFromMarking: '<meta name="copied-from" content="wysihtml">'
 15924     },
 15926     constructor: function(editableElement, config) {
 15927       this.editableElement  = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
 15928       this.config           = wysihtml.lang.object({}).merge(this.defaults).merge(config).get();
 15929       this._isCompatible    = wysihtml.browser.supported();
 15931       // merge classNames
 15932       if (config && config.classNames) {
 15933         wysihtml.lang.object(this.config.classNames).merge(config.classNames);
 15934       }
 15936       if (this.editableElement.nodeName.toLowerCase() != "textarea") {
 15937           this.config.contentEditableMode = true;
 15938           this.config.noTextarea = true;
 15939       }
 15940       if (!this.config.noTextarea) {
 15941           this.textarea         = new wysihtml.views.Textarea(this, this.editableElement, this.config);
 15942           this.currentView      = this.textarea;
 15943       }
 15945       // Sort out unsupported/unwanted browsers here
 15946       if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml.browser.isTouchDevice())) {
 15947         var that = this;
 15948         setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
 15949         return;
 15950       }
 15952       // Add class name to body, to indicate that the editor is supported
 15953       wysihtml.dom.addClass(document.body, this.config.classNames.body);
 15955       this.composer = new wysihtml.views.Composer(this, this.editableElement, this.config);
 15956       this.currentView = this.composer;
 15958       if (typeof(this.config.parser) === "function") {
 15959         this._initParser();
 15960       }
 15962       this.on("beforeload", this.handleBeforeLoad);
 15963     },
 15965     handleBeforeLoad: function() {
 15966         if (!this.config.noTextarea) {
 15967           this.synchronizer = new wysihtml.views.Synchronizer(this, this.textarea, this.composer);
 15968         } else {
 15969           this.sourceView = new wysihtml.views.SourceView(this, this.composer);
 15970         }
 15971         this.runEditorExtenders();
 15972     },
 15974     runEditorExtenders: function() {
 15975       wysihtml.editorExtenders.forEach(function(extender) {
 15976         extender(this);
 15977       }.bind(this));
 15978     },
 15980     isCompatible: function() {
 15981       return this._isCompatible;
 15982     },
 15984     clear: function() {
 15985       this.currentView.clear();
 15986       return this;
 15987     },
 15989     getValue: function(parse, clearInternals) {
 15990       return this.currentView.getValue(parse, clearInternals);
 15991     },
 15993     setValue: function(html, parse) {
 15994       this.fire("unset_placeholder");
 15996       if (!html) {
 15997         return this.clear();
 15998       }
 16000       this.currentView.setValue(html, parse);
 16001       return this;
 16002     },
 16004     cleanUp: function(rules) {
 16005         this.currentView.cleanUp(rules);
 16006     },
 16008     focus: function(setToEnd) {
 16009       this.currentView.focus(setToEnd);
 16010       return this;
 16011     },
 16013     /**
 16014      * Deactivate editor (make it readonly)
 16015      */
 16016     disable: function() {
 16017       this.currentView.disable();
 16018       return this;
 16019     },
 16021     /**
 16022      * Activate editor
 16023      */
 16024     enable: function() {
 16025       this.currentView.enable();
 16026       return this;
 16027     },
 16029     isEmpty: function() {
 16030       return this.currentView.isEmpty();
 16031     },
 16033     hasPlaceholderSet: function() {
 16034       return this.currentView.hasPlaceholderSet();
 16035     },
 16037     destroy: function() {
 16038       if (this.composer && this.composer.sandbox) {
 16039         this.composer.sandbox.destroy();
 16040       }
 16041       this.fire("destroy:composer");
 16042       this.off();
 16043     },
 16045     parse: function(htmlOrElement, clearInternals, customRules) {
 16046       var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
 16047       var returnValue = this.config.parser(htmlOrElement, {
 16048         "rules": customRules || this.config.parserRules,
 16049         "cleanUp": this.config.cleanUp,
 16050         "context": parseContext,
 16051         "uneditableClass": this.config.classNames.uneditableContainer,
 16052         "clearInternals" : clearInternals
 16053       });
 16054       if (typeof(htmlOrElement) === "object") {
 16055         wysihtml.quirks.redraw(htmlOrElement);
 16056       }
 16057       return returnValue;
 16058     },
 16060     /**
 16061      * Prepare html parser logic
 16062      *  - Observes for paste and drop
 16063      */
 16064     _initParser: function() {
 16065       var oldHtml;
 16067       if (wysihtml.browser.supportsModernPaste()) {
 16068         this.on("paste:composer", function(event) {
 16069           event.preventDefault();
 16070           oldHtml = wysihtml.dom.getPastedHtml(event);
 16071           if (oldHtml) {
 16072             this._cleanAndPaste(oldHtml);
 16073           }
 16074         }.bind(this));
 16076       } else {
 16077         this.on("beforepaste:composer", function(event) {
 16078           event.preventDefault();
 16079           var scrollPos = this.composer.getScrollPos();
 16081           wysihtml.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) {
 16082             if (pastedHTML) {
 16083               this._cleanAndPaste(pastedHTML);
 16084             }
 16085             this.composer.setScrollPos(scrollPos);
 16086           }.bind(this));
 16088         }.bind(this));
 16089       }
 16090     },
 16092     _cleanAndPaste: function (oldHtml) {
 16093       var cleanHtml = wysihtml.quirks.cleanPastedHTML(oldHtml, {
 16094         "referenceNode": this.composer.element,
 16095         "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
 16096         "uneditableClass": this.config.classNames.uneditableContainer
 16097       });
 16098       this.composer.selection.deleteContents();
 16099       this.composer.selection.insertHTML(cleanHtml);
 16100     }
 16101   });
 16102 })(wysihtml);
